fix(anthropic): honor configured model pricing (#117275)

This commit is contained in:
synth
2026-08-01 12:56:23 -04:00
committed by GitHub
parent fdec6fe7d4
commit 0df66f6d63
2 changed files with 75 additions and 5 deletions

View File

@@ -1,4 +1,5 @@
// Anthropic tests cover index plugin behavior.
import { calculateCost, type Usage } from "openclaw/plugin-sdk/llm";
import type {
ProviderResolveDynamicModelContext,
ProviderRuntimeModel,
@@ -859,6 +860,70 @@ describe("anthropic provider replay hooks", () => {
expect(normalized?.cost).toEqual((resolved as ProviderRuntimeModel).cost);
});
it.each(["claude-sonnet-5", "claude-opus-5"])(
"uses operator-configured %s pricing for assistant usage",
async (modelId) => {
const provider = await registerSingleProviderPlugin(anthropicPlugin);
const configuredCost = { input: 777, output: 888, cacheRead: 999, cacheWrite: 666 };
const config: NonNullable<ProviderResolveDynamicModelContext["config"]> = {
models: {
providers: {
anthropic: {
baseUrl: "https://api.anthropic.com",
models: [
{
id: modelId,
name: modelId,
reasoning: true,
input: ["text", "image"],
cost: configuredCost,
contextWindow: 1_000_000,
maxTokens: 128_000,
},
],
},
},
},
};
const discoveredModel = provider.resolveDynamicModel?.({
config,
provider: "anthropic",
modelId,
modelRegistry: createModelRegistry([]),
} as ProviderResolveDynamicModelContext);
expect(discoveredModel).toBeDefined();
const configuredModel = {
...(discoveredModel as ProviderRuntimeModel),
cost: configuredCost,
};
const resolvedModel =
provider.normalizeResolvedModel?.({
config,
provider: "anthropic",
modelId,
model: configuredModel,
} as never) ?? configuredModel;
const usage: Usage = {
input: 1_000,
output: 1_000,
cacheRead: 1_000,
cacheWrite: 1_000,
totalTokens: 4_000,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
};
calculateCost(resolvedModel as Parameters<typeof calculateCost>[0], usage);
expect(resolvedModel.cost).toEqual(configuredCost);
expect(usage.cost.input).toBeCloseTo(0.777);
expect(usage.cost.output).toBeCloseTo(0.888);
expect(usage.cost.cacheRead).toBeCloseTo(0.999);
expect(usage.cost.cacheWrite).toBeCloseTo(0.666);
expect(usage.cost.total).toBeCloseTo(3.33);
},
);
it("resolves Claude Mythos 5 with its direct-only mandatory-adaptive contract", async () => {
const provider = await registerSingleProviderPlugin(anthropicPlugin);
const resolved = provider.resolveDynamicModel?.({

View File

@@ -631,10 +631,11 @@ function supportsAnthropicNativeMaxEffort(modelId: string): boolean {
return supportsClaudeNativeMaxEffort({ id: modelId }) || isAnthropicMythosPreviewModel(modelId);
}
function hasConfiguredModelContextOverride(
function hasConfiguredModelOverride(
config: ProviderNormalizeResolvedModelContext["config"],
provider: string,
modelId: string,
override: "context" | "cost",
): boolean {
const providers = config?.models?.providers;
if (!providers || typeof providers !== "object") {
@@ -657,8 +658,10 @@ function hasConfiguredModelContextOverride(
continue;
}
if (
(typeof model?.contextTokens === "number" && model.contextTokens > 0) ||
(typeof model?.contextWindow === "number" && model.contextWindow > 0)
override === "cost"
? model?.cost !== undefined
: (typeof model?.contextTokens === "number" && model.contextTokens > 0) ||
(typeof model?.contextWindow === "number" && model.contextWindow > 0)
) {
return true;
}
@@ -681,7 +684,7 @@ function applyAnthropicFixedContextWindow(params: {
if (fixedContextWindow === undefined) {
return undefined;
}
if (hasConfiguredModelContextOverride(params.config, params.provider, params.modelId)) {
if (hasConfiguredModelOverride(params.config, params.provider, params.modelId, "context")) {
return undefined;
}
const exactContextWindow = isAnthropicExact1MClaude5Model(params.contractModelId);
@@ -878,8 +881,10 @@ function normalizeAnthropicResolvedModel(
contractModelId,
model: thinkingLevelModel,
}) ?? thinkingLevelModel;
// Provider catalog defaults must not replace explicit operator pricing.
const pricingModel =
normalizeLowercaseStringOrEmpty(ctx.provider) === PROVIDER_ID
normalizeLowercaseStringOrEmpty(ctx.provider) === PROVIDER_ID &&
!hasConfiguredModelOverride(ctx.config, ctx.provider, ctx.modelId, "cost")
? (applyAnthropicOpus5Cost({
modelId: contractModelId,
model: contextWindowModel,