fix(gateway): skip local model pricing refreshes

This commit is contained in:
Peter Steinberger
2026-04-27 09:08:21 +01:00
parent 563718c2e4
commit 5ff49ae03e
3 changed files with 96 additions and 1 deletions

View File

@@ -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.

View File

@@ -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: {

View File

@@ -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();