mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 10:55:31 -06:00
fix(anthropic): price fallback serving models
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { AssistantMessageDiagnostic } from "../types.js";
|
||||
import type { AssistantMessageDiagnostic, Model } from "../types.js";
|
||||
|
||||
/** Anthropic beta that re-serves safety refusals on an allowed fallback model. */
|
||||
export const ANTHROPIC_SERVER_SIDE_FALLBACK_BETA = "server-side-fallback-2026-07-01";
|
||||
@@ -6,19 +6,69 @@ export const ANTHROPIC_SERVER_SIDE_FALLBACK_BETA = "server-side-fallback-2026-07
|
||||
/** Let Anthropic select the recommended model for each refusal category. */
|
||||
export const ANTHROPIC_SERVER_SIDE_FALLBACKS = "default" as const;
|
||||
|
||||
// Fallback-served turns bill at the serving model's rates.
|
||||
export const CLAUDE_OPUS_48_FALLBACK_MODEL_COST = {
|
||||
// Anthropic's current default routes serve fallback output on Opus 5 or 4.8,
|
||||
// which share the same standard and fast-mode rates.
|
||||
export const CLAUDE_OPUS_FALLBACK_MODEL_COST = {
|
||||
input: 5,
|
||||
output: 25,
|
||||
cacheRead: 0.5,
|
||||
cacheWrite: 6.25,
|
||||
} as const;
|
||||
|
||||
const CLAUDE_OPUS_FAST_FALLBACK_MODEL_COST = {
|
||||
input: 10,
|
||||
output: 50,
|
||||
cacheRead: 1,
|
||||
cacheWrite: 12.5,
|
||||
} as const;
|
||||
|
||||
export type AnthropicFallbackBoundary = {
|
||||
fromModel: string | null;
|
||||
toModel: string | null;
|
||||
};
|
||||
|
||||
function isModelCostEqual(left: Model["cost"], right: Model["cost"]): boolean {
|
||||
return (
|
||||
left.input === right.input &&
|
||||
left.output === right.output &&
|
||||
left.cacheRead === right.cacheRead &&
|
||||
left.cacheWrite === right.cacheWrite
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeModelId(modelId: string | null): string | null {
|
||||
const normalized = modelId?.trim().toLowerCase();
|
||||
return normalized || null;
|
||||
}
|
||||
|
||||
function isClaudeOpusFallbackModel(modelId: string): boolean {
|
||||
return /^claude-opus-(?:5|4-8)$/.test(modelId);
|
||||
}
|
||||
|
||||
/** Resolve billed rates from the serving model reported by Anthropic's fallback stream. */
|
||||
export function resolveAnthropicFallbackServingModelCost(params: {
|
||||
requestedModelId: string;
|
||||
servingModelId: string | null;
|
||||
requestedCost: Model["cost"];
|
||||
}): Model["cost"] {
|
||||
const requestedModelId = normalizeModelId(params.requestedModelId);
|
||||
const servingModelId = normalizeModelId(params.servingModelId);
|
||||
if (
|
||||
!servingModelId ||
|
||||
servingModelId === requestedModelId ||
|
||||
!isClaudeOpusFallbackModel(servingModelId)
|
||||
) {
|
||||
return params.requestedCost;
|
||||
}
|
||||
if (
|
||||
requestedModelId === "claude-opus-5" &&
|
||||
isModelCostEqual(params.requestedCost, CLAUDE_OPUS_FAST_FALLBACK_MODEL_COST)
|
||||
) {
|
||||
return CLAUDE_OPUS_FAST_FALLBACK_MODEL_COST;
|
||||
}
|
||||
return CLAUDE_OPUS_FALLBACK_MODEL_COST;
|
||||
}
|
||||
|
||||
function readBoundaryModel(value: unknown): string | null {
|
||||
if (!value || typeof value !== "object") {
|
||||
return null;
|
||||
|
||||
@@ -68,9 +68,9 @@ import { applyAnthropicRefusal } from "./anthropic-refusal.js";
|
||||
import {
|
||||
ANTHROPIC_SERVER_SIDE_FALLBACK_BETA,
|
||||
ANTHROPIC_SERVER_SIDE_FALLBACKS,
|
||||
CLAUDE_OPUS_48_FALLBACK_MODEL_COST,
|
||||
applyAnthropicFallbackBoundary,
|
||||
readAnthropicFallbackBoundary,
|
||||
resolveAnthropicFallbackServingModelCost,
|
||||
} from "./anthropic-server-fallback.js";
|
||||
import {
|
||||
ANTHROPIC_OMITTED_REASONING_TEXT,
|
||||
@@ -492,6 +492,16 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti
|
||||
if (event.type === "message_start") {
|
||||
output.responseId = event.message.id;
|
||||
output.responseModel = event.message.model;
|
||||
if (refusalBuffer) {
|
||||
costModel = {
|
||||
...model,
|
||||
cost: resolveAnthropicFallbackServingModelCost({
|
||||
requestedModelId: model.id,
|
||||
servingModelId: event.message.model,
|
||||
requestedCost: model.cost,
|
||||
}),
|
||||
};
|
||||
}
|
||||
const promptUsage = readAnthropicPromptUsageSnapshot(event.message.usage);
|
||||
const messageStartPromptTokens = promptUsage
|
||||
? promptUsage.input + promptUsage.cacheRead + promptUsage.cacheWrite
|
||||
@@ -560,7 +570,14 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti
|
||||
// Cost intentionally mirrors top-level usage (serving attempt at
|
||||
// serving-model rates). A mid-stream decline's billed partial is
|
||||
// only in usage.iterations and is not folded in here.
|
||||
costModel = { ...model, cost: CLAUDE_OPUS_48_FALLBACK_MODEL_COST };
|
||||
costModel = {
|
||||
...model,
|
||||
cost: resolveAnthropicFallbackServingModelCost({
|
||||
requestedModelId: model.id,
|
||||
servingModelId: fallbackBoundary.toModel,
|
||||
requestedCost: model.cost,
|
||||
}),
|
||||
};
|
||||
calculateCost(costModel, output.usage);
|
||||
eventSink.push({ type: "start", partial: output });
|
||||
for (const [i, block] of blocks.entries()) {
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
ANTHROPIC_OMITTED_REASONING_TEXT,
|
||||
ANTHROPIC_SERVER_SIDE_FALLBACK_BETA,
|
||||
ANTHROPIC_SERVER_SIDE_FALLBACKS,
|
||||
CLAUDE_OPUS_48_FALLBACK_MODEL_COST,
|
||||
applyClaudeRequestContract,
|
||||
applyAnthropicFallbackBoundary,
|
||||
defaultsClaudeAdaptiveThinking,
|
||||
@@ -46,6 +45,7 @@ import {
|
||||
readAnthropicPromptUsageSnapshot,
|
||||
readAnthropicUsageTokenCount,
|
||||
readLastAnthropicIterationUsage,
|
||||
resolveAnthropicFallbackServingModelCost,
|
||||
supportsClaudeAdaptiveThinking,
|
||||
supportsClaudeNativeMaxEffort,
|
||||
supportsClaudeNativeXhighEffort,
|
||||
@@ -1442,6 +1442,16 @@ export function createAnthropicMessagesTransportStreamFn(): StreamFn {
|
||||
const usage = message?.usage ?? {};
|
||||
output.responseId = typeof message?.id === "string" ? message.id : undefined;
|
||||
output.responseModel = typeof message?.model === "string" ? message.model : undefined;
|
||||
if (refusalBuffer) {
|
||||
costModel = {
|
||||
...model,
|
||||
cost: resolveAnthropicFallbackServingModelCost({
|
||||
requestedModelId: model.id,
|
||||
servingModelId: output.responseModel ?? null,
|
||||
requestedCost: model.cost,
|
||||
}),
|
||||
};
|
||||
}
|
||||
const promptUsage = readAnthropicPromptUsageSnapshot(usage);
|
||||
const messageStartPromptTokens = promptUsage
|
||||
? promptUsage.input + promptUsage.cacheRead + promptUsage.cacheWrite
|
||||
@@ -1516,7 +1526,14 @@ export function createAnthropicMessagesTransportStreamFn(): StreamFn {
|
||||
// Cost intentionally mirrors top-level usage (serving attempt at
|
||||
// serving-model rates). A mid-stream decline's billed partial is
|
||||
// only in usage.iterations and is not folded in here.
|
||||
costModel = { ...model, cost: CLAUDE_OPUS_48_FALLBACK_MODEL_COST };
|
||||
costModel = {
|
||||
...model,
|
||||
cost: resolveAnthropicFallbackServingModelCost({
|
||||
requestedModelId: model.id,
|
||||
servingModelId: fallbackBoundary.toModel,
|
||||
requestedCost: model.cost,
|
||||
}),
|
||||
};
|
||||
calculateCost(costModel, output.usage);
|
||||
eventSink.push({ type: "start", partial: output as never });
|
||||
for (const [i, block] of output.content.entries()) {
|
||||
|
||||
Reference in New Issue
Block a user