Anthropic: Messages API и content blocks
Messages API у Anthropic stateless, и это определяет весь адаптер. Claude возвращает tool_use content blocks; приложение сохраняет весь assistant message, выполняет tools и отправляет один user message с соответствующими tool_result blocks. Никакого server-side id продолжения - историю держите вы.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const model = process.env.ANTHROPIC_MODEL ?? "claude-sonnet-5";
type AnthropicState = {
messages: any[];
instructions: string;
tools: ToolDefinition[];
};
function toAnthropicTool(tool: ToolDefinition) {
return {
name: tool.name,
description: tool.description,
input_schema: tool.parameters,
strict: true
};
}Общий запрос дописывает assistant message к истории и нормализует content blocks в наш ProviderTurn.
async function request(state: AnthropicState): Promise<ProviderTurn> {
const message = await client.messages.create({
model,
max_tokens: 2_048,
system: state.instructions,
tools: state.tools.map(toAnthropicTool),
messages: state.messages
});
const nextState = {
...state,
messages: [
...state.messages,
{ role: "assistant", content: message.content }
]
};
const calls = message.content
.filter(function isToolUse(block: any) {
return block.type === "tool_use";
})
.map(function normalize(block: any): ToolCall {
return { id: block.id, name: block.name, arguments: block.input };
});
if (calls.length > 0) return { kind: "tool_calls", calls, state: nextState };
const text = message.content
.filter(function isText(block: any) { return block.type === "text"; })
.map(function takeText(block: any) { return block.text; })
.join("");
return { kind: "text", text, state: nextState };
}А возврат результатов складывает tool_result blocks в один user message.
async function resume(rawState: unknown, results: ToolResult[]) {
const state = anthropicStateSchema.parse(rawState);
const toolResults = results.map(function toToolResult(result) {
return {
type: "tool_result" as const,
tool_use_id: result.callId,
content: JSON.stringify({ ok: !result.isError, value: result.output }),
is_error: result.isError
};
});
return request({
...state,
messages: [...state.messages, { role: "user", content: toolResults }]
});
}Две детали здесь легко нарушить по инерции. Первая - про модель.
Claude Sonnet 5. Модель использует adaptive thinking по умолчанию. Не переносите старую настройку manual extended thinking и не задавайте non-defaulttemperature,top_pилиtop_k: актуальная migration guide сообщает, что Sonnet 5 отклоняет такие параметры.
Вторая - про форму ответа с результатами.
Parallel results одним сообщением. Если Claude вызвал несколько tools, верните по одномуtool_resultна каждыйtool_useв одном следующем user message. Не вставляйте текст перед result blocks.
Прямой Messages API - стабильная точка понимания. Но у Anthropic есть и удобный автоматический цикл - Tool Runner.
Как обращаться с temperature и manual thinking для Claude Sonnet 5?