@roj-ai/sdk 0.1.35 → 0.1.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/agents/agent.d.ts.map +1 -1
- package/dist/core/agents/agent.js +7 -3
- package/dist/core/agents/agent.js.map +1 -1
- package/dist/core/agents/state.d.ts +24 -0
- package/dist/core/agents/state.d.ts.map +1 -1
- package/dist/core/agents/state.js.map +1 -1
- package/dist/core/context/state.d.ts.map +1 -1
- package/dist/core/context/state.js +4 -0
- package/dist/core/context/state.js.map +1 -1
- package/dist/core/llm/anthropic.d.ts.map +1 -1
- package/dist/core/llm/anthropic.js +49 -8
- package/dist/core/llm/anthropic.js.map +1 -1
- package/dist/core/llm/model-shortlist-live.test.d.ts +35 -0
- package/dist/core/llm/model-shortlist-live.test.d.ts.map +1 -0
- package/dist/core/llm/model-shortlist-live.test.js +452 -0
- package/dist/core/llm/model-shortlist-live.test.js.map +1 -0
- package/dist/core/llm/openrouter-mapping.test.js +18 -0
- package/dist/core/llm/openrouter-mapping.test.js.map +1 -1
- package/dist/core/llm/openrouter.d.ts.map +1 -1
- package/dist/core/llm/openrouter.js +20 -11
- package/dist/core/llm/openrouter.js.map +1 -1
- package/dist/core/llm/provider.d.ts +34 -3
- package/dist/core/llm/provider.d.ts.map +1 -1
- package/dist/core/llm/provider.js.map +1 -1
- package/dist/core/llm/reasoning-details.test.d.ts +2 -0
- package/dist/core/llm/reasoning-details.test.d.ts.map +1 -0
- package/dist/core/llm/reasoning-details.test.js +144 -0
- package/dist/core/llm/reasoning-details.test.js.map +1 -0
- package/dist/core/llm/state.d.ts +2 -0
- package/dist/core/llm/state.d.ts.map +1 -1
- package/dist/core/llm/state.js +11 -0
- package/dist/core/llm/state.js.map +1 -1
- package/dist/core/llm/thinking-blocks.test.d.ts +2 -0
- package/dist/core/llm/thinking-blocks.test.d.ts.map +1 -0
- package/dist/core/llm/thinking-blocks.test.js +291 -0
- package/dist/core/llm/thinking-blocks.test.js.map +1 -0
- package/dist/core/reasoning-round-trip.integration.test.d.ts +2 -0
- package/dist/core/reasoning-round-trip.integration.test.d.ts.map +1 -0
- package/dist/core/reasoning-round-trip.integration.test.js +150 -0
- package/dist/core/reasoning-round-trip.integration.test.js.map +1 -0
- package/dist/core/sessions/state.d.ts.map +1 -1
- package/dist/core/sessions/state.js +5 -0
- package/dist/core/sessions/state.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/core/agents/agent.ts +9 -3
- package/src/core/agents/state.ts +26 -0
- package/src/core/context/state.ts +4 -0
- package/src/core/llm/anthropic.ts +71 -10
- package/src/core/llm/model-shortlist-live.test.ts +577 -0
- package/src/core/llm/openrouter-mapping.test.ts +24 -0
- package/src/core/llm/openrouter.ts +28 -12
- package/src/core/llm/provider.ts +44 -3
- package/src/core/llm/reasoning-details.test.ts +177 -0
- package/src/core/llm/state.ts +12 -0
- package/src/core/llm/thinking-blocks.test.ts +345 -0
- package/src/core/reasoning-round-trip.integration.test.ts +188 -0
- package/src/core/sessions/state.ts +5 -0
- package/src/index.ts +1 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ToolCall } from '~/core/agents/state.js'
|
|
1
|
+
import type { ReasoningDetails, ToolCall } from '~/core/agents/state.js'
|
|
2
2
|
import type { ImageProcessor } from '~/core/image/types.js'
|
|
3
3
|
import type { ChatMessageContentItem } from '~/core/llm/llm-log-types.js'
|
|
4
4
|
import { ToolCallId } from '~/core/tools/schema.js'
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
LLMMetrics,
|
|
15
15
|
LLMProvider,
|
|
16
16
|
OpenRouterProviderRouting,
|
|
17
|
+
OpenRouterReasoningOptions,
|
|
17
18
|
ProviderHttpRequest,
|
|
18
19
|
RawInferenceRequest,
|
|
19
20
|
} from './provider.js'
|
|
@@ -52,6 +53,9 @@ interface OpenRouterChoice {
|
|
|
52
53
|
message: {
|
|
53
54
|
content: string | null | Array<{ type: string; text?: string }>
|
|
54
55
|
tool_calls?: OpenRouterToolCall[]
|
|
56
|
+
/** Plaintext chain of thought, when the model exposes one. Display only — the round trip goes through reasoning_details. */
|
|
57
|
+
reasoning?: string
|
|
58
|
+
reasoning_details?: ReasoningDetails
|
|
55
59
|
}
|
|
56
60
|
finish_reason: string | null
|
|
57
61
|
}
|
|
@@ -107,6 +111,7 @@ interface OpenRouterMessage {
|
|
|
107
111
|
content: string | OpenRouterContentItem[]
|
|
108
112
|
tool_calls?: OpenRouterToolCall[]
|
|
109
113
|
tool_call_id?: string
|
|
114
|
+
reasoning_details?: ReasoningDetails
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
/**
|
|
@@ -160,6 +165,8 @@ interface OpenRouterRequestBody {
|
|
|
160
165
|
stream_options: { include_usage: true }
|
|
161
166
|
/** Provider routing preferences */
|
|
162
167
|
provider?: OpenRouterProviderRouting
|
|
168
|
+
/** Reasoning budget */
|
|
169
|
+
reasoning?: OpenRouterReasoningOptions
|
|
163
170
|
/** Routing strategy */
|
|
164
171
|
route?: 'fallback'
|
|
165
172
|
/** Model transforms */
|
|
@@ -273,6 +280,9 @@ export class OpenRouterProvider implements LLMProvider {
|
|
|
273
280
|
finishReason: this.mapFinishReason(choice.finish_reason),
|
|
274
281
|
metrics,
|
|
275
282
|
providerRequestId: data.id,
|
|
283
|
+
reasoning: choice.message.reasoning || undefined,
|
|
284
|
+
// Normalized to undefined here so an empty array never reaches the wire on the way back.
|
|
285
|
+
reasoningDetails: choice.message.reasoning_details?.length ? choice.message.reasoning_details : undefined,
|
|
276
286
|
})
|
|
277
287
|
} catch (error) {
|
|
278
288
|
return Err(this.mapError(error))
|
|
@@ -317,6 +327,9 @@ export class OpenRouterProvider implements LLMProvider {
|
|
|
317
327
|
if (orOpts.providers) {
|
|
318
328
|
body.provider = orOpts.providers
|
|
319
329
|
}
|
|
330
|
+
if (orOpts.reasoning) {
|
|
331
|
+
body.reasoning = orOpts.reasoning
|
|
332
|
+
}
|
|
320
333
|
if (orOpts.route) {
|
|
321
334
|
body.route = orOpts.route
|
|
322
335
|
}
|
|
@@ -366,19 +379,22 @@ export class OpenRouterProvider implements LLMProvider {
|
|
|
366
379
|
tool_call_id: msg.toolCallId,
|
|
367
380
|
}
|
|
368
381
|
}
|
|
369
|
-
case 'assistant':
|
|
382
|
+
case 'assistant': {
|
|
383
|
+
const mapped: OpenRouterMessage = { role: 'assistant', content: msg.content }
|
|
370
384
|
if (msg.toolCalls?.length) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
type: 'function',
|
|
377
|
-
function: { name: tc.name, arguments: JSON.stringify(tc.input) },
|
|
378
|
-
})),
|
|
379
|
-
}
|
|
385
|
+
mapped.tool_calls = msg.toolCalls.map((tc): OpenRouterToolCall => ({
|
|
386
|
+
id: tc.id,
|
|
387
|
+
type: 'function',
|
|
388
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.input) },
|
|
389
|
+
}))
|
|
380
390
|
}
|
|
381
|
-
|
|
391
|
+
// Echoed verbatim and only when present: the provider rejects an edited or reordered
|
|
392
|
+
// sequence, and adding the key for a non-reasoning model would rewrite the cached prefix.
|
|
393
|
+
if (msg.reasoningDetails?.length) {
|
|
394
|
+
mapped.reasoning_details = msg.reasoningDetails
|
|
395
|
+
}
|
|
396
|
+
return mapped
|
|
397
|
+
}
|
|
382
398
|
case 'system':
|
|
383
399
|
return { role: 'system', content: msg.content }
|
|
384
400
|
case 'user':
|
package/src/core/llm/provider.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AssistantLLMMessage,
|
|
3
|
+
LLMMessage,
|
|
4
|
+
ReasoningDetails,
|
|
5
|
+
SystemLLMMessage,
|
|
6
|
+
ThinkingBlocks,
|
|
7
|
+
ToolCall,
|
|
8
|
+
ToolLLMMessage,
|
|
9
|
+
UserLLMMessage,
|
|
10
|
+
} from '~/core/agents/state.js'
|
|
2
11
|
import type { FileStore } from '~/core/file-store/types.js'
|
|
3
12
|
import type { ToolResultContent } from '~/core/llm/llm-log-types.js'
|
|
4
13
|
import type { ToolDefinition } from '~/core/tools/definition.js'
|
|
@@ -7,7 +16,7 @@ import type { Result } from '~/lib/utils/result.js'
|
|
|
7
16
|
import { ModelId } from './schema.js'
|
|
8
17
|
|
|
9
18
|
// Re-export LLMMessage types from agents/state for backwards compatibility
|
|
10
|
-
export type { AssistantLLMMessage, LLMMessage, SystemLLMMessage, ToolCall, ToolLLMMessage, UserLLMMessage }
|
|
19
|
+
export type { AssistantLLMMessage, LLMMessage, ReasoningDetails, SystemLLMMessage, ThinkingBlocks, ToolCall, ToolLLMMessage, UserLLMMessage }
|
|
11
20
|
|
|
12
21
|
// ============================================================================
|
|
13
22
|
// Request types
|
|
@@ -41,9 +50,37 @@ export interface OpenRouterProviderRouting {
|
|
|
41
50
|
quantizations?: string[]
|
|
42
51
|
}
|
|
43
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Reasoning controls.
|
|
55
|
+
*
|
|
56
|
+
* Without these the model's own default applies, and several of the defaults are expensive:
|
|
57
|
+
* `high` on Claude Sonnet 5, Opus 5 and Grok 4.5, `max` on Kimi K3. Reasoning is also
|
|
58
|
+
* `mandatory` on Gemini 3.6 Flash, Grok 4.5 and Kimi K2.7 Code — those cannot be turned off,
|
|
59
|
+
* only turned down.
|
|
60
|
+
*
|
|
61
|
+
* **Which efforts a model accepts differs per model.** The catalog's
|
|
62
|
+
* `reasoning.supported_efforts` is the authority: Kimi K3 has no `medium`, `minimal` exists
|
|
63
|
+
* only on Gemini, `xhigh`/`none` only on the OpenAI and newer Anthropic models. The union here
|
|
64
|
+
* is every value seen across them, not a set any single model takes.
|
|
65
|
+
*
|
|
66
|
+
* @see https://openrouter.ai/docs/use-cases/reasoning-tokens
|
|
67
|
+
*/
|
|
68
|
+
export interface OpenRouterReasoningOptions {
|
|
69
|
+
/** Relative budget. Mutually exclusive with `max_tokens` — set one. */
|
|
70
|
+
effort?: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max'
|
|
71
|
+
/** Absolute token budget, for models that take one instead of an effort. */
|
|
72
|
+
max_tokens?: number
|
|
73
|
+
/** Explicitly enable reasoning at the model's default effort. */
|
|
74
|
+
enabled?: boolean
|
|
75
|
+
/** Reason, but omit the blocks from the response. Note this breaks the round trip. */
|
|
76
|
+
exclude?: boolean
|
|
77
|
+
}
|
|
78
|
+
|
|
44
79
|
export interface OpenRouterRequestOptions {
|
|
45
80
|
/** Provider routing preferences */
|
|
46
81
|
providers?: OpenRouterProviderRouting
|
|
82
|
+
/** Reasoning budget. Omitted: the model's own default, which is often `high`. */
|
|
83
|
+
reasoning?: OpenRouterReasoningOptions
|
|
47
84
|
/** Routing strategy */
|
|
48
85
|
route?: 'fallback'
|
|
49
86
|
/** Model transforms */
|
|
@@ -141,8 +178,12 @@ export interface InferenceResponse {
|
|
|
141
178
|
metrics: LLMMetrics
|
|
142
179
|
/** Provider-specific request ID (e.g., OpenRouter generation ID for fetching stats) */
|
|
143
180
|
providerRequestId?: string
|
|
144
|
-
/** Extended thinking / reasoning content (for models that support it) */
|
|
181
|
+
/** Extended thinking / reasoning content (for models that support it) — human-readable, for logs and the debug UI. */
|
|
145
182
|
reasoning?: string
|
|
183
|
+
/** Opaque reasoning blocks to echo back on the next request. Absent for models that do not reason. */
|
|
184
|
+
reasoningDetails?: ReasoningDetails
|
|
185
|
+
/** Anthropic thinking blocks to replay on the next request. Absent unless extended thinking is enabled. */
|
|
186
|
+
thinkingBlocks?: ThinkingBlocks
|
|
146
187
|
}
|
|
147
188
|
|
|
148
189
|
/**
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { LLMMessage } from '~/core/agents/state.js'
|
|
3
|
+
import { isOk } from '~/lib/utils/result.js'
|
|
4
|
+
import { ToolCallId } from '~/core/tools/schema.js'
|
|
5
|
+
import { OpenRouterProvider } from './openrouter.js'
|
|
6
|
+
import type { InferenceRequest, RawInferenceRequest } from './provider.js'
|
|
7
|
+
import { ModelId } from './schema.js'
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// reasoning_details round trip
|
|
11
|
+
// ============================================================================
|
|
12
|
+
//
|
|
13
|
+
// OpenRouter returns `reasoning_details` on an assistant message and expects the
|
|
14
|
+
// exact sequence back on the following requests, or a reasoning model loses its
|
|
15
|
+
// chain of thought at every tool call. The blocks are opaque, so what matters is
|
|
16
|
+
// that they come back byte-identical — and that a model which never reasons
|
|
17
|
+
// keeps producing the same request bytes, since the prompt cache is keyed on them.
|
|
18
|
+
|
|
19
|
+
const testModel = ModelId('openai/gpt-5.6-luna')
|
|
20
|
+
|
|
21
|
+
/** Shape taken from the OpenRouter docs; treated as opaque everywhere in the SDK. */
|
|
22
|
+
const REASONING_DETAILS = [
|
|
23
|
+
{
|
|
24
|
+
type: 'reasoning.summary',
|
|
25
|
+
summary: 'Analyzed the problem by breaking it into components',
|
|
26
|
+
id: 'reasoning-summary-1',
|
|
27
|
+
format: 'anthropic-claude-v1',
|
|
28
|
+
index: 0,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
type: 'reasoning.encrypted',
|
|
32
|
+
data: 'EroBCkYIBRgCKkCr0hZ0opaque',
|
|
33
|
+
id: 'reasoning-encrypted-1',
|
|
34
|
+
format: 'openai-responses-v1',
|
|
35
|
+
index: 1,
|
|
36
|
+
},
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
type FetchFn = (input: string | URL | Request, init?: RequestInit) => Promise<Response>
|
|
40
|
+
|
|
41
|
+
const createProvider = (fetchFn?: FetchFn) =>
|
|
42
|
+
new OpenRouterProvider({
|
|
43
|
+
apiKey: 'test-key',
|
|
44
|
+
imageProcessor: { resolveContent: async (content) => content },
|
|
45
|
+
defaultModel: String(testModel),
|
|
46
|
+
fetch: fetchFn,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const respondWith = (message: Record<string, unknown>): FetchFn => async () =>
|
|
50
|
+
new Response(
|
|
51
|
+
JSON.stringify({
|
|
52
|
+
id: 'gen-1',
|
|
53
|
+
model: String(testModel),
|
|
54
|
+
choices: [{ message, finish_reason: 'stop' }],
|
|
55
|
+
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
|
56
|
+
}),
|
|
57
|
+
{ status: 200, headers: { 'content-type': 'application/json' } },
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const inferenceRequest = (): InferenceRequest => ({
|
|
61
|
+
model: testModel,
|
|
62
|
+
systemPrompt: 'You are helpful.',
|
|
63
|
+
messages: [{ role: 'user', content: 'What is the weather?' }],
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const rawRequest = (messages: LLMMessage[]): RawInferenceRequest => ({
|
|
67
|
+
model: testModel,
|
|
68
|
+
systemPrompt: 'You are helpful.',
|
|
69
|
+
messages,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('OpenRouter reasoning_details capture', () => {
|
|
73
|
+
test('captures the blocks and the plaintext reasoning from a response', async () => {
|
|
74
|
+
const provider = createProvider(respondWith({
|
|
75
|
+
content: 'It is sunny.',
|
|
76
|
+
reasoning: 'The user asked about weather, so I check the tool first.',
|
|
77
|
+
reasoning_details: REASONING_DETAILS,
|
|
78
|
+
}))
|
|
79
|
+
|
|
80
|
+
const result = await provider.inference(inferenceRequest())
|
|
81
|
+
|
|
82
|
+
expect(isOk(result)).toBe(true)
|
|
83
|
+
if (!isOk(result)) return
|
|
84
|
+
expect(result.value.reasoningDetails).toEqual(REASONING_DETAILS)
|
|
85
|
+
expect(result.value.reasoning).toBe('The user asked about weather, so I check the tool first.')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('leaves both fields unset for a model that does not reason', async () => {
|
|
89
|
+
const provider = createProvider(respondWith({ content: 'It is sunny.' }))
|
|
90
|
+
|
|
91
|
+
const result = await provider.inference(inferenceRequest())
|
|
92
|
+
|
|
93
|
+
expect(isOk(result)).toBe(true)
|
|
94
|
+
if (!isOk(result)) return
|
|
95
|
+
expect(result.value.reasoningDetails).toBeUndefined()
|
|
96
|
+
expect(result.value.reasoning).toBeUndefined()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('normalizes an empty block list to undefined, so nothing is echoed back', async () => {
|
|
100
|
+
const provider = createProvider(respondWith({ content: 'It is sunny.', reasoning_details: [] }))
|
|
101
|
+
|
|
102
|
+
const result = await provider.inference(inferenceRequest())
|
|
103
|
+
|
|
104
|
+
expect(isOk(result)).toBe(true)
|
|
105
|
+
if (!isOk(result)) return
|
|
106
|
+
expect(result.value.reasoningDetails).toBeUndefined()
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('OpenRouter reasoning_details echo', () => {
|
|
111
|
+
test('sends the blocks back verbatim on the assistant message', async () => {
|
|
112
|
+
const http = await createProvider().buildHttpRequest(rawRequest([
|
|
113
|
+
{ role: 'user', content: 'What is the weather?' },
|
|
114
|
+
{
|
|
115
|
+
role: 'assistant',
|
|
116
|
+
content: '',
|
|
117
|
+
toolCalls: [{ id: ToolCallId('call_1'), name: 'get_weather', input: { city: 'Prague' } }],
|
|
118
|
+
reasoningDetails: REASONING_DETAILS,
|
|
119
|
+
},
|
|
120
|
+
{ role: 'tool', toolCallId: ToolCallId('call_1'), content: 'Sunny' },
|
|
121
|
+
]))
|
|
122
|
+
|
|
123
|
+
// Serialized comparison, because "verbatim" is a claim about the bytes on the
|
|
124
|
+
// wire — a deep-equal check would pass even if the blocks were rebuilt.
|
|
125
|
+
const assistant = '{"role":"assistant","content":"","tool_calls":['
|
|
126
|
+
+ '{"id":"call_1","type":"function","function":{"name":"get_weather","arguments":"{\\"city\\":\\"Prague\\"}"}}'
|
|
127
|
+
+ `],"reasoning_details":${JSON.stringify(REASONING_DETAILS)}}`
|
|
128
|
+
expect(JSON.stringify(http.body)).toContain(assistant)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('echoes the blocks on an assistant message that made no tool call', async () => {
|
|
132
|
+
const http = await createProvider().buildHttpRequest(rawRequest([
|
|
133
|
+
{ role: 'user', content: 'Think about it.' },
|
|
134
|
+
{ role: 'assistant', content: 'Thought about it.', reasoningDetails: REASONING_DETAILS },
|
|
135
|
+
{ role: 'user', content: 'And now?' },
|
|
136
|
+
]))
|
|
137
|
+
|
|
138
|
+
expect(JSON.stringify(http.body)).toContain(
|
|
139
|
+
`{"role":"assistant","content":"Thought about it.","reasoning_details":${JSON.stringify(REASONING_DETAILS)}}`,
|
|
140
|
+
)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('OpenRouter request bytes without reasoning', () => {
|
|
145
|
+
// The prompt cache is keyed on the serialized prefix, so a key that appears
|
|
146
|
+
// even as `null`/`undefined` for a non-reasoning model would rewrite it.
|
|
147
|
+
test('adds nothing to a plain assistant message', async () => {
|
|
148
|
+
const http = await createProvider().buildHttpRequest(rawRequest([
|
|
149
|
+
{ role: 'user', content: 'What is 2+2?' },
|
|
150
|
+
{ role: 'assistant', content: '4' },
|
|
151
|
+
{ role: 'user', content: 'And 3+3?' },
|
|
152
|
+
]))
|
|
153
|
+
|
|
154
|
+
const body = JSON.stringify(http.body)
|
|
155
|
+
expect(body).toContain('{"role":"assistant","content":"4"}')
|
|
156
|
+
expect(body).not.toContain('reasoning')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('adds nothing to an assistant message with tool calls', async () => {
|
|
160
|
+
const http = await createProvider().buildHttpRequest(rawRequest([
|
|
161
|
+
{ role: 'user', content: 'What is the weather?' },
|
|
162
|
+
{
|
|
163
|
+
role: 'assistant',
|
|
164
|
+
content: '',
|
|
165
|
+
toolCalls: [{ id: ToolCallId('call_1'), name: 'get_weather', input: { city: 'Prague' } }],
|
|
166
|
+
},
|
|
167
|
+
{ role: 'tool', toolCallId: ToolCallId('call_1'), content: 'Sunny' },
|
|
168
|
+
]))
|
|
169
|
+
|
|
170
|
+
const body = JSON.stringify(http.body)
|
|
171
|
+
expect(body).toContain(
|
|
172
|
+
'{"role":"assistant","content":"","tool_calls":['
|
|
173
|
+
+ '{"id":"call_1","type":"function","function":{"name":"get_weather","arguments":"{\\"city\\":\\"Prague\\"}"}}]}',
|
|
174
|
+
)
|
|
175
|
+
expect(body).not.toContain('reasoning')
|
|
176
|
+
})
|
|
177
|
+
})
|
package/src/core/llm/state.ts
CHANGED
|
@@ -48,6 +48,9 @@ export type LLMMetrics = {
|
|
|
48
48
|
// LLM events
|
|
49
49
|
// ============================================================================
|
|
50
50
|
|
|
51
|
+
/** Opaque by design — provider chain-of-thought blocks are stored and echoed verbatim, never inspected. */
|
|
52
|
+
const opaqueBlocksSchema = z4.array(z4.unknown())
|
|
53
|
+
|
|
51
54
|
const llmMetricsSchema = z4.object({
|
|
52
55
|
promptTokens: z4.number(),
|
|
53
56
|
completionTokens: z4.number(),
|
|
@@ -77,6 +80,15 @@ export const llmEvents = createEventsFactory({
|
|
|
77
80
|
name: z4.string(),
|
|
78
81
|
input: z4.unknown(),
|
|
79
82
|
})),
|
|
83
|
+
/**
|
|
84
|
+
* Opaque provider reasoning blocks (see ReasoningDetails). Carried by the
|
|
85
|
+
* event, not just by the live response, because a session rebuilt from the
|
|
86
|
+
* log would otherwise resume without the chain of thought the provider
|
|
87
|
+
* expects to get back.
|
|
88
|
+
*/
|
|
89
|
+
reasoningDetails: opaqueBlocksSchema.optional(),
|
|
90
|
+
/** Anthropic thinking blocks (see ThinkingBlocks). Carried for the same reason. */
|
|
91
|
+
thinkingBlocks: opaqueBlocksSchema.optional(),
|
|
80
92
|
}),
|
|
81
93
|
metrics: llmMetricsSchema,
|
|
82
94
|
llmCallId: llmCallIdSchema.optional(),
|