mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:00:43 +00:00
fix(gateway): skip local model pricing refreshes
This commit is contained in:
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Gateway/models: skip external OpenRouter and LiteLLM pricing refreshes for local/self-hosted model endpoints so startup does not wait on remote pricing catalogs for local-only Ollama, vLLM, and compatible providers. Thanks @codex.
|
||||
- CLI/plugins: stop security-blocked plugin installs from retrying as hook packs, so normal plugin packages report the scanner failure without a misleading "not a valid hook pack" follow-up. Fixes #61175; supersedes #64102. Thanks @KonsultDigital and @ziyincody.
|
||||
- Control UI/Dreaming: require explicit confirmation before applying restart-impacting Dreaming mode changes, with restart warning copy and loading feedback. Fixes #63804. (#63807) Thanks @bbddbb1.
|
||||
- CLI/update: keep the automatic post-update completion refresh on the core-command tree so it no longer stages bundled plugin runtime deps before the Gateway restart path, avoiding `.24` update hangs and 1006 disconnect cascades. Fixes #72665. Thanks @sakalaboator and @He-Pin.
|
||||
|
||||
@@ -121,6 +121,41 @@ describe("model-pricing-cache", () => {
|
||||
expect(refs).toContain("tavily/search-preview");
|
||||
});
|
||||
|
||||
it("skips remote pricing catalogs for local-only model providers", async () => {
|
||||
const config = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "ollama/llama3.2:latest" },
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
ollama: {
|
||||
baseUrl: "http://127.0.0.1:11434",
|
||||
api: "ollama",
|
||||
models: [{ id: "llama3.2:latest" }],
|
||||
},
|
||||
vllm: {
|
||||
baseUrl: "http://192.168.1.25:8000/v1",
|
||||
api: "openai-completions",
|
||||
models: [{ id: "qwen2.5-coder:7b" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
subagents: { model: { primary: "vllm/qwen2.5-coder:7b" } },
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
const fetchImpl = vi.fn<typeof fetch>();
|
||||
|
||||
await refreshGatewayModelPricingCache({ config, fetchImpl });
|
||||
|
||||
expect(fetchImpl).not.toHaveBeenCalled();
|
||||
expect(
|
||||
getCachedGatewayModelPricing({ provider: "ollama", model: "llama3.2:latest" }),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("loads openrouter pricing and maps provider aliases, wrappers, and anthropic dotted ids", async () => {
|
||||
const config = {
|
||||
agents: {
|
||||
|
||||
@@ -58,6 +58,8 @@ const WRAPPER_PROVIDERS = new Set([
|
||||
"openrouter",
|
||||
"vercel-ai-gateway",
|
||||
]);
|
||||
const LOCAL_MODEL_PROVIDER_APIS = new Set(["ollama"]);
|
||||
const LOCAL_MODEL_PROVIDER_IDS = new Set(["lmstudio", "ollama", "sglang", "vllm"]);
|
||||
const log = createSubsystemLogger("gateway").child("model-pricing");
|
||||
|
||||
let refreshTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
@@ -422,6 +424,60 @@ function addConfiguredWebSearchPluginModels(params: {
|
||||
}
|
||||
}
|
||||
|
||||
function isPrivateOrLoopbackHost(hostname: string): boolean {
|
||||
const host = hostname
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^\[|\]$/g, "");
|
||||
if (
|
||||
host === "localhost" ||
|
||||
host === "localhost.localdomain" ||
|
||||
host.endsWith(".localhost") ||
|
||||
host.endsWith(".local")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (host === "::1" || host === "0:0:0:0:0:0:0:1" || host.startsWith("fe80:")) {
|
||||
return true;
|
||||
}
|
||||
if (host.startsWith("fc") || host.startsWith("fd")) {
|
||||
return true;
|
||||
}
|
||||
if (host.startsWith("127.") || host.startsWith("10.") || host.startsWith("192.168.")) {
|
||||
return true;
|
||||
}
|
||||
return /^172\.(1[6-9]|2\d|3[0-1])\./u.test(host) || host.startsWith("169.254.");
|
||||
}
|
||||
|
||||
function isPrivateOrLoopbackBaseUrl(baseUrl: string | undefined): boolean {
|
||||
if (!baseUrl) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return isPrivateOrLoopbackHost(new URL(baseUrl).hostname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldFetchExternalPricingForRef(config: OpenClawConfig, ref: ModelRef): boolean {
|
||||
const providerConfig = config.models?.providers?.[ref.provider];
|
||||
if (providerConfig?.api && LOCAL_MODEL_PROVIDER_APIS.has(providerConfig.api)) {
|
||||
return false;
|
||||
}
|
||||
if (LOCAL_MODEL_PROVIDER_IDS.has(ref.provider)) {
|
||||
return false;
|
||||
}
|
||||
if (isPrivateOrLoopbackBaseUrl(providerConfig?.baseUrl)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function filterExternalPricingRefs(config: OpenClawConfig, refs: ModelRef[]): ModelRef[] {
|
||||
return refs.filter((ref) => shouldFetchExternalPricingForRef(config, ref));
|
||||
}
|
||||
|
||||
export function collectConfiguredModelPricingRefs(config: OpenClawConfig): ModelRef[] {
|
||||
const refs = new Map<string, ModelRef>();
|
||||
const aliasIndex = buildModelAliasIndex({
|
||||
@@ -547,7 +603,10 @@ export async function refreshGatewayModelPricingCache(params: {
|
||||
}
|
||||
const fetchImpl = params.fetchImpl ?? fetch;
|
||||
inFlightRefresh = (async () => {
|
||||
const refs = collectConfiguredModelPricingRefs(params.config);
|
||||
const refs = filterExternalPricingRefs(
|
||||
params.config,
|
||||
collectConfiguredModelPricingRefs(params.config),
|
||||
);
|
||||
if (refs.length === 0) {
|
||||
replaceGatewayModelPricingCache(new Map());
|
||||
clearRefreshTimer();
|
||||
|
||||
Reference in New Issue
Block a user