/** * Text-generation core for every Anthropic text/reasoning `ModelType`. Exposes * the per-slot handlers (`handleTextSmall`, `handleTextLarge`, * `handleReasoningLarge`, `handleActionPlanner`, …), each of which resolves its * default model from `utils/config` and calls the shared `generateTextWithModel`. * * `resolveTextParams` normalizes the request before it reaches the AI SDK: * builds the canonical system prompt, applies prompt-cache breakpoints, forces * `temperature=1` for opus-4 / temperature-locked models, drops `topP` when * both topP and temperature are set (the API rejects both), and caps * `maxTokens`. Streaming vs non-streaming is chosen per request; tool-using and * `ELIZA_ANTHROPIC_DISABLE_STREAM` requests take the non-streaming path to avoid * `AI_NoOutputGeneratedError` on tool_use-only responses. `responseSchema` * requests build a native AI SDK `output` object and return parsed JSON. * * When the auth mode is `cli`, generation is delegated to `claude -p` via * `generateViaCli` / `streamViaCli` instead of the SDK client. */ import type { GenerateTextParams, IAgentRuntime, ModelTypeName, PromptSegment, TextStreamResult, } from "@elizaos/core"; import { buildCanonicalSystemPrompt, dropDuplicateLeadingSystemMessage, logger, ModelType, resolveEffectiveSystemPrompt, } from "@elizaos/core"; import { generateText, type JSONSchema7, jsonSchema, type ModelMessage, streamText, type ToolChoice, type ToolSet, type UserContent, } from "ai"; import { createAnthropicClientWithTopPSupport } from "../providers/anthropic"; import { createModelName, type ModelName, type ModelSize } from "../types"; import { generateViaCli, streamViaCli } from "../utils/claude-cli"; import { type AnthropicEffort, getActionPlannerModel, getAnthropicEffort, getAuthMode, getCoTBudget, getExperimentalTelemetry, getLargeModel, getMaxOutputTokensOverride, getMediumModel, getMegaModel, getNanoModel, getReasoningLargeModel, getReasoningSmallModel, getResponseHandlerModel, getSmallModel, isTemperatureLockedModel, } from "../utils/config"; import { emitModelUsageEvent } from "../utils/events"; import { executeWithRetry, formatModelError } from "../utils/retry"; type ProviderOptionValue = | string | number | boolean | null | ProviderOptionValue[] | { [key: string]: ProviderOptionValue | undefined }; interface ProviderOptions { [key: string]: ProviderOptionValue | undefined; readonly agentName?: string; readonly anthropic?: AnthropicProviderOptions; } interface AnthropicProviderOptions { [key: string]: ProviderOptionValue | undefined; readonly thinking?: | { [key: string]: ProviderOptionValue | undefined; readonly type: "enabled"; readonly budgetTokens: number; } | { [key: string]: ProviderOptionValue | undefined; readonly type: "adaptive"; }; /** output_config.effort on the wire; see getAnthropicEffort. */ readonly effort?: AnthropicEffort; readonly cacheControl?: { [key: string]: ProviderOptionValue | undefined; readonly type: "ephemeral"; readonly ttl?: "5m" | "1h"; }; } type ChatAttachment = { data: string | Uint8Array | URL; mediaType: string; filename?: string; }; interface ResolvedTextParams { readonly prompt: string; readonly stopSequences: readonly string[]; readonly maxTokens: number; readonly temperature: number | undefined; readonly topP: number | undefined; readonly frequencyPenalty: number; readonly presencePenalty: number; readonly providerOptions: ProviderOptions; } interface GenerateTextParamsWithProviderOptions extends Omit< GenerateTextParams, "messages" | "tools" | "toolChoice" | "responseSchema" | "providerOptions" > { attachments?: ChatAttachment[]; messages?: ModelMessage[]; tools?: ToolSet; toolChoice?: ToolChoice; responseSchema?: unknown; providerOptions?: ProviderOptions; } function resolveRequestedModelName(params: GenerateTextParams, fallback: ModelName): ModelName { const requestedModel = (params as GenerateTextParams & { model?: unknown }).model; return typeof requestedModel === "string" && requestedModel.trim().length > 0 ? createModelName(requestedModel.trim()) : fallback; } type NativeOutput = NonNullable>[0]["output"]>; type NativeGenerateTextParams = Parameters>[0]; type NativeStreamTextParams = Parameters>[0]; type NativePrompt = | { prompt: string; messages?: never } | { messages: ModelMessage[]; prompt?: never }; type NativeTextParams = Omit & Omit & NativePrompt; type NativeProviderOptions = NativeTextParams["providerOptions"]; type NativeTelemetrySettings = NativeTextParams["experimental_telemetry"]; type AnthropicCacheControl = NonNullable["cacheControl"]>; type AnthropicCacheBreakpoint = { segmentIndex?: number; ttl?: "short" | "long" | "5m" | "1h"; cacheControl?: AnthropicCacheControl; }; interface AnthropicUsageWithCache { // Legacy (older AI SDK / direct Anthropic SDK) field names — kept for // back-compat with stream usage emitted in pre-v6 callers. promptTokens?: number; completionTokens?: number; cacheReadInputTokens?: number; cacheCreationInputTokens?: number; // AI SDK v6 LanguageModelUsage shape — what `generateText`/`streamText` // actually return today. The Anthropic provider populates // `inputTokenDetails.cacheReadTokens` for cache hits, and exposes // `cacheCreationInputTokens` via `providerMetadata.anthropic` (read by the // caller, not on the usage object directly). inputTokens?: number; outputTokens?: number; totalTokens?: number; cachedInputTokens?: number; inputTokenDetails?: { noCacheTokens?: number; cacheReadTokens?: number; cacheWriteTokens?: number; }; } interface AnthropicNormalizedUsage { promptTokens: number; completionTokens: number; totalTokens: number; cacheReadInputTokens?: number; cacheCreationInputTokens?: number; } interface NativeGenerateTextResult { text: string; toolCalls?: unknown[]; finishReason?: string; usage?: AnthropicNormalizedUsage; providerMetadata?: Record; } const TEXT_NANO_MODEL_TYPE = ModelType.TEXT_NANO as ModelTypeName; const TEXT_MEDIUM_MODEL_TYPE = ModelType.TEXT_MEDIUM as ModelTypeName; const TEXT_MEGA_MODEL_TYPE = ModelType.TEXT_MEGA as ModelTypeName; const RESPONSE_HANDLER_MODEL_TYPE = ModelType.RESPONSE_HANDLER as ModelTypeName; const ACTION_PLANNER_MODEL_TYPE = ModelType.ACTION_PLANNER as ModelTypeName; type TextModelType = | typeof TEXT_NANO_MODEL_TYPE | typeof ModelType.TEXT_SMALL | typeof TEXT_MEDIUM_MODEL_TYPE | typeof ModelType.TEXT_LARGE | typeof TEXT_MEGA_MODEL_TYPE | typeof RESPONSE_HANDLER_MODEL_TYPE | typeof ACTION_PLANNER_MODEL_TYPE | typeof TEXT_REASONING_SMALL_MODEL_TYPE | typeof TEXT_REASONING_LARGE_MODEL_TYPE; type AnthropicTextPart = { type: "text"; text: string; providerOptions?: { anthropic?: { cacheControl?: AnthropicCacheControl; }; }; }; type AnthropicFilePart = { type: "file"; data: string | Uint8Array | URL; mediaType: string; filename?: string; }; type AnthropicUserContentPart = AnthropicTextPart | AnthropicFilePart; function isProviderOptionValue(value: unknown): value is ProviderOptionValue { if ( value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ) { return true; } if (Array.isArray(value)) { return value.every(isProviderOptionValue); } if (typeof value === "object" && value !== null) { return Object.values(value).every( (entry) => entry === undefined || isProviderOptionValue(entry) ); } return false; } function readProviderOptions(value: unknown): ProviderOptions | undefined { if (typeof value !== "object" || value === null || Array.isArray(value)) { return undefined; } const entries = Object.entries(value); if (!entries.every(([, entry]) => entry === undefined || isProviderOptionValue(entry))) { return undefined; } return Object.fromEntries(entries) as ProviderOptions; } function isRecord(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); } function isModelMessage(value: unknown): value is ModelMessage { if (!isRecord(value) || typeof value.role !== "string") { return false; } switch (value.role) { case "system": return typeof value.content === "string"; case "user": case "tool": // Eliza runtime synthesizes tool / user messages with string or array // content (see buildStageChatMessages); the AI SDK accepts these and // the underlying provider normalizes them. return typeof value.content === "string" || Array.isArray(value.content); case "assistant": // Most callers emit string-or-array content. Defensively also accept // assistant messages with `content: null` when a tool call is attached // — the OpenAI v0.x / legacy shape that some callers still produce. // Without this, `readModelMessages` returns `undefined` and the AI SDK // silently drops the entire conversation, blinding any downstream model // call to the tool history. if (typeof value.content === "string" || Array.isArray(value.content)) { return true; } if (value.content === null || value.content === undefined) { return Array.isArray(value.toolCalls) && value.toolCalls.length > 0; } return false; default: return false; } } function readModelMessages(value: GenerateTextParams["messages"]): ModelMessage[] | undefined { if (!value) { return undefined; } const messages: ModelMessage[] = []; for (const message of value) { if (!isModelMessage(message)) { return undefined; } messages.push(message as ModelMessage); } return messages; } function readToolSet(value: GenerateTextParams["tools"]): ToolSet | undefined { if (!value) { return undefined; } // Source can be either an array of ToolDefinition (each with .name) or a // Record. ELIZAOS upstream sometimes passes the array as a // Record with numeric keys (`{0: tool, 1: tool}`), which makes the AI SDK // wire the tool name as "0" / "1" — the runtime parser then can't match // the response against canonical names like HANDLE_RESPONSE / PLAN_ACTIONS. // Walk both forms and rebuild keyed by tool.name when present. Heterogeneous // Records (raw ToolDefinitions mixed with already-built AI SDK Tool objects // that lack `.name`) preserve the SDK Tool entries under their original key // so we don't silently drop them. Two passes so named-tool keys always win // deterministically over an SDK passthrough at the same key, regardless of // iteration order. const isArr = Array.isArray(value); const entries: Array<[string, unknown]> = isArr ? (value as unknown[]).map((v, i) => [String(i), v] as [string, unknown]) : Object.entries(value as Record); const namedKeys = new Set(); for (const [, rawTool] of entries) { if (isRecord(rawTool) && typeof rawTool.name === "string" && rawTool.name) { namedKeys.add(rawTool.name); } } const tools: Record = {}; let sawNamedTool = false; for (const [origKey, rawTool] of entries) { if (!isRecord(rawTool)) { continue; } if (typeof rawTool.name === "string" && rawTool.name) { sawNamedTool = true; const schema = isRecord(rawTool.parameters) ? (rawTool.parameters as JSONSchema7) : isRecord(rawTool.input_schema) ? (rawTool.input_schema as JSONSchema7) : ({ type: "object" } satisfies JSONSchema7); tools[rawTool.name] = { ...(typeof rawTool.description === "string" ? { description: rawTool.description } : {}), inputSchema: jsonSchema(schema), }; } else if (!isArr && !namedKeys.has(origKey)) { // Pre-built AI SDK Tool entry inside a Record — pass through under its // original string key, but only if no named tool will claim that key // later in the same pass; otherwise the named tool would silently // overwrite (or be overwritten by) this entry depending on order. tools[origKey] = rawTool; } } if (sawNamedTool) { return Object.keys(tools).length > 0 ? (tools as ToolSet) : undefined; } // Fall back to the original Record (already keyed by canonical names). return !isArr && isRecord(value) ? (value as ToolSet) : undefined; } function readToolChoice(value: GenerateTextParams["toolChoice"]): ToolChoice | undefined { if (!value) { return undefined; } if (typeof value === "string" && (value === "auto" || value === "none" || value === "required")) { return value; } if (!isRecord(value)) { return undefined; } const choice = value as Record; if (choice.type === "tool" && typeof choice.name === "string") { return { type: "tool", toolName: choice.name }; } if (choice.type === "function" && isRecord(choice.function)) { const name = choice.function.name; return typeof name === "string" ? { type: "tool", toolName: name } : undefined; } return typeof choice.name === "string" ? { type: "tool", toolName: choice.name } : undefined; } function toAnthropicTextParams(params: GenerateTextParams): GenerateTextParamsWithProviderOptions { const { messages, providerOptions, tools, toolChoice, ...rest } = params; const normalized: GenerateTextParamsWithProviderOptions = { ...rest, messages: readModelMessages(messages), tools: readToolSet(tools), toolChoice: readToolChoice(toolChoice), providerOptions: readProviderOptions(providerOptions), }; return normalized; } function isOpus4Model(modelName: ModelName): boolean { return modelName.toLowerCase().includes("opus-4"); } /** * Whether a model accepts the effort parameter (output_config.effort) at all. * Live-probed 2026-07-12: haiku-4-5 rejects both `effort` ("This model does * not support the effort parameter") and adaptive thinking, so sending the * knob 400s every request. Claude-3-era models predate the parameter. Mirrors * the server-side model catalog (packages/agent/src/api/model-catalog.ts). */ function supportsEffortParameter(modelName: ModelName): boolean { const name = modelName.toLowerCase(); return !name.includes("haiku") && !name.includes("claude-3"); } /** * Whether a model accepts the xhigh/max effort tiers. Mirrors the server-side * model catalog (packages/agent/src/api/model-catalog.ts): fable-5 and * opus >= 4.7 take the full range; everything else caps at high — sending * higher 400s the request. */ function supportsExtendedEffort(modelName: ModelName): boolean { const name = modelName.toLowerCase(); if (name.includes("fable-5")) return true; const opus = name.match(/opus-4-(\d+)/); return opus !== null && Number(opus[1]) >= 7; } /** * Clamp a configured effort to what the resolved model accepts. Clamping (to * "high") rather than dropping keeps the operator's intent — they asked for * maximum reasoning; the model's ceiling is the closest legal request. */ function clampEffortForModel(effort: AnthropicEffort, modelName: ModelName): AnthropicEffort { if ((effort === "xhigh" || effort === "max") && !supportsExtendedEffort(modelName)) { logger.warn( `[Anthropic] effort "${effort}" is not supported by ${modelName}; clamping to "high"` ); return "high"; } return effort; } function buildUserContent(params: GenerateTextParamsWithProviderOptions): UserContent { const content: AnthropicUserContentPart[] = [{ type: "text", text: params.prompt ?? "" }]; appendAttachments(content, params.attachments); return content; } function appendAttachments( content: AnthropicUserContentPart[], attachments: ChatAttachment[] | undefined ): void { for (const attachment of attachments ?? []) { content.push({ type: "file", data: attachment.data, mediaType: attachment.mediaType, ...(attachment.filename ? { filename: attachment.filename } : {}), }); } } function buildSegmentedUserContent( params: GenerateTextParamsWithProviderOptions, anthropicOptions?: ProviderOptions["anthropic"], fallbackCacheControl?: AnthropicCacheControl, reservedNonSegmentBreakpoints = 0 ): UserContent { const segmentCacheControls = buildSegmentCacheControls( params, anthropicOptions, fallbackCacheControl, reservedNonSegmentBreakpoints ); return buildSegmentedUserContentFromSegments( params.promptSegments ?? [], params.attachments, segmentCacheControls ); } function buildSegmentedUserContentFromSegments( segments: readonly PromptSegment[], attachments: ChatAttachment[] | undefined, segmentCacheControls: Map = new Map() ): UserContent { const content: AnthropicUserContentPart[] = []; for (const [index, segment] of segments.entries()) { const textPart: AnthropicTextPart = { type: "text", text: segment.content, }; const cacheControl = segmentCacheControls.get(index); if (cacheControl) { textPart.providerOptions = { anthropic: { cacheControl } }; } content.push(textPart); } appendAttachments(content, attachments); return content; } function buildSegmentedUserContentForMessages( params: GenerateTextParamsWithProviderOptions ): UserContent | undefined { const dynamicSegments = (params.promptSegments ?? []).filter( (segment: PromptSegment) => !segment.stable ); if (dynamicSegments.length === 0 && (params.attachments?.length ?? 0) === 0) { return undefined; } return buildSegmentedUserContentFromSegments(dynamicSegments, params.attachments); } function buildPlannerWireMessages( wireMessages: ModelMessage[], userContent: UserContent | string ): ModelMessage[] { if (wireMessages[0]?.role === "user") { const [first, ...tail] = wireMessages; return [{ ...first, content: userContent }, ...tail]; } return [{ role: "user", content: userContent }, ...wireMessages]; } function buildSegmentCacheControls( params: GenerateTextParamsWithProviderOptions, anthropicOptions?: ProviderOptions["anthropic"], fallbackCacheControl?: AnthropicCacheControl, reservedNonSegmentBreakpoints = 0 ): Map { const controls = new Map(); if (!fallbackCacheControl) { return controls; } const maxBreakpointsRaw = anthropicOptions?.maxBreakpoints; const maxBreakpoints = typeof maxBreakpointsRaw === "number" && Number.isFinite(maxBreakpointsRaw) ? Math.max(0, Math.floor(maxBreakpointsRaw)) : 4; // Anthropic allows at most 4 cache_control breakpoints per request. The // budget is spent in priority order: // 1. system prompt (cacheSystem !== false) // 2. non-segment reservations passed by the caller — currently the tools // array tail breakpoint (tools render before system, so caching them // is the widest shared prefix for tool-heavy agents) // 3. stable prompt segments (whatever budget remains) // The trajectory-tail breakpoint only exists on the native-messages path, // which never stamps segment breakpoints, so it never competes here. const systemConsumesBreakpoint = anthropicOptions?.cacheSystem !== false; const maxSegmentBreakpoints = Math.max( 0, maxBreakpoints - (systemConsumesBreakpoint ? 1 : 0) - Math.max(0, Math.floor(reservedNonSegmentBreakpoints)) ); if (maxSegmentBreakpoints === 0) { return controls; } const plannedBreakpoints = Array.isArray(anthropicOptions?.cacheBreakpoints) ? (anthropicOptions.cacheBreakpoints as AnthropicCacheBreakpoint[]) : undefined; if (plannedBreakpoints) { // When the plan carries more breakpoints than the remaining budget, keep // the LAST N (highest segment indexes). A breakpoint caches everything // before it, so later breakpoints produce the longest matching prefix; // dropping the earliest ones only loses partial-prefix granularity. for (const breakpoint of plannedBreakpoints.slice(-maxSegmentBreakpoints)) { if (typeof breakpoint.segmentIndex !== "number") { continue; } controls.set( breakpoint.segmentIndex, normalizeBreakpointCacheControl(breakpoint, fallbackCacheControl) ); } return controls; } // Pick the LAST N stable segments rather than the first N. A cache_control // breakpoint says "everything up to here is cached"; placing breakpoints at // late stable segments creates the longest matching cached prefix on // subsequent calls. Earlier stable segments still ride along inside any // longer matching prefix that a later breakpoint creates — we lose // granularity on partial-prefix hits but not coverage. const stableIndices: number[] = []; (params.promptSegments ?? []).forEach((segment: PromptSegment, index: number) => { if (segment.stable) stableIndices.push(index); }); for (const index of stableIndices.slice(-maxSegmentBreakpoints)) { controls.set(index, fallbackCacheControl); } return controls; } function normalizeBreakpointCacheControl( breakpoint: AnthropicCacheBreakpoint, fallbackCacheControl: AnthropicCacheControl ): AnthropicCacheControl { if (isAnthropicCacheControl(breakpoint.cacheControl)) { return breakpoint.cacheControl; } if (breakpoint.ttl === "long" || breakpoint.ttl === "1h") { return { type: "ephemeral", ttl: "1h" }; } if (breakpoint.ttl === "short" || breakpoint.ttl === "5m") { return { ...fallbackCacheControl }; } return fallbackCacheControl; } function isAnthropicCacheControl(value: unknown): value is AnthropicCacheControl { return ( typeof value === "object" && value !== null && (value as { type?: unknown }).type === "ephemeral" ); } function getRuntimeCacheControl(runtime: IAgentRuntime): AnthropicCacheControl { // cache_control is always emitted for stable segments — Anthropic requires it. // TTL is configurable via ANTHROPIC_PROMPT_CACHE_TTL ("5m" | "1h"); default is "5m". const ttlSetting = runtime.getSetting("ANTHROPIC_PROMPT_CACHE_TTL"); if (typeof ttlSetting === "string") { const ttl = ttlSetting.trim().toLowerCase(); if (ttl === "1h") { return { type: "ephemeral", ttl: "1h" }; } } return { type: "ephemeral" }; } function buildCacheableSystemPrompt( systemPrompt: string | undefined, cacheControl: AnthropicCacheControl | undefined ): NativeTextParams["system"] { if (!systemPrompt) { return undefined; } if (!cacheControl) { return systemPrompt; } return { role: "system", content: systemPrompt, providerOptions: { anthropic: { cacheControl }, }, }; } function stripLocalAnthropicCacheOptions( anthropicOptions: ProviderOptions["anthropic"] | undefined ): ProviderOptions["anthropic"] | undefined { if (!anthropicOptions) { return undefined; } const { cacheControl: _cacheControl, cacheBreakpoints: _cacheBreakpoints, cacheSystem: _cacheSystem, maxBreakpoints: _maxBreakpoints, cacheTools: _cacheTools, cacheTrajectory: _cacheTrajectory, ...wireOptions } = anthropicOptions as Record; return Object.keys(wireOptions).length > 0 ? (wireOptions as ProviderOptions["anthropic"]) : undefined; } /** * Stamp a cache_control breakpoint on the LAST tool in the tool set. Tools * render before `system` and `messages` in Anthropic's prompt, so a single * breakpoint after the last tool caches the entire (stable) tool catalog — * the widest shared prefix for tool-heavy agents. Consumes one of the four * breakpoints; callers must reserve budget for it (see * `buildSegmentCacheControls`). A tool that already carries an explicit * cacheControl wins; the input tool set is never mutated. */ function applyToolsCacheBreakpoint(tools: ToolSet, cacheControl: AnthropicCacheControl): ToolSet { const names = Object.keys(tools); const lastName = names[names.length - 1]; if (!lastName) { return tools; } const lastTool = tools[lastName]; if (!isRecord(lastTool)) { return tools; } const existingProviderOptions = isRecord(lastTool.providerOptions) ? lastTool.providerOptions : {}; const existingAnthropic = isRecord(existingProviderOptions.anthropic) ? (existingProviderOptions.anthropic as Record) : {}; if (existingAnthropic.cacheControl) { return tools; } return { ...tools, [lastName]: { ...lastTool, providerOptions: { ...existingProviderOptions, anthropic: { ...existingAnthropic, cacheControl }, }, }, } as ToolSet; } const TRAJECTORY_CACHEABLE_PART_TYPES = new Set(["text", "tool-call", "tool-result"]); /** * Stamp a cache_control breakpoint on the last content part of the final * assistant/tool message — the tail of the kept trajectory history. The * planner loop's assistant/tool suffix grows append-only across iterations, * so a breakpoint at the tail lets every subsequent planner call read the * whole prior trajectory (system + tools + context + earlier tool calls) * from cache and re-process only the newly appended turn. * * Deliberately skipped when the final message is a `user` turn: on this wire * shape the leading user message carries the per-turn dynamic context, and a * `[user]`-only request (planner iteration 1, plain chat) would stamp * volatile content — a cache write that is never read back. String-content * tails (legacy shapes) are also skipped; the planner always emits part * arrays. The input array is never mutated; a part that already carries an * explicit cacheControl wins. */ function applyTrajectoryTailCacheBreakpoint( messages: ModelMessage[], cacheControl: AnthropicCacheControl ): ModelMessage[] { const lastIndex = messages.length - 1; const last = messages[lastIndex]; if (!last || (last.role !== "assistant" && last.role !== "tool")) { return messages; } if (!Array.isArray(last.content) || last.content.length === 0) { return messages; } const parts = last.content as unknown[]; const lastPart = parts[parts.length - 1]; if ( !isRecord(lastPart) || typeof lastPart.type !== "string" || !TRAJECTORY_CACHEABLE_PART_TYPES.has(lastPart.type) ) { return messages; } const existingProviderOptions = isRecord(lastPart.providerOptions) ? lastPart.providerOptions : {}; const existingAnthropic = isRecord(existingProviderOptions.anthropic) ? (existingProviderOptions.anthropic as Record) : {}; if (existingAnthropic.cacheControl) { return messages; } const stampedPart = { ...lastPart, providerOptions: { ...existingProviderOptions, anthropic: { ...existingAnthropic, cacheControl }, }, }; const nextMessages = [...messages]; nextMessages[lastIndex] = { ...last, content: [...parts.slice(0, -1), stampedPart], } as ModelMessage; return nextMessages; } function firstNumber(...values: unknown[]): number | undefined { for (const value of values) { if (typeof value === "number" && Number.isFinite(value)) { return value; } } return undefined; } function readAnthropicCacheCreationFromProviderMetadata( providerMetadata: unknown ): number | undefined { if ( !providerMetadata || typeof providerMetadata !== "object" || Array.isArray(providerMetadata) ) { return undefined; } const anthropic = (providerMetadata as Record).anthropic; if (!anthropic || typeof anthropic !== "object" || Array.isArray(anthropic)) { return undefined; } const value = (anthropic as Record).cacheCreationInputTokens; return typeof value === "number" && Number.isFinite(value) ? value : undefined; } function normalizeAnthropicUsage( usage: AnthropicUsageWithCache | undefined, providerMetadata?: unknown ): AnthropicNormalizedUsage | undefined { if (!usage) { return undefined; } const promptTokens = firstNumber(usage.promptTokens, usage.inputTokens) ?? 0; const completionTokens = firstNumber(usage.completionTokens, usage.outputTokens) ?? 0; // The AI SDK v6 Anthropic provider reports cache reads via // `inputTokenDetails.cacheReadTokens` (and the deprecated `cachedInputTokens` // mirror). Older callers may still pass the legacy `cacheReadInputTokens` // field directly. Read both. const cacheRead = firstNumber( usage.cacheReadInputTokens, usage.inputTokenDetails?.cacheReadTokens, usage.cachedInputTokens ); // Cache writes ride on `inputTokenDetails.cacheWriteTokens` in the v6 SDK // shape, with the canonical count exposed via // `providerMetadata.anthropic.cacheCreationInputTokens`. Either source is // authoritative; fall back to the legacy direct field for callers that still // emit the pre-v6 shape (e.g. our streaming usage promise). const cacheCreation = firstNumber( usage.cacheCreationInputTokens, usage.inputTokenDetails?.cacheWriteTokens, readAnthropicCacheCreationFromProviderMetadata(providerMetadata) ); return { promptTokens, completionTokens, totalTokens: usage.totalTokens ?? promptTokens + completionTokens, ...(cacheRead !== undefined ? { cacheReadInputTokens: cacheRead } : {}), ...(cacheCreation !== undefined ? { cacheCreationInputTokens: cacheCreation } : {}), }; } function buildStructuredOutput(responseSchema: unknown): NativeOutput { if ( responseSchema && typeof responseSchema === "object" && "responseFormat" in responseSchema && "parseCompleteOutput" in responseSchema ) { return responseSchema as NativeOutput; } const schemaOptions = responseSchema && typeof responseSchema === "object" && "schema" in responseSchema ? (responseSchema as { schema: unknown; name?: string; description?: string }) : { schema: responseSchema }; return { name: "object", responseFormat: Promise.resolve({ type: "json" as const, schema: schemaOptions.schema as JSONSchema7, ...(schemaOptions.name ? { name: schemaOptions.name } : {}), ...(schemaOptions.description ? { description: schemaOptions.description } : {}), }), async parseCompleteOutput({ text }: { text: string }) { return JSON.parse(text); }, async parsePartialOutput(): Promise { return undefined; }, createElementStreamTransform(): undefined { return undefined; }, } satisfies NativeOutput; } function usesNativeTextResult(params: GenerateTextParamsWithProviderOptions): boolean { return Boolean(params.messages || params.tools || params.toolChoice || params.responseSchema); } function buildNativeTextResult( result: { text: string; toolCalls?: unknown[]; finishReason?: string; usage?: AnthropicUsageWithCache; providerMetadata?: unknown; }, modelName?: string ): NativeGenerateTextResult { return { text: result.text, toolCalls: result.toolCalls ?? [], finishReason: result.finishReason, usage: normalizeAnthropicUsage(result.usage, result.providerMetadata), providerMetadata: mergeProviderModelName(result.providerMetadata, modelName), }; } function mergeProviderModelName( providerMetadata: unknown, modelName?: string ): Record | undefined { if (!modelName) { return providerMetadata && typeof providerMetadata === "object" && !Array.isArray(providerMetadata) ? (providerMetadata as Record) : undefined; } if ( providerMetadata && typeof providerMetadata === "object" && !Array.isArray(providerMetadata) ) { return { ...(providerMetadata as Record), modelName, }; } return { modelName }; } function resolveTextParams( runtime: IAgentRuntime, params: GenerateTextParamsWithProviderOptions, modelName: ModelName, cotBudget: number, effort?: AnthropicEffort ): ResolvedTextParams { const prompt = params.prompt ?? ""; const stopSequences = params.stopSequences ?? []; const frequencyPenalty = params.frequencyPenalty ?? 0.7; const presencePenalty = params.presencePenalty ?? 0.7; const hasTopP = params.topP !== undefined; const hasTemperature = params.temperature !== undefined; let temperature: number | undefined; let topP: number | undefined; if (hasTopP && hasTemperature) { // Anthropic only supports one at a time; prefer temperature, drop topP logger.warn( "[Anthropic] Both temperature and topP provided; using temperature only (Anthropic API limitation)." ); temperature = params.temperature; topP = undefined; } else if (hasTopP) { topP = params.topP; temperature = undefined; } else { temperature = params.temperature ?? 0.7; topP = undefined; } // Temperature-locked models only accept temperature=1; Anthropic returns 400 // "Invalid request data" otherwise. ANTHROPIC_TEMPERATURE_LOCKED_MODELS lets // an operator declare the constraint for any model id (new releases the // substring heuristic can't know about); the opus-4 name check remains the // built-in default. const temperatureLocked = isTemperatureLockedModel(runtime, modelName) || isOpus4Model(modelName); if (temperatureLocked && temperature !== undefined && temperature !== 1) { temperature = 1; } const defaultMaxTokens = modelName.includes("-3-") ? 4096 : 8192; // Cap output tokens at the model's hard limit. Opus 4.x = 32k, Sonnet 4.x = 64k. // Callers (eliza runtime) sometimes pass the prompt context window (128k+) as // maxTokens, which the API rejects with "Invalid request data". // ANTHROPIC_MAX_OUTPUT_TOKENS overrides the heuristic (bare number or // per-model `id:tokens` pairs) so unknown ids get the right ceiling. const modelHardCap = getMaxOutputTokensOverride(runtime, modelName) ?? (isOpus4Model(modelName) ? 32_000 : 64_000); // Anthropic's Messages API REQUIRES max_tokens — an opt-out caller (direct- // channel Stage-1) can't drop it, so send the model's hard cap. The reply is // then bounded only by the model's real max (never an arbitrary 8192), and the // value never 400s because it equals the documented limit. Other callers keep // the existing default, Math.min-capped. const maxTokens = params.omitMaxTokens ? modelHardCap : Math.min(params.maxTokens ?? defaultMaxTokens, modelHardCap); const rawProviderOptions = params.providerOptions; const rawAnthropicOptions = rawProviderOptions?.anthropic; const baseProviderOptions: ProviderOptions = rawProviderOptions ? { ...rawProviderOptions, anthropic: rawAnthropicOptions && typeof rawAnthropicOptions === "object" ? { ...(rawAnthropicOptions as Record) } : undefined, } : {}; // Effort (the modern knob — maps to the API's output_config.effort, paired // with adaptive thinking) wins over the legacy fixed CoT budget when both // are configured; the budget shape stays for existing ANTHROPIC_COT_BUDGET // operators. A model without the effort parameter falls back to the budget // path (or nothing) — sending the knob anyway would 400 every request. let clampedEffort = effort !== undefined ? clampEffortForModel(effort, modelName) : undefined; if (clampedEffort !== undefined && !supportsEffortParameter(modelName)) { logger.warn( `[Anthropic] effort is configured but ${modelName} does not support the effort parameter; ignoring it for this model` ); clampedEffort = undefined; } const providerOptions: ProviderOptions = clampedEffort !== undefined ? { ...baseProviderOptions, anthropic: { ...(baseProviderOptions.anthropic ?? {}), thinking: { type: "adaptive" }, effort: clampedEffort, }, } : cotBudget > 0 ? { ...baseProviderOptions, anthropic: { ...(baseProviderOptions.anthropic ?? {}), thinking: { type: "enabled", budgetTokens: cotBudget }, }, } : baseProviderOptions; // Thinking-enabled requests only accept temperature=1 and reject topP — the // API 400s otherwise. The opus-4 lock above covers those models regardless // of thinking; this covers thinking on everything else. if (clampedEffort !== undefined || cotBudget > 0) { if (temperature !== undefined && temperature !== 1) { temperature = 1; } if (topP !== undefined) { logger.warn("[Anthropic] dropping topP: not accepted alongside extended thinking"); topP = undefined; } } return { prompt, stopSequences, maxTokens, temperature, topP, frequencyPenalty, presencePenalty, providerOptions, }; } async function generateTextWithModel( runtime: IAgentRuntime, params: GenerateTextParams, modelName: ModelName, modelSize: ModelSize, modelType: TextModelType ): Promise { const paramsWithAttachments = toAnthropicTextParams(params); const shouldReturnNativeResult = usesNativeTextResult(paramsWithAttachments); const systemPrompt = resolveEffectiveSystemPrompt({ params: paramsWithAttachments, fallback: buildCanonicalSystemPrompt({ character: runtime.character }), }); const cotBudget = getCoTBudget(runtime, modelSize); const effort = getAnthropicEffort(runtime, modelSize); const resolved = resolveTextParams(runtime, paramsWithAttachments, modelName, cotBudget, effort); if (getAuthMode(runtime) === "cli") { if (shouldReturnNativeResult) { throw new Error( "[Anthropic] Native messages, tools, toolChoice, and responseSchema are not supported when ANTHROPIC_AUTH_MODE=cli." ); } if (params.stream) { return streamViaCli( runtime, resolved.prompt, modelName, modelType, params.maxTokens, systemPrompt ); } const result = await generateViaCli( runtime, resolved.prompt, modelName, modelType, params.maxTokens, systemPrompt ); return result.text; } const anthropic = createAnthropicClientWithTopPSupport(runtime); const experimentalTelemetry = getExperimentalTelemetry(runtime); logger.log(`[Anthropic] Using ${modelType} model: ${modelName}`); // cache_control is always-on: getRuntimeCacheControl always returns a value. // Callers can still override by supplying anthropic.cacheControl in providerOptions. const runtimeCacheControl = getRuntimeCacheControl(runtime); const providerOptions: ProviderOptions = { ...resolved.providerOptions, anthropic: { ...(resolved.providerOptions.anthropic ?? {}), ...(!resolved.providerOptions.anthropic?.cacheControl ? { cacheControl: runtimeCacheControl } : {}), }, }; const segmentedPrompt = Array.isArray(paramsWithAttachments.promptSegments) && paramsWithAttachments.promptSegments.length > 0; const cacheControl = providerOptions.anthropic?.cacheControl; const cacheSystem = providerOptions.anthropic?.cacheSystem !== false; // Tools-array breakpoint (one of the four): tools render first in // Anthropic's prompt, so caching the (stable) tool catalog benefits every // call that carries tools. Opt out per call with // providerOptions.anthropic.cacheTools = false. const hasNamedTools = paramsWithAttachments.tools ? Object.keys(paramsWithAttachments.tools).length > 0 : false; const cacheToolsEnabled = providerOptions.anthropic?.cacheTools !== false; const toolsCacheControl = hasNamedTools && cacheToolsEnabled && cacheControl ? cacheControl : undefined; const system = buildCacheableSystemPrompt(systemPrompt, cacheSystem ? cacheControl : undefined); const userContent = segmentedPrompt || (paramsWithAttachments.attachments?.length ?? 0) > 0 ? segmentedPrompt ? buildSegmentedUserContent( paramsWithAttachments, providerOptions.anthropic, cacheControl, toolsCacheControl ? 1 : 0 ) : buildUserContent(paramsWithAttachments) : undefined; const anthropicOptions = providerOptions.anthropic && (segmentedPrompt || system) ? stripLocalAnthropicCacheOptions(providerOptions.anthropic) : providerOptions.anthropic; const anthropicProviderOptions = anthropicOptions ? { anthropic: anthropicOptions } : undefined; const agentName = resolved.providerOptions.agentName; const telemetryConfig: NativeTelemetrySettings = { isEnabled: experimentalTelemetry, functionId: agentName ? `agent:${agentName}` : undefined, metadata: agentName ? { agentName } : undefined, }; const wireMessages = dropDuplicateLeadingSystemMessage( paramsWithAttachments.messages, systemPrompt ); // Planner / evaluator wire path: when the runtime passes BOTH `messages` // (system + user + assistant/tool trajectory built by `buildStageChatMessages`) // AND `promptSegments` (the same content as labeled stable/dynamic parts), // the segmented `userContent` carries cache_control on stable parts. Without // this branch the segmented content is built and discarded because the // messages path sends `wireMessages` directly with flat string content. We // inject `userContent` as the leading user message and keep the trajectory // turns verbatim. The leading user message in `wireMessages` was synthesized // from dynamic context that is fully covered by `promptSegments`, so we drop // it to avoid duplicating tokens. Unlike PR #7469 we keep `system` because // our `buildCacheableSystemPrompt` puts cache_control on the system param // itself (Anthropic's separate `system` parameter accepts cache_control via // providerOptions). const segmentedMessageUserContent = segmentedPrompt && paramsWithAttachments.messages ? buildSegmentedUserContentForMessages(paramsWithAttachments) : undefined; const basePromptOrMessages: NativePrompt = paramsWithAttachments.messages ? wireMessages && wireMessages.length > 0 ? segmentedMessageUserContent ? { messages: buildPlannerWireMessages(wireMessages, segmentedMessageUserContent) } : { messages: wireMessages } : { messages: [ { role: "user" as const, content: userContent ?? resolved.prompt, }, ], } : { messages: [ { role: "user" as const, content: userContent ?? resolved.prompt, }, ], }; // Kept-trajectory tail breakpoint (planner/evaluator wire path): stamp the // final assistant/tool turn so the next planner iteration reads the whole // prior trajectory from cache. The helper is a no-op for user-tail message // arrays (dynamic content) and string-content tails, so plain chat calls are // untouched. Opt out per call with providerOptions.anthropic.cacheTrajectory // = false. Budget: this path stamps no segment breakpoints, so system(1) + // tools(0..1) + trajectory(1) stays within Anthropic's four-breakpoint cap. const cacheTrajectoryEnabled = providerOptions.anthropic?.cacheTrajectory !== false; const promptOrMessages: NativePrompt = cacheControl && cacheTrajectoryEnabled && basePromptOrMessages.messages ? { messages: applyTrajectoryTailCacheBreakpoint(basePromptOrMessages.messages, cacheControl), } : basePromptOrMessages; const generateParams: NativeTextParams = { model: anthropic(modelName), ...promptOrMessages, system, temperature: resolved.temperature, stopSequences: resolved.stopSequences as string[], frequencyPenalty: resolved.frequencyPenalty, presencePenalty: resolved.presencePenalty, experimental_telemetry: telemetryConfig, maxOutputTokens: resolved.maxTokens, topP: resolved.topP, ...(paramsWithAttachments.tools ? { tools: toolsCacheControl ? applyToolsCacheBreakpoint(paramsWithAttachments.tools, toolsCacheControl) : paramsWithAttachments.tools, } : {}), ...(paramsWithAttachments.toolChoice ? { toolChoice: paramsWithAttachments.toolChoice } : {}), ...(paramsWithAttachments.responseSchema ? { output: buildStructuredOutput(paramsWithAttachments.responseSchema) } : {}), ...(anthropicProviderOptions ? { providerOptions: anthropicProviderOptions as NativeProviderOptions } : {}), }; const operationName = `${modelType} request using ${modelName}`; // Route tool-using requests (and any request when ELIZA_ANTHROPIC_DISABLE_STREAM=1) // to the non-streaming generateText path. The AI SDK streaming companion // promises raise AI_NoOutputGeneratedError when a response contains only // tool_use blocks and no text; generateText preserves response.toolCalls and // text reliably. `readToolSet` has already normalized tools to a ToolSet // record (or undefined), so a non-empty tool set means there are tool keys. const toolSet = paramsWithAttachments.tools; const hasToolSurface = (toolSet ? Object.keys(toolSet).length > 0 : false) || Boolean(paramsWithAttachments.toolChoice); const streamDisabled = process.env.ELIZA_ANTHROPIC_DISABLE_STREAM === "1" || hasToolSurface; // Structured-output calls must not stream: the parsed native object is only // available on the non-stream `generateText` result (returned via // `buildNativeTextResult` below). A streamed structured call would emit raw // text chunks and discard the parsed object, so fall through to generateText. if (params.stream && !streamDisabled && !paramsWithAttachments.responseSchema) { try { const streamResult = streamText(generateParams); // error-policy:J5 unhandled-rejection suppression — provider metadata is // usage-normalization enrichment only; the underlying stream failure is // observed in `textStreamWithUsage` (finishReason await rethrows). const providerMetadataPromise: Promise = Promise.resolve( (streamResult as { providerMetadata?: PromiseLike }).providerMetadata ).catch((): undefined => undefined); const usagePromise = Promise.resolve(streamResult.usage).then(async (usage) => { if (!usage) { return undefined; } // Normalize BEFORE emitting so the MODEL_USED event (and its // structured cache-usage log) carries cacheReadInputTokens / // cacheCreationInputTokens even in the AI SDK v6 usage shape, where // cache counts ride on inputTokenDetails / providerMetadata instead // of the legacy direct fields. const providerMetadata = await providerMetadataPromise; const normalizedUsage = normalizeAnthropicUsage( usage as AnthropicUsageWithCache, providerMetadata ); emitModelUsageEvent( runtime, modelType, resolved.prompt, normalizedUsage ?? (usage as AnthropicUsageWithCache), modelName ); return normalizedUsage; }); // error-policy:J5 unhandled-rejection suppression — usage emission is // telemetry; the underlying stream failure is observed in // `textStreamWithUsage` (finishReason await rethrows), never here. const ignoreUsageError = (): undefined => undefined; async function* textStreamWithUsage(): AsyncIterable { let completed = false; try { for await (const chunk of streamResult.textStream) { yield chunk; } // The AI SDK's `textStream` terminates with zero chunks on a hard // failure (auth/transport) instead of throwing — the real error // (e.g. APICallError 401) only rejects the companion promises. Await // `finishReason` here so an errored/empty stream re-throws the real // cause (matching the non-stream generateText branch) rather than // silently returning ''. The happy path resolves with a value. await streamResult.finishReason; completed = true; } catch (error) { // error-policy:J2 context-adding rethrow — formatModelError wraps the // provider error with `cause`; an errored/empty stream surfaces to // the consumer instead of silently yielding "". throw formatModelError(operationName, error); } finally { if (completed) { await usagePromise.catch(ignoreUsageError); } } } // error-policy:J5 unhandled-rejection suppression — the streaming path // primarily consumes `textStream`. The AI SDK's companion promises // (text/toolCalls/finishReason/usage) reject on an empty stream ("No // output generated") even when no caller awaits them, which otherwise // surfaces as an unhandled rejection. Attach a no-op catch so each bare // promise is always considered handled; real consumers still observe the // value or error. Mirrors plugin-openai's `handledPromise`. const handledPromise = (value: T | PromiseLike): Promise => { const promise = Promise.resolve(value); promise.catch(() => {}); return promise; }; return { textStream: textStreamWithUsage(), text: handledPromise( Promise.resolve(streamResult.text).then(async (text) => { await usagePromise.catch(ignoreUsageError); return text; }) ), ...(shouldReturnNativeResult ? { toolCalls: handledPromise(Promise.resolve(streamResult.toolCalls)) } : {}), usage: handledPromise(usagePromise), finishReason: handledPromise( Promise.resolve(streamResult.finishReason) as Promise ), }; } catch (error) { // error-policy:J2 context-adding rethrow — formatModelError wraps the // provider error with `cause` and a caller-facing reason. throw formatModelError(operationName, error); } } try { const response = await executeWithRetry(operationName, () => generateText(generateParams)); if (response.usage) { // Normalize BEFORE emitting so MODEL_USED (and the structured cache // log) carries cache read/write counts in the AI SDK v6 usage shape. emitModelUsageEvent( runtime, modelType, resolved.prompt, normalizeAnthropicUsage( response.usage as AnthropicUsageWithCache, response.providerMetadata ) ?? (response.usage as AnthropicUsageWithCache), modelName ); } if (shouldReturnNativeResult) { return buildNativeTextResult(response, modelName) as string & NativeGenerateTextResult; } return response.text; } catch (error) { // error-policy:J2 context-adding rethrow — formatModelError wraps the // provider error with `cause` and a caller-facing reason. throw formatModelError(operationName, error); } } export async function handleTextSmall( runtime: IAgentRuntime, params: GenerateTextParams ): Promise { const modelName = resolveRequestedModelName(params, getSmallModel(runtime)); return generateTextWithModel(runtime, params, modelName, "small", ModelType.TEXT_SMALL); } export async function handleTextLarge( runtime: IAgentRuntime, params: GenerateTextParams ): Promise { const modelName = resolveRequestedModelName(params, getLargeModel(runtime)); return generateTextWithModel(runtime, params, modelName, "large", ModelType.TEXT_LARGE); } export async function handleTextNano( runtime: IAgentRuntime, params: GenerateTextParams ): Promise { return generateTextWithModel( runtime, params, resolveRequestedModelName(params, getNanoModel(runtime)), "small", TEXT_NANO_MODEL_TYPE ); } export async function handleTextMedium( runtime: IAgentRuntime, params: GenerateTextParams ): Promise { return generateTextWithModel( runtime, params, resolveRequestedModelName(params, getMediumModel(runtime)), "large", TEXT_MEDIUM_MODEL_TYPE ); } export async function handleTextMega( runtime: IAgentRuntime, params: GenerateTextParams ): Promise { return generateTextWithModel( runtime, params, resolveRequestedModelName(params, getMegaModel(runtime)), "large", TEXT_MEGA_MODEL_TYPE ); } export async function handleResponseHandler( runtime: IAgentRuntime, params: GenerateTextParams ): Promise { return generateTextWithModel( runtime, params, resolveRequestedModelName(params, getResponseHandlerModel(runtime)), "small", RESPONSE_HANDLER_MODEL_TYPE ); } export async function handleActionPlanner( runtime: IAgentRuntime, params: GenerateTextParams ): Promise { return generateTextWithModel( runtime, params, resolveRequestedModelName(params, getActionPlannerModel(runtime)), "large", ACTION_PLANNER_MODEL_TYPE ); } const TEXT_REASONING_SMALL_MODEL_TYPE = ModelType.TEXT_REASONING_SMALL as ModelTypeName; const TEXT_REASONING_LARGE_MODEL_TYPE = ModelType.TEXT_REASONING_LARGE as ModelTypeName; export async function handleReasoningSmall( runtime: IAgentRuntime, params: GenerateTextParams ): Promise { return generateTextWithModel( runtime, params, resolveRequestedModelName(params, getReasoningSmallModel(runtime)), "small", TEXT_REASONING_SMALL_MODEL_TYPE ); } export async function handleReasoningLarge( runtime: IAgentRuntime, params: GenerateTextParams ): Promise { return generateTextWithModel( runtime, params, resolveRequestedModelName(params, getReasoningLargeModel(runtime)), "large", TEXT_REASONING_LARGE_MODEL_TYPE ); }