mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:10:45 +00:00
fix(gateway): skip plugin pricing scans when disabled
This commit is contained in:
@@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI/model probes: reject empty or whitespace-only `infer model run --prompt` values before calling local providers or the Gateway, so smoke checks do not spend provider calls on invalid turns. Fixes #73185. Thanks @iot2edge.
|
||||
- Gateway/media: route text-only `chat.send` image offloads through media-understanding fields so `agents.defaults.imageModel` can describe WebChat attachments instead of leaving only an opaque `media://inbound` marker. Fixes #72968. Thanks @vorajeeah.
|
||||
- Gateway/Windows: route no-listener restart handoffs through the Windows supervisor without leaving restart tokens in flight, so failed task scheduling can be retried and successful handoffs do not coalesce later restart requests. (#69056) Thanks @Thatgfsj.
|
||||
- Gateway/model pricing: skip plugin manifest discovery during background pricing refreshes when `plugins.enabled: false`, so disabled-plugin setups do not keep rebuilding plugin metadata from the Gateway hot path. Fixes #73291. Thanks @slideshow-dingo and @fishgills.
|
||||
- Gateway/sessions: remove automatic oversized `sessions.json` rotation backups, deprecate `session.maintenance.rotateBytes`, and teach `openclaw doctor --fix` to remove the ignored key so hot session writes no longer copy multi-MB stores. Refs #72338. Thanks @midhunmonachan and @DougButdorf.
|
||||
- Channels/Telegram: fail fast when Telegram rejects the startup `getMe` token probe with 401, so invalid or stale BotFather tokens are reported as token auth failures instead of misleading `deleteWebhook` cleanup failures. Fixes #47674. Thanks @samaedan-arch.
|
||||
- ACPX: keep generated Codex and Claude ACP wrapper startup paths working when remote or special state filesystems reject chmod, since OpenClaw invokes the wrappers through Node instead of executing them directly. Fixes #73333. Thanks @david-garcia-garcia.
|
||||
|
||||
@@ -23,7 +23,14 @@ export function modelKey(provider: string, model: string): string {
|
||||
: `${providerId}/${modelId}`;
|
||||
}
|
||||
|
||||
export function normalizeStaticProviderModelId(provider: string, model: string): string {
|
||||
export function normalizeStaticProviderModelId(
|
||||
provider: string,
|
||||
model: string,
|
||||
options: { allowManifestNormalization?: boolean } = {},
|
||||
): string {
|
||||
if (options.allowManifestNormalization === false) {
|
||||
return model;
|
||||
}
|
||||
return (
|
||||
normalizeProviderModelIdWithManifest({
|
||||
provider,
|
||||
|
||||
@@ -38,9 +38,11 @@ export {
|
||||
function normalizeProviderModelId(
|
||||
provider: string,
|
||||
model: string,
|
||||
options?: { allowPluginNormalization?: boolean },
|
||||
options?: { allowManifestNormalization?: boolean; allowPluginNormalization?: boolean },
|
||||
): string {
|
||||
const staticModelId = normalizeStaticProviderModelId(provider, model);
|
||||
const staticModelId = normalizeStaticProviderModelId(provider, model, {
|
||||
allowManifestNormalization: options?.allowManifestNormalization,
|
||||
});
|
||||
if (options?.allowPluginNormalization === false) {
|
||||
return staticModelId;
|
||||
}
|
||||
@@ -56,6 +58,7 @@ function normalizeProviderModelId(
|
||||
}
|
||||
|
||||
type ModelRefNormalizeOptions = {
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -165,6 +165,7 @@ function isConcreteOpenRouterFreeModelRef(ref: ModelRef): boolean {
|
||||
function resolveConfiguredOpenRouterCompatFreeRef(params: {
|
||||
cfg: OpenClawConfig;
|
||||
defaultProvider: string;
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
}): ModelRef | null {
|
||||
const configuredModels = params.cfg.agents?.defaults?.models ?? {};
|
||||
@@ -173,6 +174,7 @@ function resolveConfiguredOpenRouterCompatFreeRef(params: {
|
||||
continue;
|
||||
}
|
||||
const parsed = parseModelRef(raw, params.defaultProvider, {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
if (parsed && isConcreteOpenRouterFreeModelRef(parsed)) {
|
||||
@@ -190,6 +192,7 @@ function resolveConfiguredOpenRouterCompatFreeRef(params: {
|
||||
continue;
|
||||
}
|
||||
return normalizeModelRef("openrouter", modelId, {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
}
|
||||
@@ -201,11 +204,13 @@ export function resolveConfiguredOpenRouterCompatAlias(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
raw: string;
|
||||
defaultProvider: string;
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
}): ModelRef | null {
|
||||
const normalized = normalizeLowercaseStringOrEmpty(params.raw);
|
||||
if (normalized === "openrouter:auto") {
|
||||
return normalizeModelRef("openrouter", "auto", {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
}
|
||||
@@ -215,6 +220,7 @@ export function resolveConfiguredOpenRouterCompatAlias(params: {
|
||||
return resolveConfiguredOpenRouterCompatFreeRef({
|
||||
cfg: params.cfg,
|
||||
defaultProvider: params.defaultProvider,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
}
|
||||
@@ -223,12 +229,14 @@ export function parseModelRefWithCompatAlias(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
raw: string;
|
||||
defaultProvider: string;
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
}): ModelRef | null {
|
||||
return (
|
||||
resolveConfiguredOpenRouterCompatAlias(params) ??
|
||||
resolveExactConfiguredProviderRef(params) ??
|
||||
parseModelRef(params.raw, params.defaultProvider, {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
})
|
||||
);
|
||||
@@ -237,6 +245,7 @@ export function parseModelRefWithCompatAlias(params: {
|
||||
function resolveExactConfiguredProviderRef(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
raw: string;
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
}): ModelRef | null {
|
||||
const slash = params.raw.indexOf("/");
|
||||
@@ -265,7 +274,9 @@ function resolveExactConfiguredProviderRef(params: {
|
||||
const provider = normalizeLowercaseStringOrEmpty(configuredProvider);
|
||||
return {
|
||||
provider,
|
||||
model: normalizeStaticProviderModelId(provider, modelRaw.trim()),
|
||||
model: normalizeStaticProviderModelId(provider, modelRaw.trim(), {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -311,6 +322,7 @@ export function buildConfiguredAllowlistKeys(params: {
|
||||
export function buildModelAliasIndex(params: {
|
||||
cfg: OpenClawConfig;
|
||||
defaultProvider: string;
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
}): ModelAliasIndex {
|
||||
const byAlias = new Map<string, { alias: string; ref: ModelRef }>();
|
||||
@@ -322,6 +334,7 @@ export function buildModelAliasIndex(params: {
|
||||
cfg: params.cfg,
|
||||
raw: keyRaw,
|
||||
defaultProvider: params.defaultProvider,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
if (!parsed) {
|
||||
@@ -430,6 +443,7 @@ export function resolveModelRefFromString(params: {
|
||||
raw: string;
|
||||
defaultProvider: string;
|
||||
aliasIndex?: ModelAliasIndex;
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
}): { ref: ModelRef; alias?: string } | null {
|
||||
const { model } = splitTrailingAuthProfile(params.raw);
|
||||
@@ -447,6 +461,7 @@ export function resolveModelRefFromString(params: {
|
||||
cfg: params.cfg,
|
||||
raw: model,
|
||||
defaultProvider: params.defaultProvider,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
if (!parsed) {
|
||||
@@ -459,6 +474,7 @@ export function resolveConfiguredModelRef(params: {
|
||||
cfg: OpenClawConfig;
|
||||
defaultProvider: string;
|
||||
defaultModel: string;
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
}): ModelRef {
|
||||
const rawModel = resolveAgentModelPrimaryValue(params.cfg.agents?.defaults?.model) ?? "";
|
||||
@@ -467,6 +483,7 @@ export function resolveConfiguredModelRef(params: {
|
||||
const aliasIndex = buildModelAliasIndex({
|
||||
cfg: params.cfg,
|
||||
defaultProvider: params.defaultProvider,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
if (!trimmed.includes("/")) {
|
||||
@@ -474,6 +491,7 @@ export function resolveConfiguredModelRef(params: {
|
||||
cfg: params.cfg,
|
||||
raw: trimmed,
|
||||
defaultProvider: params.defaultProvider,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
if (openrouterCompatRef) {
|
||||
@@ -507,6 +525,7 @@ export function resolveConfiguredModelRef(params: {
|
||||
raw: trimmed,
|
||||
defaultProvider: params.defaultProvider,
|
||||
aliasIndex,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
if (resolved) {
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { modelKey } from "../agents/model-selection.js";
|
||||
import type { normalizeProviderModelIdWithRuntime } from "../agents/provider-model-normalization.runtime.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resetLogger, setLoggerOverride } from "../logging/logger.js";
|
||||
import { loggingState } from "../logging/state.js";
|
||||
import type { PluginManifestRecord, PluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import type { normalizeProviderModelIdWithPlugin } from "../plugins/provider-runtime.js";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
|
||||
const normalizeProviderModelIdWithPluginMock = vi.hoisted(() =>
|
||||
vi.fn<typeof normalizeProviderModelIdWithPlugin>(({ context }) => context.modelId),
|
||||
const normalizeProviderModelIdWithRuntimeMock = vi.hoisted(() =>
|
||||
vi.fn<typeof normalizeProviderModelIdWithRuntime>(({ context }) => context.modelId),
|
||||
);
|
||||
const pluginManifestRegistryMocks = vi.hoisted(() => ({
|
||||
manifestRegistry: undefined as PluginManifestRegistry | undefined,
|
||||
loadPluginManifestRegistryForInstalledIndex: vi.fn(),
|
||||
listOpenClawPluginManifestMetadata: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/provider-runtime.js", () => {
|
||||
return { normalizeProviderModelIdWithPlugin: normalizeProviderModelIdWithPluginMock };
|
||||
vi.mock("../agents/provider-model-normalization.runtime.js", () => {
|
||||
return { normalizeProviderModelIdWithRuntime: normalizeProviderModelIdWithRuntimeMock };
|
||||
});
|
||||
|
||||
vi.mock("../plugins/manifest-registry-installed.js", async (importOriginal) => {
|
||||
@@ -35,6 +36,19 @@ vi.mock("../plugins/manifest-registry-installed.js", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../plugins/manifest-metadata-scan.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../plugins/manifest-metadata-scan.js")>();
|
||||
return {
|
||||
...actual,
|
||||
listOpenClawPluginManifestMetadata: (
|
||||
params?: Parameters<typeof actual.listOpenClawPluginManifestMetadata>[0],
|
||||
) => {
|
||||
pluginManifestRegistryMocks.listOpenClawPluginManifestMetadata(params);
|
||||
return actual.listOpenClawPluginManifestMetadata(params);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import {
|
||||
__resetGatewayModelPricingCacheForTest,
|
||||
collectConfiguredModelPricingRefs,
|
||||
@@ -48,6 +62,8 @@ describe("model-pricing-cache", () => {
|
||||
__resetGatewayModelPricingCacheForTest();
|
||||
pluginManifestRegistryMocks.manifestRegistry = undefined;
|
||||
pluginManifestRegistryMocks.loadPluginManifestRegistryForInstalledIndex.mockClear();
|
||||
pluginManifestRegistryMocks.listOpenClawPluginManifestMetadata.mockClear();
|
||||
normalizeProviderModelIdWithRuntimeMock.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -186,6 +202,58 @@ describe("model-pricing-cache", () => {
|
||||
expect(fetchImpl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not load plugin manifests for pricing when plugins are globally disabled", async () => {
|
||||
const config = {
|
||||
plugins: {
|
||||
enabled: false,
|
||||
entries: {
|
||||
"search-plugin": {
|
||||
config: {
|
||||
webSearch: {
|
||||
model: "local-search/search-model",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "custom/gpt-local" },
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
custom: {
|
||||
baseUrl: "https://models.example/v1",
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "gpt-local",
|
||||
cost: { input: 0.12, output: 0.48 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
const fetchImpl = vi.fn<typeof fetch>();
|
||||
|
||||
await refreshGatewayModelPricingCache({ config, fetchImpl });
|
||||
|
||||
expect(
|
||||
pluginManifestRegistryMocks.loadPluginManifestRegistryForInstalledIndex,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(pluginManifestRegistryMocks.listOpenClawPluginManifestMetadata).not.toHaveBeenCalled();
|
||||
expect(normalizeProviderModelIdWithRuntimeMock).not.toHaveBeenCalled();
|
||||
expect(fetchImpl).not.toHaveBeenCalled();
|
||||
expect(getCachedGatewayModelPricing({ provider: "custom", model: "gpt-local" })).toEqual({
|
||||
input: 0.12,
|
||||
output: 0.48,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("skips remote pricing catalogs for local-only model providers", async () => {
|
||||
const config = {
|
||||
agents: {
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
loadPluginRegistrySnapshot,
|
||||
type PluginRegistrySnapshot,
|
||||
} from "../plugins/plugin-registry.js";
|
||||
import { normalizeProviderModelIdWithPlugin } from "../plugins/provider-runtime.js";
|
||||
import { normalizeOptionalString, resolvePrimaryStringValue } from "../shared/string-coerce.js";
|
||||
import {
|
||||
clearGatewayModelPricingCacheState,
|
||||
@@ -67,6 +66,11 @@ type ExternalPricingSourcePolicy = {
|
||||
|
||||
export { getCachedGatewayModelPricing };
|
||||
|
||||
type PricingModelNormalizationOptions = {
|
||||
allowManifestNormalization: boolean;
|
||||
allowPluginNormalization: boolean;
|
||||
};
|
||||
|
||||
const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
||||
const LITELLM_PRICING_URL =
|
||||
"https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
|
||||
@@ -86,6 +90,16 @@ function clearRefreshTimer(): void {
|
||||
refreshTimer = null;
|
||||
}
|
||||
|
||||
function getPricingModelNormalizationOptions(
|
||||
config: OpenClawConfig,
|
||||
): PricingModelNormalizationOptions {
|
||||
const allowPluginBackedNormalization = config.plugins?.enabled !== false;
|
||||
return {
|
||||
allowManifestNormalization: allowPluginBackedNormalization,
|
||||
allowPluginNormalization: allowPluginBackedNormalization,
|
||||
};
|
||||
}
|
||||
|
||||
function listLikeFallbacks(value: ModelListLike): string[] {
|
||||
if (!value || typeof value !== "object") {
|
||||
return [];
|
||||
@@ -404,6 +418,13 @@ function resolveModelPricingManifestMetadata(params: {
|
||||
activeRegistry: params.manifestRegistry,
|
||||
};
|
||||
}
|
||||
if (params.config.plugins?.enabled === false) {
|
||||
const emptyRegistry: PluginManifestRegistry = { plugins: [], diagnostics: [] };
|
||||
return {
|
||||
allRegistry: emptyRegistry,
|
||||
activeRegistry: emptyRegistry,
|
||||
};
|
||||
}
|
||||
const index = loadPluginRegistrySnapshot({ config: params.config });
|
||||
const allRegistry = loadPluginManifestRegistryForInstalledIndex({
|
||||
index,
|
||||
@@ -472,7 +493,13 @@ function applyModelIdTransforms(
|
||||
return [...variants];
|
||||
}
|
||||
|
||||
function canonicalizeOpenRouterLookupId(id: string): string {
|
||||
function canonicalizeOpenRouterLookupId(
|
||||
id: string,
|
||||
options: PricingModelNormalizationOptions = {
|
||||
allowManifestNormalization: true,
|
||||
allowPluginNormalization: true,
|
||||
},
|
||||
): string {
|
||||
const trimmed = id.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
@@ -481,19 +508,18 @@ function canonicalizeOpenRouterLookupId(id: string): string {
|
||||
if (slash === -1) {
|
||||
return trimmed;
|
||||
}
|
||||
const provider = normalizeModelRef(trimmed.slice(0, slash), "placeholder").provider;
|
||||
const provider = normalizeModelRef(trimmed.slice(0, slash), "placeholder", {
|
||||
allowManifestNormalization: options.allowManifestNormalization,
|
||||
allowPluginNormalization: options.allowPluginNormalization,
|
||||
}).provider;
|
||||
const model = trimmed.slice(slash + 1).trim();
|
||||
if (!model) {
|
||||
return provider;
|
||||
}
|
||||
const normalizedModel =
|
||||
normalizeProviderModelIdWithPlugin({
|
||||
provider,
|
||||
context: {
|
||||
provider,
|
||||
modelId: model,
|
||||
},
|
||||
}) ?? model;
|
||||
const normalizedModel = normalizeModelRef(provider, model, {
|
||||
allowManifestNormalization: options.allowManifestNormalization,
|
||||
allowPluginNormalization: options.allowPluginNormalization,
|
||||
}).model;
|
||||
return modelKey(provider, normalizedModel);
|
||||
}
|
||||
|
||||
@@ -502,6 +528,8 @@ function buildExternalCatalogCandidates(params: {
|
||||
source: "openRouter" | "liteLLM";
|
||||
policies: ReadonlyMap<string, ExternalPricingPolicy>;
|
||||
seen?: Set<string>;
|
||||
allowManifestNormalization?: boolean;
|
||||
allowPluginNormalization?: boolean;
|
||||
}): string[] {
|
||||
const { ref, source, policies } = params;
|
||||
const refKey = modelKey(ref.provider, ref.model);
|
||||
@@ -529,17 +557,29 @@ function buildExternalCatalogCandidates(params: {
|
||||
|
||||
for (const model of applyModelIdTransforms(ref.model, transforms)) {
|
||||
const candidate = modelKey(provider, model);
|
||||
candidates.add(source === "openRouter" ? canonicalizeOpenRouterLookupId(candidate) : candidate);
|
||||
candidates.add(
|
||||
source === "openRouter"
|
||||
? canonicalizeOpenRouterLookupId(candidate, {
|
||||
allowManifestNormalization: params.allowManifestNormalization ?? true,
|
||||
allowPluginNormalization: params.allowPluginNormalization ?? true,
|
||||
})
|
||||
: candidate,
|
||||
);
|
||||
}
|
||||
|
||||
if (sourcePolicy?.passthroughProviderModel && ref.model.includes("/")) {
|
||||
const nestedRef = parseModelRef(ref.model, DEFAULT_PROVIDER);
|
||||
const nestedRef = parseModelRef(ref.model, DEFAULT_PROVIDER, {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
if (nestedRef) {
|
||||
for (const candidate of buildExternalCatalogCandidates({
|
||||
ref: nestedRef,
|
||||
source,
|
||||
policies,
|
||||
seen: nextSeen,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
})) {
|
||||
candidates.add(candidate);
|
||||
}
|
||||
@@ -553,6 +593,8 @@ function addResolvedModelRef(params: {
|
||||
raw: string | undefined;
|
||||
aliasIndex: ReturnType<typeof buildModelAliasIndex>;
|
||||
refs: Map<string, ModelRef>;
|
||||
allowManifestNormalization: boolean;
|
||||
allowPluginNormalization: boolean;
|
||||
}): void {
|
||||
const raw = params.raw?.trim();
|
||||
if (!raw) {
|
||||
@@ -562,11 +604,16 @@ function addResolvedModelRef(params: {
|
||||
raw,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
aliasIndex: params.aliasIndex,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
if (!resolved) {
|
||||
return;
|
||||
}
|
||||
const normalized = normalizeModelRef(resolved.ref.provider, resolved.ref.model);
|
||||
const normalized = normalizeModelRef(resolved.ref.provider, resolved.ref.model, {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
params.refs.set(modelKey(normalized.provider, normalized.model), normalized);
|
||||
}
|
||||
|
||||
@@ -574,17 +621,23 @@ function addModelListLike(params: {
|
||||
value: ModelListLike;
|
||||
aliasIndex: ReturnType<typeof buildModelAliasIndex>;
|
||||
refs: Map<string, ModelRef>;
|
||||
allowManifestNormalization: boolean;
|
||||
allowPluginNormalization: boolean;
|
||||
}): void {
|
||||
addResolvedModelRef({
|
||||
raw: resolvePrimaryStringValue(params.value),
|
||||
aliasIndex: params.aliasIndex,
|
||||
refs: params.refs,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
for (const fallback of listLikeFallbacks(params.value)) {
|
||||
addResolvedModelRef({
|
||||
raw: fallback,
|
||||
aliasIndex: params.aliasIndex,
|
||||
refs: params.refs,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -593,13 +646,18 @@ function addProviderModelPair(params: {
|
||||
provider: string | undefined;
|
||||
model: string | undefined;
|
||||
refs: Map<string, ModelRef>;
|
||||
allowManifestNormalization: boolean;
|
||||
allowPluginNormalization: boolean;
|
||||
}): void {
|
||||
const provider = params.provider?.trim();
|
||||
const model = params.model?.trim();
|
||||
if (!provider || !model) {
|
||||
return;
|
||||
}
|
||||
const normalized = normalizeModelRef(provider, model);
|
||||
const normalized = normalizeModelRef(provider, model, {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
params.refs.set(modelKey(normalized.provider, normalized.model), normalized);
|
||||
}
|
||||
|
||||
@@ -608,6 +666,8 @@ function addConfiguredWebSearchPluginModels(params: {
|
||||
aliasIndex: ReturnType<typeof buildModelAliasIndex>;
|
||||
refs: Map<string, ModelRef>;
|
||||
manifestRegistry: PluginManifestRegistry;
|
||||
allowManifestNormalization: boolean;
|
||||
allowPluginNormalization: boolean;
|
||||
}): void {
|
||||
for (const pluginId of params.manifestRegistry.plugins
|
||||
.filter((plugin) => (plugin.contracts?.webSearchProviders ?? []).length > 0)
|
||||
@@ -617,6 +677,8 @@ function addConfiguredWebSearchPluginModels(params: {
|
||||
raw: resolvePluginWebSearchConfig(params.config, pluginId)?.model as string | undefined,
|
||||
aliasIndex: params.aliasIndex,
|
||||
refs: params.refs,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -660,10 +722,17 @@ function isPrivateOrLoopbackBaseUrl(baseUrl: string | undefined): boolean {
|
||||
function findConfiguredProviderModel(
|
||||
config: OpenClawConfig,
|
||||
ref: ModelRef,
|
||||
options: PricingModelNormalizationOptions = {
|
||||
allowManifestNormalization: true,
|
||||
allowPluginNormalization: true,
|
||||
},
|
||||
): ModelDefinitionConfig | undefined {
|
||||
const providerConfig = config.models?.providers?.[ref.provider];
|
||||
return providerConfig?.models?.find((model) => {
|
||||
const normalized = normalizeModelRef(ref.provider, model.id);
|
||||
const normalized = normalizeModelRef(ref.provider, model.id, {
|
||||
allowManifestNormalization: options.allowManifestNormalization,
|
||||
allowPluginNormalization: options.allowPluginNormalization,
|
||||
});
|
||||
return modelKey(normalized.provider, normalized.model) === modelKey(ref.provider, ref.model);
|
||||
});
|
||||
}
|
||||
@@ -671,13 +740,24 @@ function findConfiguredProviderModel(
|
||||
function getConfiguredModelPricing(
|
||||
config: OpenClawConfig,
|
||||
ref: ModelRef,
|
||||
options: PricingModelNormalizationOptions = {
|
||||
allowManifestNormalization: true,
|
||||
allowPluginNormalization: true,
|
||||
},
|
||||
): CachedModelPricing | undefined {
|
||||
return toCachedModelPricing(findConfiguredProviderModel(config, ref)?.cost);
|
||||
return toCachedModelPricing(findConfiguredProviderModel(config, ref, options)?.cost);
|
||||
}
|
||||
|
||||
function hasPrivateOrLoopbackConfiguredEndpoint(config: OpenClawConfig, ref: ModelRef): boolean {
|
||||
function hasPrivateOrLoopbackConfiguredEndpoint(
|
||||
config: OpenClawConfig,
|
||||
ref: ModelRef,
|
||||
options: PricingModelNormalizationOptions = {
|
||||
allowManifestNormalization: true,
|
||||
allowPluginNormalization: true,
|
||||
},
|
||||
): boolean {
|
||||
const providerConfig = config.models?.providers?.[ref.provider];
|
||||
const model = findConfiguredProviderModel(config, ref);
|
||||
const model = findConfiguredProviderModel(config, ref, options);
|
||||
return (
|
||||
isPrivateOrLoopbackBaseUrl(model?.baseUrl) ||
|
||||
isPrivateOrLoopbackBaseUrl(providerConfig?.baseUrl)
|
||||
@@ -689,11 +769,18 @@ function shouldFetchExternalPricingForRef(params: {
|
||||
ref: ModelRef;
|
||||
policies: ReadonlyMap<string, ExternalPricingPolicy>;
|
||||
seededPricing: ReadonlyMap<string, CachedModelPricing>;
|
||||
allowManifestNormalization: boolean;
|
||||
allowPluginNormalization: boolean;
|
||||
}): boolean {
|
||||
if (params.seededPricing.has(modelKey(params.ref.provider, params.ref.model))) {
|
||||
return false;
|
||||
}
|
||||
if (hasPrivateOrLoopbackConfiguredEndpoint(params.config, params.ref)) {
|
||||
if (
|
||||
hasPrivateOrLoopbackConfiguredEndpoint(params.config, params.ref, {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (params.policies.get(params.ref.provider)?.external === false) {
|
||||
@@ -707,6 +794,8 @@ function filterExternalPricingRefs(params: {
|
||||
refs: ModelRef[];
|
||||
policies: ReadonlyMap<string, ExternalPricingPolicy>;
|
||||
seededPricing: ReadonlyMap<string, CachedModelPricing>;
|
||||
allowManifestNormalization: boolean;
|
||||
allowPluginNormalization: boolean;
|
||||
}): ModelRef[] {
|
||||
return params.refs.filter((ref) =>
|
||||
shouldFetchExternalPricingForRef({
|
||||
@@ -714,6 +803,8 @@ function filterExternalPricingRefs(params: {
|
||||
ref,
|
||||
policies: params.policies,
|
||||
seededPricing: params.seededPricing,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -724,29 +815,104 @@ export function collectConfiguredModelPricingRefs(
|
||||
): ModelRef[] {
|
||||
const manifestRegistry =
|
||||
options.manifestRegistry ?? resolveModelPricingManifestMetadata({ config }).allRegistry;
|
||||
const normalizationOptions = getPricingModelNormalizationOptions(config);
|
||||
const refs = new Map<string, ModelRef>();
|
||||
const aliasIndex = buildModelAliasIndex({
|
||||
cfg: config,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
});
|
||||
|
||||
addModelListLike({ value: config.agents?.defaults?.model, aliasIndex, refs });
|
||||
addModelListLike({ value: config.agents?.defaults?.imageModel, aliasIndex, refs });
|
||||
addModelListLike({ value: config.agents?.defaults?.pdfModel, aliasIndex, refs });
|
||||
addResolvedModelRef({ raw: config.agents?.defaults?.compaction?.model, aliasIndex, refs });
|
||||
addResolvedModelRef({ raw: config.agents?.defaults?.heartbeat?.model, aliasIndex, refs });
|
||||
addModelListLike({ value: config.tools?.subagents?.model, aliasIndex, refs });
|
||||
addResolvedModelRef({ raw: config.messages?.tts?.summaryModel, aliasIndex, refs });
|
||||
addResolvedModelRef({ raw: config.hooks?.gmail?.model, aliasIndex, refs });
|
||||
addModelListLike({
|
||||
value: config.agents?.defaults?.model,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
});
|
||||
addModelListLike({
|
||||
value: config.agents?.defaults?.imageModel,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
});
|
||||
addModelListLike({
|
||||
value: config.agents?.defaults?.pdfModel,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
});
|
||||
addResolvedModelRef({
|
||||
raw: config.agents?.defaults?.compaction?.model,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
});
|
||||
addResolvedModelRef({
|
||||
raw: config.agents?.defaults?.heartbeat?.model,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
});
|
||||
addModelListLike({
|
||||
value: config.tools?.subagents?.model,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
});
|
||||
addResolvedModelRef({
|
||||
raw: config.messages?.tts?.summaryModel,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
});
|
||||
addResolvedModelRef({
|
||||
raw: config.hooks?.gmail?.model,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
});
|
||||
|
||||
for (const agent of config.agents?.list ?? []) {
|
||||
addModelListLike({ value: agent.model, aliasIndex, refs });
|
||||
addModelListLike({ value: agent.subagents?.model, aliasIndex, refs });
|
||||
addResolvedModelRef({ raw: agent.heartbeat?.model, aliasIndex, refs });
|
||||
addModelListLike({
|
||||
value: agent.model,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
});
|
||||
addModelListLike({
|
||||
value: agent.subagents?.model,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
});
|
||||
addResolvedModelRef({
|
||||
raw: agent.heartbeat?.model,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
});
|
||||
}
|
||||
|
||||
for (const mapping of config.hooks?.mappings ?? []) {
|
||||
addResolvedModelRef({ raw: mapping.model, aliasIndex, refs });
|
||||
addResolvedModelRef({
|
||||
raw: mapping.model,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
});
|
||||
}
|
||||
|
||||
for (const channelMap of Object.values(config.channels?.modelByChannel ?? {})) {
|
||||
@@ -758,23 +924,56 @@ export function collectConfiguredModelPricingRefs(
|
||||
raw: typeof raw === "string" ? raw : undefined,
|
||||
aliasIndex,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
addConfiguredWebSearchPluginModels({ config, aliasIndex, refs, manifestRegistry });
|
||||
addConfiguredWebSearchPluginModels({
|
||||
config,
|
||||
aliasIndex,
|
||||
refs,
|
||||
manifestRegistry,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
});
|
||||
|
||||
for (const entry of config.tools?.media?.models ?? []) {
|
||||
addProviderModelPair({ provider: entry.provider, model: entry.model, refs });
|
||||
addProviderModelPair({
|
||||
provider: entry.provider,
|
||||
model: entry.model,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
});
|
||||
}
|
||||
for (const entry of config.tools?.media?.image?.models ?? []) {
|
||||
addProviderModelPair({ provider: entry.provider, model: entry.model, refs });
|
||||
addProviderModelPair({
|
||||
provider: entry.provider,
|
||||
model: entry.model,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
});
|
||||
}
|
||||
for (const entry of config.tools?.media?.audio?.models ?? []) {
|
||||
addProviderModelPair({ provider: entry.provider, model: entry.model, refs });
|
||||
addProviderModelPair({
|
||||
provider: entry.provider,
|
||||
model: entry.model,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
});
|
||||
}
|
||||
for (const entry of config.tools?.media?.video?.models ?? []) {
|
||||
addProviderModelPair({ provider: entry.provider, model: entry.model, refs });
|
||||
addProviderModelPair({
|
||||
provider: entry.provider,
|
||||
model: entry.model,
|
||||
refs,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
});
|
||||
}
|
||||
|
||||
return Array.from(refs.values());
|
||||
@@ -810,11 +1009,15 @@ function resolveCatalogPricingForRef(params: {
|
||||
policies: ReadonlyMap<string, ExternalPricingPolicy>;
|
||||
catalogById: Map<string, OpenRouterPricingEntry>;
|
||||
catalogByNormalizedId: Map<string, OpenRouterPricingEntry>;
|
||||
allowManifestNormalization: boolean;
|
||||
allowPluginNormalization: boolean;
|
||||
}): CachedModelPricing | undefined {
|
||||
const candidates = buildExternalCatalogCandidates({
|
||||
ref: params.ref,
|
||||
source: "openRouter",
|
||||
policies: params.policies,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
for (const candidate of candidates) {
|
||||
const exact = params.catalogById.get(candidate);
|
||||
@@ -823,7 +1026,10 @@ function resolveCatalogPricingForRef(params: {
|
||||
}
|
||||
}
|
||||
for (const candidate of candidates) {
|
||||
const normalized = canonicalizeOpenRouterLookupId(candidate);
|
||||
const normalized = canonicalizeOpenRouterLookupId(candidate, {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
if (!normalized) {
|
||||
continue;
|
||||
}
|
||||
@@ -839,11 +1045,15 @@ function resolveLiteLLMPricingForRef(params: {
|
||||
ref: ModelRef;
|
||||
policies: ReadonlyMap<string, ExternalPricingPolicy>;
|
||||
catalog: LiteLLMPricingCatalog;
|
||||
allowManifestNormalization: boolean;
|
||||
allowPluginNormalization: boolean;
|
||||
}): CachedModelPricing | undefined {
|
||||
for (const candidate of buildExternalCatalogCandidates({
|
||||
ref: params.ref,
|
||||
source: "liteLLM",
|
||||
policies: params.policies,
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
})) {
|
||||
const pricing = params.catalog.get(candidate);
|
||||
if (pricing) {
|
||||
@@ -867,11 +1077,16 @@ function collectSeededPricing(params: {
|
||||
config: OpenClawConfig;
|
||||
refs: readonly ModelRef[];
|
||||
catalogPricing: ReadonlyMap<string, CachedModelPricing>;
|
||||
allowManifestNormalization: boolean;
|
||||
allowPluginNormalization: boolean;
|
||||
}): Map<string, CachedModelPricing> {
|
||||
const seeded = new Map<string, CachedModelPricing>();
|
||||
for (const ref of params.refs) {
|
||||
const key = modelKey(ref.provider, ref.model);
|
||||
const configuredPricing = getConfiguredModelPricing(params.config, ref);
|
||||
const configuredPricing = getConfiguredModelPricing(params.config, ref, {
|
||||
allowManifestNormalization: params.allowManifestNormalization,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
if (configuredPricing) {
|
||||
seeded.set(key, configuredPricing);
|
||||
continue;
|
||||
@@ -900,6 +1115,7 @@ export async function refreshGatewayModelPricingCache(params: {
|
||||
pluginLookUpTable: params.pluginLookUpTable,
|
||||
manifestRegistry: params.manifestRegistry,
|
||||
});
|
||||
const normalizationOptions = getPricingModelNormalizationOptions(params.config);
|
||||
const pricingContext = loadManifestPricingContext(manifestMetadata.activeRegistry);
|
||||
const allRefs = collectConfiguredModelPricingRefs(params.config, {
|
||||
manifestRegistry: manifestMetadata.allRegistry,
|
||||
@@ -908,12 +1124,16 @@ export async function refreshGatewayModelPricingCache(params: {
|
||||
config: params.config,
|
||||
refs: allRefs,
|
||||
catalogPricing: pricingContext.catalogPricing,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
});
|
||||
const refs = filterExternalPricingRefs({
|
||||
config: params.config,
|
||||
refs: allRefs,
|
||||
policies: pricingContext.policies,
|
||||
seededPricing,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
});
|
||||
if (refs.length === 0) {
|
||||
replaceGatewayModelPricingCache(seededPricing);
|
||||
@@ -940,7 +1160,7 @@ export async function refreshGatewayModelPricingCache(params: {
|
||||
|
||||
const catalogByNormalizedId = new Map<string, OpenRouterPricingEntry>();
|
||||
for (const entry of catalogById.values()) {
|
||||
const normalizedId = canonicalizeOpenRouterLookupId(entry.id);
|
||||
const normalizedId = canonicalizeOpenRouterLookupId(entry.id, normalizationOptions);
|
||||
if (!normalizedId || catalogByNormalizedId.has(normalizedId)) {
|
||||
continue;
|
||||
}
|
||||
@@ -955,6 +1175,8 @@ export async function refreshGatewayModelPricingCache(params: {
|
||||
policies: pricingContext.policies,
|
||||
catalogById,
|
||||
catalogByNormalizedId,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
});
|
||||
|
||||
// 2. Try LiteLLM (may contain tiered pricing)
|
||||
@@ -962,6 +1184,8 @@ export async function refreshGatewayModelPricingCache(params: {
|
||||
ref,
|
||||
policies: pricingContext.policies,
|
||||
catalog: litellmCatalog,
|
||||
allowManifestNormalization: normalizationOptions.allowManifestNormalization,
|
||||
allowPluginNormalization: normalizationOptions.allowPluginNormalization,
|
||||
});
|
||||
|
||||
// Merge strategy: OpenRouter provides the base flat pricing;
|
||||
|
||||
Reference in New Issue
Block a user