mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 07:41:33 +00:00
fix(anthropic): honor configured model pricing (#117275)
This commit is contained in:
@@ -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?.({
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user