Skip to content

Commit 6618e2b

Browse files
authored
feat(native-llm): route Anthropic API-key models through native runtime (anomalyco#28271)
1 parent cb15b3a commit 6618e2b

7 files changed

Lines changed: 349 additions & 291 deletions

File tree

packages/opencode/src/session/llm/native-runtime.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,16 @@ type StreamInput = {
3737
}
3838

3939
export function status(input: Pick<StreamInput, "model" | "provider" | "auth">): RuntimeStatus {
40-
if (input.model.providerID !== "openai" && !input.model.providerID.startsWith("opencode"))
41-
return { type: "unsupported", reason: "provider is not openai or opencode" }
42-
if (input.model.api.npm !== "@ai-sdk/openai") return { type: "unsupported", reason: "provider package is not OpenAI" }
40+
const providerID = input.model.providerID
41+
if (providerID !== "openai" && providerID !== "anthropic" && !providerID.startsWith("opencode"))
42+
return { type: "unsupported", reason: "provider is not openai, opencode, or anthropic" }
43+
const npm = input.model.api.npm
44+
if (npm !== "@ai-sdk/openai" && npm !== "@ai-sdk/anthropic")
45+
return { type: "unsupported", reason: "provider package is not OpenAI or Anthropic" }
4346
if (input.auth?.type === "oauth") return { type: "unsupported", reason: "OAuth auth is not supported" }
4447

4548
const apiKey = typeof input.provider.options.apiKey === "string" ? input.provider.options.apiKey : input.provider.key
46-
if (!apiKey) return { type: "unsupported", reason: "OpenAI API key is not configured" }
49+
if (!apiKey) return { type: "unsupported", reason: "API key is not configured" }
4750

4851
return {
4952
type: "supported",
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"version": 1,
3+
"metadata": {
4+
"name": "session/native-anthropic-tool-loop",
5+
"recordedAt": "2026-05-19T01:40:12.788Z",
6+
"provider": "anthropic",
7+
"protocol": "anthropic-messages",
8+
"route": "anthropic-messages",
9+
"tags": [
10+
"opencode",
11+
"native",
12+
"tool-loop"
13+
]
14+
},
15+
"interactions": [
16+
{
17+
"transport": "http",
18+
"request": {
19+
"method": "POST",
20+
"url": "https://api.anthropic.com/v1/messages",
21+
"headers": {
22+
"content-type": "application/json"
23+
},
24+
"body": "{\"model\":\"claude-haiku-4-5-20251001\",\"system\":[{\"type\":\"text\",\"text\":\"Answer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\",\"cache_control\":{\"type\":\"ephemeral\"}}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is the weather in Paris?\",\"cache_control\":{\"type\":\"ephemeral\"}}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"input_schema\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"cache_control\":{\"type\":\"ephemeral\"}}],\"stream\":true,\"max_tokens\":32000,\"temperature\":0}"
25+
},
26+
"response": {
27+
"status": 200,
28+
"headers": {
29+
"content-type": "text/event-stream; charset=utf-8"
30+
},
31+
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_01KSRzhxWxF38x5yYVYvktbc\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":622,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":54,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_01A8pEqifk2HVQfq1ZDNP6iY\",\"name\":\"get_weather\",\"input\":{},\"caller\":{\"type\":\"direct\"}} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"city\\\": \\\"P\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"aris\\\"}\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":622,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":54} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n"
32+
}
33+
},
34+
{
35+
"transport": "http",
36+
"request": {
37+
"method": "POST",
38+
"url": "https://api.anthropic.com/v1/messages",
39+
"headers": {
40+
"content-type": "application/json"
41+
},
42+
"body": "{\"model\":\"claude-haiku-4-5-20251001\",\"system\":[{\"type\":\"text\",\"text\":\"Answer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\",\"cache_control\":{\"type\":\"ephemeral\"}}],\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"What is the weather in Paris?\",\"cache_control\":{\"type\":\"ephemeral\"}}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"toolu_01A8pEqifk2HVQfq1ZDNP6iY\",\"name\":\"get_weather\",\"input\":{\"city\":{}}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"toolu_01A8pEqifk2HVQfq1ZDNP6iY\",\"content\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}]}],\"tools\":[{\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"input_schema\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"cache_control\":{\"type\":\"ephemeral\"}}],\"stream\":true,\"max_tokens\":32000,\"temperature\":0}"
43+
},
44+
"response": {
45+
"status": 200,
46+
"headers": {
47+
"content-type": "text/event-stream; charset=utf-8"
48+
},
49+
"body": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_01UyghbuSVecMVozDny14vCD\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":697,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}} }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Paris\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" is sunny.\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":697,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":7} }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n"
50+
}
51+
}
52+
]
53+
}

packages/opencode/test/fixtures/recordings/session/native-openai-tool-call.json

Lines changed: 0 additions & 31 deletions
This file was deleted.

0 commit comments

Comments
 (0)