Skip to content

Commit 6058833

Browse files
author
nix-auto-updates[bot]
committed
Merge remote-tracking branch 'upstream/v2' into nix-auto-updates
* upstream/v2: feat(core): publish pending MCP status during connect (anomalyco#37605) feat(codemode): native coercion parity in interpreter (anomalyco#37608) test(core): stabilize shell progress test (anomalyco#37643) fix(ai): parse compatible reasoning deltas (anomalyco#37558) fix(core): remove session import cycle (anomalyco#37596)
2 parents 8006629 + d625bc8 commit 6058833

23 files changed

Lines changed: 595 additions & 135 deletions

packages/ai/src/protocols/openai-chat.ts

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ const OpenAIChatMessage = Schema.Union([
7575
content: Schema.NullOr(Schema.String),
7676
tool_calls: optionalArray(OpenAIChatAssistantToolCall),
7777
reasoning_content: Schema.optional(Schema.String),
78+
reasoning: Schema.optional(Schema.String),
79+
reasoning_text: Schema.optional(Schema.String),
7880
}),
7981
Schema.Struct({ role: Schema.Literal("tool"), tool_call_id: Schema.String, content: Schema.String }),
8082
]).pipe(Schema.toTaggedUnion("role"))
@@ -145,6 +147,8 @@ type OpenAIChatToolCallDelta = Schema.Schema.Type<typeof OpenAIChatToolCallDelta
145147
const OpenAIChatDelta = Schema.Struct({
146148
content: optionalNull(Schema.String),
147149
reasoning_content: optionalNull(Schema.String),
150+
reasoning: optionalNull(Schema.String),
151+
reasoning_text: optionalNull(Schema.String),
148152
tool_calls: optionalNull(Schema.Array(OpenAIChatToolCallDelta)),
149153
})
150154

@@ -166,6 +170,7 @@ export interface ParserState {
166170
readonly usage?: Usage
167171
readonly finishReason?: FinishReason
168172
readonly lifecycle: Lifecycle.State
173+
readonly reasoningField?: "reasoning" | "reasoning_content" | "reasoning_text"
169174
}
170175

171176
// =============================================================================
@@ -208,6 +213,12 @@ const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part: MediaPart
208213
const openAICompatibleReasoningContent = (native: unknown) =>
209214
isRecord(native) && typeof native.reasoning_content === "string" ? native.reasoning_content : undefined
210215

216+
const reasoningField = (part: ReasoningPart) => {
217+
const field = part.providerMetadata?.openai?.reasoningField
218+
if (field === "reasoning" || field === "reasoning_content" || field === "reasoning_text") return field
219+
return "reasoning_content"
220+
}
221+
211222
const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* (message: OpenAIChatRequestMessage) {
212223
const content: Array<Schema.Schema.Type<typeof OpenAIChatUserContent>> = []
213224
for (const part of message.content) {
@@ -248,14 +259,20 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
248259
continue
249260
}
250261
}
262+
const text = reasoning.map((part) => part.text).join("")
263+
const field = reasoning[0] ? reasoningField(reasoning[0]) : "reasoning_content"
251264
return {
252265
role: "assistant" as const,
253266
content: content.length === 0 ? null : ProviderShared.joinText(content),
254267
tool_calls: toolCalls.length === 0 ? undefined : toolCalls,
255268
reasoning_content:
256-
reasoning.length > 0
257-
? reasoning.map((part) => part.text).join("")
258-
: openAICompatibleReasoningContent(message.native?.openaiCompatible),
269+
reasoning.length === 0
270+
? openAICompatibleReasoningContent(message.native?.openaiCompatible)
271+
: field === "reasoning_content"
272+
? text
273+
: undefined,
274+
reasoning: reasoning.length > 0 && field === "reasoning" ? text : undefined,
275+
reasoning_text: reasoning.length > 0 && field === "reasoning_text" ? text : undefined,
259276
}
260277
})
261278

@@ -400,6 +417,12 @@ const mapUsage = (usage: OpenAIChatEvent["usage"]): Usage | undefined => {
400417
})
401418
}
402419

420+
const reasoningDelta = (delta: Schema.Schema.Type<typeof OpenAIChatDelta> | null | undefined) => {
421+
if (delta?.reasoning_content) return { field: "reasoning_content", text: delta.reasoning_content } as const
422+
if (delta?.reasoning) return { field: "reasoning", text: delta.reasoning } as const
423+
if (delta?.reasoning_text) return { field: "reasoning_text", text: delta.reasoning_text } as const
424+
}
425+
403426
const step = (state: ParserState, event: OpenAIChatEvent) =>
404427
Effect.gen(function* () {
405428
const events: LLMEvent[] = []
@@ -412,8 +435,12 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
412435

413436
let lifecycle = state.lifecycle
414437

415-
if (delta?.reasoning_content)
416-
lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", delta.reasoning_content)
438+
const reasoning = reasoningDelta(delta)
439+
const reasoningField = state.reasoningField ?? reasoning?.field
440+
if (reasoning)
441+
lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", reasoning.text, {
442+
openai: { reasoningField: reasoningField ?? reasoning.field },
443+
})
417444

418445
if (delta?.content) {
419446
lifecycle = Lifecycle.reasoningEnd(lifecycle, events, "reasoning-0")
@@ -450,6 +477,7 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
450477
usage,
451478
finishReason,
452479
lifecycle,
480+
reasoningField,
453481
},
454482
events,
455483
] as const
@@ -482,7 +510,12 @@ export const protocol = Protocol.make({
482510
},
483511
stream: {
484512
event: Protocol.jsonEvent(OpenAIChatEvent),
485-
initial: () => ({ tools: ToolStream.empty<number>(), toolCallEvents: [], lifecycle: Lifecycle.initial() }),
513+
initial: () => ({
514+
tools: ToolStream.empty<number>(),
515+
toolCallEvents: [],
516+
lifecycle: Lifecycle.initial(),
517+
reasoningField: undefined,
518+
}),
486519
step,
487520
onHalt: finishEvents,
488521
},
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"version": 1,
3+
"metadata": {
4+
"model": "anthropic/claude-sonnet-4.6",
5+
"tags": [
6+
"prefix:openai-compatible-chat",
7+
"provider:openrouter",
8+
"protocol:openai-chat",
9+
"reasoning"
10+
],
11+
"name": "openrouter-reasoning",
12+
"recordedAt": "2026-07-18T11:28:39.267Z"
13+
},
14+
"interactions": [
15+
{
16+
"transport": "http",
17+
"request": {
18+
"method": "POST",
19+
"url": "https://openrouter.ai/api/v1/chat/completions",
20+
"headers": {
21+
"content-type": "application/json"
22+
},
23+
"body": "{\"model\":\"anthropic/claude-sonnet-4.6\",\"messages\":[{\"role\":\"system\",\"content\":\"Think through the arithmetic, then reply with only the final integer.\"},{\"role\":\"user\",\"content\":\"What is 173 multiplied by 219?\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":1536,\"temperature\":0,\"reasoning\":{\"max_tokens\":1024}}"
24+
},
25+
"response": {
26+
"status": 200,
27+
"headers": {
28+
"content-type": "text/event-stream"
29+
},
30+
"body": ": OPENROUTER PROCESSING\n\n: OPENROUTER PROCESSING\n\ndata: {\"id\":\"gen-1784374117-AXXPsQRoclZeQGx2uHeK\",\"object\":\"chat.completion.chunk\",\"created\":1784374117,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Anthropic\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\"173\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\"173\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\n: OPENROUTER PROCESSING\n\ndata: {\"id\":\"gen-1784374117-AXXPsQRoclZeQGx2uHeK\",\"object\":\"chat.completion.chunk\",\"created\":1784374117,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Anthropic\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\" × 219\\n\\n173 × 200 = 34,600\\n173 × 19 = 173 × 20 - 173 = 3,460 - 173 = 3,287\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\" × 219\\n\\n173 × 200 = 34,600\\n173 × 19 = 173 × 20 - 173 = 3,460 - 173 = 3,287\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1784374117-AXXPsQRoclZeQGx2uHeK\",\"object\":\"chat.completion.chunk\",\"created\":1784374117,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Anthropic\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":\"\\n\\n34,600 + 3,287 = 37,887\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\"\\n\\n34,600 + 3,287 = 37,887\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1784374117-AXXPsQRoclZeQGx2uHeK\",\"object\":\"chat.completion.chunk\",\"created\":1784374117,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Anthropic\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"signature\":\"EtgCCosBCA8YAipA0W4viH3kgBs43Cl5ewwVBPXTQElvzfbA2TLF4iSbKy9ZZDCSDjjAlF3Bs4ELEnP3vrrTuTioC6OB380lXQdyIDIRY2xhdWRlLXNvbm5ldC00LTY4AEIIdGhpbmtpbmdaJDRjMGYwNDZmLTI1ZmQtNDVmYi1iZmIzLWEwOGE4ZTI0OWNhNxIMMiUlJC3x/5p5PuTwGgwlc8eipZyoM94BHwMiMO45uQx/ymeOjbugi7RDVPZ4jZXSIiEbVi2CD7zPjAK5fFQoVGP1HD55v9CER823JCp6Dg5Xb7Lrk6NUd1XN2KTKrttK7mATE+IBrDTFmor/1cNeg+9gjIbxM/jn/6L5HPmh3/esEVu24Q0IGLZVoE7cTgGgxsrceKMD71Jp2XQgIWD8ltsPfWw3gSc4p+z18UuPN6LuR0mHHENTnClHrAPnOrxbDIl4ZwZgMX8YAQ==\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1784374117-AXXPsQRoclZeQGx2uHeK\",\"object\":\"chat.completion.chunk\",\"created\":1784374117,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Anthropic\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"37887\",\"role\":\"assistant\"},\"finish_reason\":null,\"native_finish_reason\":null}]}\n\ndata: {\"id\":\"gen-1784374117-AXXPsQRoclZeQGx2uHeK\",\"object\":\"chat.completion.chunk\",\"created\":1784374117,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Anthropic\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\",\"reasoning\":null},\"finish_reason\":\"stop\",\"native_finish_reason\":\"end_turn\"}]}\n\ndata: {\"id\":\"gen-1784374117-AXXPsQRoclZeQGx2uHeK\",\"object\":\"chat.completion.chunk\",\"created\":1784374117,\"model\":\"anthropic/claude-sonnet-4.6\",\"provider\":\"Anthropic\",\"service_tier\":\"default\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"\",\"role\":\"assistant\"},\"finish_reason\":\"stop\",\"native_finish_reason\":\"end_turn\"}],\"usage\":{\"prompt_tokens\":61,\"completion_tokens\":80,\"total_tokens\":141,\"cost\":0.001383,\"is_byok\":false,\"prompt_tokens_details\":{\"cached_tokens\":0,\"cache_write_tokens\":0,\"audio_tokens\":0,\"video_tokens\":0},\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"cost_details\":{\"upstream_inference_cost\":0.001383,\"upstream_inference_prompt_cost\":0.000183,\"upstream_inference_completions_cost\":0.0012},\"completion_tokens_details\":{\"reasoning_tokens\":29,\"image_tokens\":0,\"audio_tokens\":0}}}\n\ndata: [DONE]\n\n"
31+
}
32+
}
33+
]
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"version": 1,
3+
"metadata": {
4+
"model": "anthropic/claude-sonnet-4.6",
5+
"tags": [
6+
"prefix:openai-compatible-chat",
7+
"provider:vercel-ai-gateway",
8+
"protocol:openai-chat",
9+
"reasoning"
10+
],
11+
"name": "vercel-ai-gateway-reasoning",
12+
"recordedAt": "2026-07-18T11:28:42.077Z"
13+
},
14+
"interactions": [
15+
{
16+
"transport": "http",
17+
"request": {
18+
"method": "POST",
19+
"url": "https://ai-gateway.vercel.sh/v1/chat/completions",
20+
"headers": {
21+
"content-type": "application/json"
22+
},
23+
"body": "{\"model\":\"anthropic/claude-sonnet-4.6\",\"messages\":[{\"role\":\"system\",\"content\":\"Think through the arithmetic, then reply with only the final integer.\"},{\"role\":\"user\",\"content\":\"What is 173 multiplied by 219?\"}],\"stream\":true,\"stream_options\":{\"include_usage\":true},\"max_tokens\":1536,\"temperature\":0,\"reasoning\":{\"enabled\":true,\"max_tokens\":1024}}"
24+
},
25+
"response": {
26+
"status": 200,
27+
"headers": {
28+
"content-type": "text/event-stream"
29+
},
30+
"body": "data: {\"id\":\"gen_01KXTFRJXJ8CKP0W3DKC3WC004\",\"object\":\"chat.completion.chunk\",\"created\":1784374121,\"model\":\"anthropic/claude-sonnet-4.6\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\"},\"logprobs\":null,\"finish_reason\":null}],\"system_fingerprint\":\"fp_zfth1fcyet\"}\n\ndata: {\"id\":\"gen_01KXTFRJXJ8CKP0W3DKC3WC004\",\"object\":\"chat.completion.chunk\",\"created\":1784374121,\"model\":\"anthropic/claude-sonnet-4.6\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning\":\"173\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\"173\",\"signature\":\"\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"logprobs\":null,\"finish_reason\":null}],\"system_fingerprint\":\"fp_zfth1fcyet\"}\n\ndata: {\"id\":\"gen_01KXTFRJXJ8CKP0W3DKC3WC004\",\"object\":\"chat.completion.chunk\",\"created\":1784374121,\"model\":\"anthropic/claude-sonnet-4.6\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning\":\" × 219\\n\\n173 × 200 = 34,600\\n173 × 19 = 173 × 20 - 173 = 3,460\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\" × 219\\n\\n173 × 200 = 34,600\\n173 × 19 = 173 × 20 - 173 = 3,460\",\"signature\":\"\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"logprobs\":null,\"finish_reason\":null}],\"system_fingerprint\":\"fp_zfth1fcyet\"}\n\ndata: {\"id\":\"gen_01KXTFRJXJ8CKP0W3DKC3WC004\",\"object\":\"chat.completion.chunk\",\"created\":1784374121,\"model\":\"anthropic/claude-sonnet-4.6\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning\":\" - 173 = 3,287\\n\\n34,600 + 3,287 = 37,887\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\" - 173 = 3,287\\n\\n34,600 + 3,287 = 37,887\",\"signature\":\"\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"logprobs\":null,\"finish_reason\":null}],\"system_fingerprint\":\"fp_zfth1fcyet\"}\n\ndata: {\"id\":\"gen_01KXTFRJXJ8CKP0W3DKC3WC004\",\"object\":\"chat.completion.chunk\",\"created\":1784374121,\"model\":\"anthropic/claude-sonnet-4.6\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning\":\"\",\"reasoning_details\":[{\"type\":\"reasoning.text\",\"text\":\"\",\"signature\":\"EtgCCosBCA8YAipA0W4viH3kgBs43Cl5ewwVBPXTQElvzfbA2TLF4iSbKy9ZZDCSDjjAlF3Bs4ELEnP3vrrTuTioC6OB380lXQdyIDIRY2xhdWRlLXNvbm5ldC00LTY4AEIIdGhpbmtpbmdaJDNiOTNhNWRkLTczMDItNDgzZi1hZWFlLTM2MjA3NTU0OGFlMRIMKkQaBkOSYB8cgQNiGgwDinVmv/KYSAKQEsUiMPSSFWvVpNuyEyYC8HlxrEZsb5KEEETuMjAI2hcC3m/NwGR+PC7chh2JWwD7wyK+eyp6OoF973UNMHAsWsKykCSJv60aXeOiDomxdfR9CRWVtaroVTkhtL2FPgplBPZYr75XvS0l6If3fqCPKevNE5WaOsSaXNfnMCCKGX7A0Pkhs4NazmCnntWGOsW7J03bAQKzIZ+c+Yr0rSwmwvuiocDqzsSE8bOeVv4352EYAQ==\",\"format\":\"anthropic-claude-v1\",\"index\":0}]},\"logprobs\":null,\"finish_reason\":null}],\"system_fingerprint\":\"fp_zfth1fcyet\"}\n\ndata: {\"id\":\"gen_01KXTFRJXJ8CKP0W3DKC3WC004\",\"object\":\"chat.completion.chunk\",\"created\":1784374121,\"model\":\"anthropic/claude-sonnet-4.6\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"37,887\"},\"logprobs\":null,\"finish_reason\":null}],\"system_fingerprint\":\"fp_zfth1fcyet\"}\n\ndata: {\"id\":\"gen_01KXTFRJXJ8CKP0W3DKC3WC004\",\"object\":\"chat.completion.chunk\",\"created\":1784374121,\"model\":\"anthropic/claude-sonnet-4.6\",\"choices\":[{\"index\":0,\"delta\":{\"provider_metadata\":{\"anthropic\":{\"usage\":{\"input_tokens\":61,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":81,\"service_tier\":\"standard\",\"inference_geo\":\"global\",\"output_tokens_details\":{\"thinking_tokens\":73}},\"cacheCreationInputTokens\":0,\"stopSequence\":null,\"iterations\":null,\"container\":null,\"contextManagement\":null},\"gateway\":{\"routing\":{\"originalModelId\":\"anthropic/claude-sonnet-4.6\",\"resolvedProvider\":\"anthropic\",\"fallbacksAvailable\":[\"vertexAnthropic\",\"bedrock\"],\"planningReasoning\":\"System credentials planned for: anthropic, vertexAnthropic, bedrock. Total execution order: anthropic(system) → vertexAnthropic(system) → bedrock(system)\",\"canonicalSlug\":\"anthropic/claude-sonnet-4.6\",\"finalProvider\":\"anthropic\",\"modelAttemptCount\":1,\"modelAttempts\":[{\"canonicalSlug\":\"anthropic/claude-sonnet-4.6\",\"success\":true,\"providerAttemptCount\":1,\"providerAttempts\":[{\"provider\":\"anthropic\",\"credentialType\":\"system\",\"success\":true,\"startTime\":1784374119500,\"endTime\":1784374122066,\"providerRequestId\":\"req_011Cd9SjHykKUqTQ1S8cAG17\",\"statusCode\":200,\"providerResponseId\":\"msg_011Cd9SjLvMmTFY8Pyj8n336\"}]}],\"totalProviderAttemptCount\":1},\"cost\":\"0.001398\",\"marketCost\":\"0.001398\",\"surchargeCost\":\"0\",\"gatewayCost\":\"0.001398\",\"inferenceCost\":\"0.001398\",\"inputInferenceCost\":\"0.000183\",\"outputInferenceCost\":\"0.001215\",\"generationId\":\"gen_01KXTFRJXJ8CKP0W3DKC3WC004\"}}},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":61,\"completion_tokens\":81,\"total_tokens\":142,\"cost\":0.001398,\"is_byok\":false,\"prompt_tokens_details\":{\"cached_tokens\":0,\"audio_tokens\":0,\"video_tokens\":0},\"cost_details\":{\"upstream_inference_cost\":null,\"upstream_inference_prompt_cost\":0,\"upstream_inference_completions_cost\":0},\"completion_tokens_details\":{\"reasoning_tokens\":26,\"reasoning_tokens_estimated\":true,\"image_tokens\":0},\"cache_creation_input_tokens\":0,\"market_cost\":0.001398,\"gateway_cost\":0.001398},\"system_fingerprint\":\"fp_zfth1fcyet\",\"generationId\":\"gen_01KXTFRJXJ8CKP0W3DKC3WC004\"}\n\ndata: [DONE]\n\n"
31+
}
32+
}
33+
]
34+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { describe, expect } from "bun:test"
2+
import { Effect } from "effect"
3+
import { LLM, LLMEvent } from "../../src"
4+
import * as OpenAICompatible from "../../src/providers/openai-compatible"
5+
import * as OpenRouter from "../../src/providers/openrouter"
6+
import { LLMClient } from "../../src/route"
7+
import { recordedTests } from "../recorded-test"
8+
9+
const cases = [
10+
{
11+
name: "OpenRouter",
12+
model: OpenRouter.configure({
13+
apiKey: process.env.OPENROUTER_API_KEY ?? "fixture",
14+
providerOptions: { openrouter: { reasoning: { max_tokens: 1024 } } },
15+
}).model("anthropic/claude-sonnet-4.6"),
16+
requires: ["OPENROUTER_API_KEY"],
17+
cassette: "openrouter-reasoning",
18+
},
19+
{
20+
name: "Vercel AI Gateway",
21+
model: OpenAICompatible.configure({
22+
provider: "vercel-ai-gateway",
23+
baseURL: "https://ai-gateway.vercel.sh/v1",
24+
apiKey: process.env.AI_GATEWAY_API_KEY ?? "fixture",
25+
http: { body: { reasoning: { enabled: true, max_tokens: 1024 } } },
26+
}).model("anthropic/claude-sonnet-4.6"),
27+
requires: ["AI_GATEWAY_API_KEY"],
28+
cassette: "vercel-ai-gateway-reasoning",
29+
},
30+
] as const
31+
32+
for (const item of cases) {
33+
const recorded = recordedTests({
34+
prefix: "openai-compatible-chat",
35+
provider: item.model.provider,
36+
protocol: "openai-chat",
37+
requires: item.requires,
38+
tags: ["reasoning"],
39+
metadata: { model: item.model.id },
40+
})
41+
42+
describe(`${item.name} reasoning recorded`, () => {
43+
recorded.effect.with(
44+
"streams scalar reasoning",
45+
{ cassette: item.cassette },
46+
() =>
47+
Effect.gen(function* () {
48+
const response = yield* LLMClient.generate(
49+
LLM.request({
50+
model: item.model,
51+
system: "Think through the arithmetic, then reply with only the final integer.",
52+
prompt: "What is 173 multiplied by 219?",
53+
generation: { maxTokens: 1536, temperature: 0 },
54+
}),
55+
)
56+
57+
expect(response.text.replaceAll(",", "").trim()).toBe("37887")
58+
expect(response.reasoning.length).toBeGreaterThan(0)
59+
expect(response.events.some(LLMEvent.is.reasoningDelta)).toBe(true)
60+
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
61+
openai: { reasoningField: "reasoning" },
62+
})
63+
}),
64+
30_000,
65+
)
66+
})
67+
}

0 commit comments

Comments
 (0)