import type Anthropic from "@anthropic-ai/sdk";
import { t, toJSONSchema } from "structural";
import type {
Compiler,
CompilerImplementationParams,
CompilerIR,
CompilerModalities,
} from "./compiler-interface.ts";
import { compilerUsage, defineCompiler } from "./compiler-interface.ts";
import { parseToolCall } from "./parse-tool-call.ts";
import type {
Agent,
AnthropicAssistantData,
AssistantMessage,
Content as IRContent,
MalformedToolRequest,
} from "../llm-ir.ts";
import type { LoadedTools, ToolCall } from "../tool-def.ts";
import { errorToString, err } from "../result.ts";
import * as irPrompts from "./ir-prompts.ts";
type ToolCallRequest> = ToolCall;
type LoadedTool> = LoadedTools[keyof LoadedTools<
A["tools"]
>];
export type AnthropicCompilerModel = {
client: Anthropic;
model: string;
maxTokens: number;
modalities?: CompilerModalities;
thinking?: Anthropic.ThinkingConfigParam;
outputConfig?: Anthropic.OutputConfig;
};
const ThinkingBlockSchema = t.subtype({
type: t.value("thinking"),
thinking: t.str,
signature: t.str,
});
function imagePlaceholderContent(): string {
return irPrompts.imageAttachmentPlaceholderText();
}
function anthropicContentParts(
content: IRContent["content"],
modalities?: CompilerModalities,
): Array {
const output: Array = [];
for (const part of content) {
if (part.type === "text") {
output.push({ type: "text", text: part.content });
continue;
}
if (modalities?.includes("vision")) {
output.push({
type: "image",
source: {
type: "base64",
media_type: part.image.mimeType,
data: part.image.base64Data,
},
});
} else {
output.push({ type: "text", text: imagePlaceholderContent() });
}
}
return output;
}
function toModelMessage>(
messages: Array>,
modalities?: CompilerModalities,
): Array {
const output: Anthropic.MessageParam[] = [];
for (const ir of messages) {
output.push(modelMessageFromIr(ir, modalities));
}
return output;
}
function modelMessageFromIr>(
ir: CompilerIR,
modalities?: CompilerModalities,
): Anthropic.MessageParam {
if (ir.role === "assistant") {
let thinkingBlocks = ir.anthropic?.thinkingBlocks || [];
const toolCalls = ir.toolCalls || [];
return {
role: "assistant",
content: [
...thinkingBlocks,
{ type: "text", text: ir.content || " " },
...toolCalls
.filter((t: any) => t.type === "tool-call")
.map((t: any) => {
return {
type: "tool_use" as const,
id: t.toolCallId,
name: t.name,
input: t.original || {},
};
}),
],
};
}
if (ir.role === "user") {
return {
role: "user",
content: anthropicContentParts(ir.content, modalities),
};
}
if (ir.role === "tool-output") {
return {
role: "user",
content: [
{
type: "tool_result",
tool_use_id: ir.toolCall.toolCallId,
content: anthropicContentParts(ir.content, modalities),
},
],
};
}
if (ir.role === "tool-skip-output") {
return {
role: "user",
content: [
{
type: "tool_result",
tool_use_id: ir.toolCall.toolCallId,
is_error: true,
content: irPrompts.toolSkip(ir.reason),
},
],
};
}
if (ir.role === "tool-runtime-error") {
return {
role: "user",
content: [
{
type: "tool_result",
tool_use_id: ir.toolCall.toolCallId,
is_error: true,
content: `Error: ${ir.error}`,
},
],
};
}
if (ir.role === "tool-parse-error") {
return {
role: "user",
content: [
{
type: "tool_result",
tool_use_id: ir.malformedRequest.toolCallId,
is_error: true,
content: `Error: ${ir.malformedRequest.error}`,
},
],
};
}
if (ir.role === "tool-validation-error") {
return {
role: "user",
content: [
{
type: "tool_result",
tool_use_id: ir.toolCall.toolCallId,
is_error: true,
content: `Error: ${ir.error}`,
},
],
};
}
if (ir.role === "lowered-checkpoint") {
return {
role: "user",
content: anthropicContentParts(ir.content, modalities),
};
}
const _: never = ir;
throw new Error(`Unsupported IR role: ${(ir as any).role}`);
}
function generateCurlFrom(params: {
baseURL: string;
model: string;
system: string;
messages: Array;
tools?: Array<{ description: string; input_schema: any; name: string }>;
maxTokens: number;
thinking?: Anthropic.ThinkingConfigParam;
outputConfig?: Anthropic.OutputConfig;
}): string {
const { baseURL, model, system, messages, tools, maxTokens, thinking, outputConfig } = params;
const requestBody = {
model,
system,
messages,
tools,
tool_choice: {
type: "auto",
disable_parallel_tool_use: false,
},
max_tokens: maxTokens,
...(thinking ? { thinking } : {}),
...(outputConfig ? { output_config: outputConfig } : {}),
stream: true,
};
// Curl requests need an API Version
// Currently hardcoded in Anthropic SDK
const ANTHROPIC_API_VERSION = "2023-06-01";
return `curl -X POST "${baseURL}/v1/messages" \\
-H "Content-Type: application/json" \\
-H "x-api-key: [REDACTED_API_KEY]" \\
-H "anthropic-version: ${ANTHROPIC_API_VERSION}" \\
-d @- <<'JSON'
${JSON.stringify(requestBody)}
JSON`;
}
export const runAnthropicAgent: Compiler = defineCompiler(
async >(
params: CompilerImplementationParams,
) => {
const { model, irs, abortSignal, transport, systemPrompt, autofixJson } = params;
const messages = toModelMessage(irs, model.modalities);
const sysPrompt = systemPrompt ? await systemPrompt() : "";
const toolDefs = params.tools || {};
const toolDefinitions: Array<{ description: string; input_schema: any; name: string }> = [];
const toolEntries = Object.entries(toolDefs) as Array<[string, LoadedTool]>;
toolEntries.forEach(([name, toolDef]) => {
const argJsonSchema = toJSONSchema("ignore", toolDef.ArgumentsSchema);
// Delete JSON schema fields unused by AI SDK
// @ts-ignore
delete argJsonSchema.$schema;
delete argJsonSchema.description;
// @ts-ignore
delete argJsonSchema.title;
toolDefinitions.push({
name,
description: toolDef.description,
input_schema: argJsonSchema,
});
});
const toolParams =
toolDefinitions.length === 0
? {}
: {
tools: toolDefinitions,
};
const modelOptions = {
...(model.thinking ? { thinking: model.thinking } : {}),
...(model.outputConfig ? { output_config: model.outputConfig } : {}),
};
const curl = generateCurlFrom({
baseURL: model.client.baseURL,
model: model.model,
system: sysPrompt,
messages,
...toolParams,
maxTokens: model.maxTokens,
thinking: model.thinking,
outputConfig: model.outputConfig,
});
try {
const system = sysPrompt == null ? {} : { system: sysPrompt };
const { data: stream, response } = await model.client.messages
.create({
...system,
model: model.model,
messages,
...toolParams,
tool_choice: {
type: "auto",
disable_parallel_tool_use: false,
},
max_tokens: model.maxTokens,
...modelOptions,
stream: true,
})
.withResponse();
let content = "";
let reasoningContent: string | undefined = undefined;
let usage = {
input: 0,
cachedInput: 0,
output: 0,
};
const thinkingBlocks: Array<
| {
type: "thinking";
thinking?: string;
signature?: string;
index: number;
}
| {
type: "redacted_thinking";
data: string;
}
> = [];
let inProgressTools = new Map<
number,
{
id: string;
index: number;
name: string;
partialJson: string;
}
>();
try {
for await (const chunk of stream) {
if (abortSignal.aborted) break;
switch (chunk.type) {
case "content_block_delta":
switch (chunk.delta.type) {
case "text_delta":
content += chunk.delta.text;
params.onTokens(chunk.delta.text, "content");
break;
case "thinking_delta":
if (reasoningContent == null) reasoningContent = "";
reasoningContent += chunk.delta.thinking;
params.onTokens(chunk.delta.thinking, "reasoning");
if (thinkingBlocks.length === 0) {
thinkingBlocks.push({
type: "thinking",
thinking: chunk.delta.thinking,
index: chunk.index,
});
} else {
const lastBlock = thinkingBlocks[thinkingBlocks.length - 1];
if (lastBlock.type === "thinking" && lastBlock.index === chunk.index) {
lastBlock.thinking += chunk.delta.thinking;
} else {
thinkingBlocks.push({
type: "thinking",
thinking: chunk.delta.thinking,
index: chunk.index,
});
}
}
break;
case "signature_delta":
if (thinkingBlocks.length === 0) {
thinkingBlocks.push({
type: "thinking",
signature: chunk.delta.signature,
index: chunk.index,
});
} else {
const lastBlock = thinkingBlocks[thinkingBlocks.length - 1];
if (lastBlock.type === "thinking" && lastBlock.index === chunk.index) {
lastBlock.signature = chunk.delta.signature;
} else {
thinkingBlocks.push({
type: "thinking",
signature: chunk.delta.signature,
index: chunk.index,
});
}
}
break;
case "input_json_delta":
{
const tool = inProgressTools.get(chunk.index);
if (tool != null) {
params.onTokens(chunk.delta.partial_json, "tool");
tool.partialJson += chunk.delta.partial_json;
}
}
break;
}
break;
case "content_block_start":
switch (chunk.content_block.type) {
case "tool_use":
params.onTokens(chunk.content_block.name, "tool");
inProgressTools.set(chunk.index, {
id: chunk.content_block.id,
index: chunk.index,
name: chunk.content_block.name,
partialJson: "",
});
break;
case "redacted_thinking":
thinkingBlocks.push({
type: "redacted_thinking",
data: chunk.content_block.data,
});
break;
}
break;
case "message_delta":
usage.output = chunk.usage.output_tokens;
if (chunk.usage.input_tokens && chunk.usage.input_tokens > 0) {
usage.input = chunk.usage.input_tokens;
}
usage.cachedInput = chunk.usage.cache_read_input_tokens ?? usage.cachedInput;
break;
case "message_start":
usage.input = chunk.message.usage.input_tokens;
usage.cachedInput = chunk.message.usage.cache_read_input_tokens ?? 0;
break;
}
}
} catch (e) {
if (!abortSignal.aborted) {
return err({
type: "stream-error",
requestError: errorToString(e),
curl,
usage: compilerUsage(usage.input, usage.output, usage.cachedInput),
headers: response.headers,
});
}
}
const compilerTokens = compilerUsage(usage.input, usage.output, usage.cachedInput);
let anthropic: { anthropic?: AnthropicAssistantData } = {};
if (thinkingBlocks.length > 0) {
anthropic.anthropic = {
thinkingBlocks: thinkingBlocks.map(b => {
if (b.type === "redacted_thinking") return b;
return ThinkingBlockSchema.slice({
type: "thinking",
signature: b.signature || "",
thinking: b.thinking || "",
});
}),
};
}
const assistantMessage: AssistantMessage = {
role: "assistant",
content,
reasoningContent,
...anthropic,
usage: compilerTokens,
};
return params.finish({
curl,
headers: response.headers,
usage: compilerTokens,
abortedOutput: assistantMessage,
parsedOutput: async () => {
if (inProgressTools.size === 0) return assistantMessage;
// Sort tool calls by their content block index to preserve ordering
const sortedTools = Array.from(inProgressTools.entries())
.sort(([a], [b]) => a - b)
.map(([_, v]) => v);
const toolCalls: Array | MalformedToolRequest> = [];
for (const inProgressTool of sortedTools) {
const chatToolCall = {
toolCallId: inProgressTool.id,
toolName: inProgressTool.name,
args: inProgressTool.partialJson,
};
const parseResult = await parseToolCall({
toolCall: chatToolCall,
toolDefs,
autofixJson,
abortSignal,
transport,
});
if (parseResult.status === "error") {
toolCalls.push({
type: "malformed-tool-request",
error: parseResult.message,
toolCallId: inProgressTool.id,
call: {
original: {
name: inProgressTool.name,
arguments: inProgressTool.partialJson,
},
},
});
continue;
}
toolCalls.push(parseResult.tool);
}
if (toolCalls.length > 0) assistantMessage.toolCalls = toolCalls;
return assistantMessage;
},
});
} catch (e) {
return err({
type: "request-error",
requestError: errorToString(e),
curl,
});
}
},
);