import type {
CanonicalContentBlock,
CanonicalMessage,
CanonicalModelRequest,
CanonicalToolResultContentBlock,
CanonicalToolChoice,
CanonicalToolSchema,
ModelDefinition,
} from "../../protocol/canonical.js";
export type AnthropicRequestBody = {
model: string;
max_tokens: number;
messages: AnthropicMessage[];
system?: string | unknown[];
tools?: AnthropicTool[];
tool_choice?: Record<string, unknown>;
temperature?: number;
thinking?: {
type: "enabled";
budget_tokens?: number;
};
stream?: boolean;
metadata?: Record<string, unknown>;
};
type AnthropicMessage = {
role: "user" | "assistant";
content: unknown[];
};
type AnthropicTool = {
name: string;
description?: string;
input_schema: Record<string, unknown>;
};
* Reserved tool name for Anthropic structured-output enforcement.
* Exported so `extractStructuredOutput` and tests can recognize it.
*/
export const ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME = "__output__";
export function buildAnthropicRequest(
request: CanonicalModelRequest,
model: ModelDefinition,
): AnthropicRequestBody {
const baseTools = request.tools?.map(toAnthropicTool) ?? [];
const outputTool = request.outputSchema
? toAnthropicStructuredOutputTool(request.outputSchema)
: null;
let toolChoice: Record<string, unknown> | undefined;
if (outputTool && request.outputSchema?.strict !== false) {
toolChoice = { type: "tool", name: ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME };
} else {
toolChoice = toAnthropicToolChoice(request.toolChoice);
}
const tools: AnthropicTool[] = outputTool ? [outputTool, ...baseTools] : baseTools;
const MAX_MESSAGE_BREAKPOINTS = 3;
const trimmedBreakpoints = request.cacheBreakpoints
? request.cacheBreakpoints.length > MAX_MESSAGE_BREAKPOINTS
? request.cacheBreakpoints.slice(-MAX_MESSAGE_BREAKPOINTS)
: request.cacheBreakpoints
: null;
const cacheBreakpoints = trimmedBreakpoints
? new Set(trimmedBreakpoints)
: null;
return {
model: request.model,
max_tokens: request.maxOutputTokens ?? model.capabilities.maxOutputTokens,
messages: request.messages.map((message, index) =>
toAnthropicMessage(message, cacheBreakpoints?.has(index) ?? false),
),
system: request.systemPrompt
? cacheBreakpoints
? [{ type: "text", text: request.systemPrompt, cache_control: { type: "ephemeral" } }]
: request.systemPrompt
: undefined,
tools: tools.length > 0 ? tools : undefined,
tool_choice: toolChoice,
temperature: request.temperature,
thinking:
request.thinking?.enabled && model.capabilities.supportsThinking
? { type: "enabled", budget_tokens: request.thinking.budgetTokens }
: undefined,
stream: request.stream,
metadata: toAnthropicMetadata(request.metadata),
};
}
function toAnthropicStructuredOutputTool(
schema: CanonicalModelRequest["outputSchema"] & object,
): AnthropicTool {
return {
name: ANTHROPIC_STRUCTURED_OUTPUT_TOOL_NAME,
description:
schema.description ??
`Return the final structured payload (schema name: ${schema.name}). Always call this tool exactly once.`,
input_schema: schema.schema,
};
}
function toAnthropicMessage(
message: CanonicalMessage,
markCacheBreakpoint: boolean,
): AnthropicMessage {
const content = message.content.map(toAnthropicContentBlock);
if (markCacheBreakpoint && content.length > 0) {
const last = content[content.length - 1];
if (last && typeof last === "object") {
content[content.length - 1] = {
...(last as Record<string, unknown>),
cache_control: { type: "ephemeral" },
};
}
}
return {
role: message.role,
content,
};
}
function toAnthropicContentBlock(block: CanonicalContentBlock): unknown {
switch (block.type) {
case "text":
return { type: "text", text: block.text };
case "thinking":
return { type: "thinking", thinking: block.text };
case "image":
return block.source === "url"
? { type: "image", source: { type: "url", url: block.data } }
: {
type: "image",
source: { type: "base64", media_type: block.mimeType, data: block.data },
};
case "pdf":
return {
type: "document",
source: { type: "base64", media_type: block.mimeType, data: block.data },
};
case "audio":
return block.source === "url"
? { type: "audio", source: { type: "url", url: block.data } }
: {
type: "audio",
source: { type: "base64", media_type: block.mimeType, data: block.data },
};
case "tool_call":
return { type: "tool_use", id: block.id, name: block.name, input: block.input };
case "tool_result":
return {
type: "tool_result",
tool_use_id: block.toolCallId,
content: block.content.map(toAnthropicToolResultContentBlock),
is_error: block.isError,
};
case "tool_result_reference":
return {
type: "tool_result",
tool_use_id: block.toolCallId,
content: [{
type: "text",
text: block.preview + (block.hasMore
? `\n\n[Truncated: original ${block.originalBytes} bytes, file: ${block.path}]`
: ""),
}],
is_error: false,
};
}
}
function toAnthropicToolResultContentBlock(block: CanonicalToolResultContentBlock): unknown {
switch (block.type) {
case "text":
return { type: "text", text: block.text };
case "image":
return block.source === "url"
? { type: "image", source: { type: "url", url: block.data } }
: {
type: "image",
source: { type: "base64", media_type: block.mimeType, data: block.data },
};
case "pdf":
return {
type: "document",
source: { type: "base64", media_type: block.mimeType, data: block.data },
};
}
}
function toAnthropicTool(tool: CanonicalToolSchema): AnthropicTool {
return {
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema,
};
}
function toAnthropicMetadata(metadata: Record<string, unknown> | undefined): Record<string, string> | undefined {
if (!metadata || typeof metadata.user_id !== "string" || metadata.user_id.length === 0) {
return undefined;
}
return { user_id: metadata.user_id };
}
function toAnthropicToolChoice(toolChoice: CanonicalToolChoice | undefined): Record<string, unknown> | undefined {
if (!toolChoice) {
return undefined;
}
if (toolChoice === "auto") {
return { type: "auto" };
}
if (toolChoice === "none") {
return { type: "none" };
}
if (toolChoice === "required") {
return { type: "any" };
}
return { type: "tool", name: toolChoice.name };
}