narraeon 0.1.1 → 0.3.0

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.
Files changed (29) hide show
  1. package/dist/node/protocol/modelConnections.js +49 -0
  2. package/dist/node/protocol/modelUsage.js +39 -0
  3. package/dist/node/protocol/v1.js +35 -0
  4. package/dist/node/runtime/FileNativePhysicalClone.js +86 -0
  5. package/dist/node/runtime/V1Runtime.js +66 -29
  6. package/dist/node/runtime/model/FileNativeModelAdapters.js +730 -287
  7. package/dist/node/runtime/model/ModelConnectionStore.js +123 -2
  8. package/dist/node/runtime/model/ModelHost.js +81 -9
  9. package/dist/node/runtime/model/ModelHostStream.js +61 -1
  10. package/dist/node/runtime/model/SettingAuthorStream.js +16 -113
  11. package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +269 -0
  12. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +1128 -0
  13. package/dist/node/runtime/play/PlayCallChain.js +904 -369
  14. package/dist/node/runtime/play/PlayDocumentTools.js +35 -0
  15. package/dist/node/runtime/play/PlayFollowupRequests.js +43 -26
  16. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +50 -35
  17. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +55 -29
  18. package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +9 -4
  19. package/dist/node/runtime/world/FileNativeAuthorityV3.js +971 -0
  20. package/dist/node/runtime/world/FileNativeHistoryProjection.js +39 -0
  21. package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +1067 -0
  22. package/dist/node/runtime/world/FileNativeWorldStore.js +615 -784
  23. package/dist/node/server/createServer.js +10 -2
  24. package/dist/web/assets/index-D5VqDvjY.js +1192 -0
  25. package/dist/web/assets/index-Xa0s35CJ.css +1 -0
  26. package/dist/web/index.html +2 -2
  27. package/package.json +1 -1
  28. package/dist/web/assets/index-CRGTZ9Ak.css +0 -1
  29. package/dist/web/assets/index-DDx1itzT.js +0 -1192
@@ -2,18 +2,50 @@ import { createHash } from "node:crypto";
2
2
  import { appendFile, chmod, mkdir, stat } from "node:fs/promises";
3
3
  import { dirname } from "node:path";
4
4
  import { defaultAppLocale, } from "../../protocol/appPreferences.js";
5
- import { ModelHostFailureError, ModelHostOutcomeUnknownError, } from "./ModelHost.js";
5
+ import { hasCLIProxyThinkingSuffix, modelReasoningPolicyIssue, } from "../../protocol/modelConnections.js";
6
+ import { ModelHostBindingMismatchError, ModelHostCancelledError, ModelHostContinuationError, ModelHostFailureError, ModelHostOutcomeUnknownError, } from "./ModelHost.js";
6
7
  import { renderPromptDeltaMessage, } from "../prompt/FileNativePromptCompiler.js";
7
8
  import { aggregateAnthropicSettingStream, aggregateChatSettingStream, } from "./SettingAuthorStream.js";
8
9
  import { aggregateAnthropicModelStream, aggregateChatModelStream, aggregateResponsesModelStream, } from "./ModelHostStream.js";
9
- import { defaultRuntimeToolDefinitionStrategy, } from "../prompt/FileNativeToolRegistry.js";
10
+ import { defaultRuntimeToolDefinitionStrategy, portableRuntimeToolInputSchema, } from "../prompt/FileNativeToolRegistry.js";
10
11
  import { AiExchangeCapture, errorDescription, } from "./AiFailureLog.js";
12
+ const providerContinuationCodecVersion = 1;
13
+ function resolveConnection(connection) {
14
+ const resolved = structuredClone({
15
+ ...connection,
16
+ dialect: connection.dialect ?? "standard",
17
+ reasoningEffort: connection.reasoningEffort ?? "provider_default",
18
+ reasoningSummary: connection.reasoningSummary ?? "provider_default",
19
+ thinkingMode: connection.thinkingMode ?? "provider_default",
20
+ thinkingBudgetTokens: connection.thinkingBudgetTokens ?? null,
21
+ });
22
+ const issue = modelReasoningPolicyIssue({
23
+ provider: resolved.provider,
24
+ dialect: resolved.dialect,
25
+ modelId: resolved.modelId,
26
+ effort: resolved.reasoningEffort,
27
+ summary: resolved.reasoningSummary,
28
+ thinking: resolved.thinkingMode,
29
+ thinkingBudgetTokens: resolved.thinkingBudgetTokens,
30
+ maxOutputTokens: resolved.maxOutputTokens,
31
+ });
32
+ if (issue !== null)
33
+ throw new Error(issue);
34
+ return resolved;
35
+ }
36
+ function modelCacheStrategy(connection) {
37
+ if (connection.provider === "anthropic_messages")
38
+ return "explicit_anthropic_blocks";
39
+ if (connection.dialect === "cliproxyapi")
40
+ return "explicit_cliproxyapi_message";
41
+ return "provider_managed";
42
+ }
11
43
  export class FileNativeModelHost {
12
44
  #connection;
13
45
  #fetch;
14
46
  #failureLog;
15
47
  constructor(connection, fetchImplementation = fetch, failureLog) {
16
- this.#connection = structuredClone(connection);
48
+ this.#connection = resolveConnection(connection);
17
49
  this.#fetch = fetchImplementation;
18
50
  this.#failureLog = failureLog;
19
51
  }
@@ -24,6 +56,7 @@ export class FileNativeModelHost {
24
56
  modelId,
25
57
  contextWindowTokens,
26
58
  maxOutputTokens,
59
+ cacheStrategy: modelCacheStrategy(this.#connection),
27
60
  endpointFingerprint: createHash("sha256")
28
61
  .update(`${provider}\0${this.#connection.baseUrl}`)
29
62
  .digest("hex"),
@@ -35,75 +68,71 @@ export class FileNativeModelHost {
35
68
  responseStorage: provider === "openai_responses" ? "disabled" : "provider_default",
36
69
  encryptedReasoningContinuation: provider === "openai_responses" ? "included" : "not_requested",
37
70
  anthropicVersion: provider === "anthropic_messages" ? "2023-06-01" : null,
71
+ dialect: this.#connection.dialect,
72
+ reasoningEffort: this.#connection.reasoningEffort,
73
+ reasoningSummary: this.#connection.reasoningSummary,
74
+ thinkingMode: this.#connection.thinkingMode,
75
+ thinkingBudgetTokens: this.#connection.thinkingBudgetTokens,
76
+ cachePolicy: modelCacheStrategy(this.#connection),
77
+ providerContinuationCodecVersion,
38
78
  }))
39
79
  .digest("hex"),
40
80
  };
41
81
  }
42
82
  async exchange(request, observer) {
83
+ assertExchangeCompatibility(this.#connection, request);
43
84
  await trace("model_host_exchange", modelHostTrace(request));
44
- if (this.#connection.provider === "chat_completions")
45
- return this.#chat(request, observer?.onDelta);
46
- if (this.#connection.provider === "openai_responses")
47
- return this.#responses(request, observer?.onDelta);
48
- return this.#anthropic(request, observer?.onDelta);
85
+ const signal = observer?.signal;
86
+ if (signalWasAborted(signal))
87
+ throw cancelledModelRequest();
88
+ try {
89
+ if (this.#connection.provider === "chat_completions")
90
+ return await this.#chat(request, observer?.onDelta, signal);
91
+ if (this.#connection.provider === "openai_responses")
92
+ return await this.#responses(request, observer?.onDelta, signal);
93
+ return await this.#anthropic(request, observer?.onDelta, signal);
94
+ }
95
+ catch (error) {
96
+ if (signalWasAborted(signal) &&
97
+ !(error instanceof ModelHostCancelledError))
98
+ throw cancelledModelRequest(error);
99
+ throw error;
100
+ }
49
101
  }
50
- async #responses(request, onDelta) {
102
+ /** Uses the production encoders and deliberately omits credentials. */
103
+ previewRequest(request) {
104
+ assertExchangeCompatibility(this.#connection, request);
105
+ const provider = this.#connection.provider;
106
+ const body = provider === "chat_completions"
107
+ ? chatRequestBody(this.#connection, request)
108
+ : provider === "openai_responses"
109
+ ? responsesRequestBody(responsesRequestInput(this.#connection, request))
110
+ : anthropicRequestBody(this.#connection, request);
111
+ return {
112
+ provider,
113
+ method: "POST",
114
+ endpointPath: providerUrl(this.#connection).pathname,
115
+ headerNames: provider === "anthropic_messages"
116
+ ? ["Accept", "anthropic-version", "x-api-key", "Content-Type"]
117
+ : ["Accept", "Authorization", "Content-Type"],
118
+ body: cloneProviderValue(body),
119
+ };
120
+ }
121
+ async #responses(request, onDelta, signal) {
51
122
  return openAIResponsesRequest({
52
- connection: this.#connection,
123
+ ...responsesRequestInput(this.#connection, request),
53
124
  fetchImplementation: this.#fetch,
54
- input: [
55
- ...request.bootstrap.provider.messages.map(responsesBootstrapMessage),
56
- ...responsesAppend(request),
57
- ],
58
- tools: providerToolDefinitions(request).map((tool) => ({
59
- name: tool.name,
60
- description: tool.description,
61
- parameters: tool.inputSchema,
62
- })),
63
- allowedTools: providerAllowedTools(request),
64
- toolStrategy: providerToolStrategy(request, this.#connection.provider),
65
- hasFrozenToolPolicy: hasFrozenToolPolicy(request),
66
- maxOutputTokens: request.maxOutputTokens,
67
125
  tracePrefix: "responses",
68
126
  ...(this.#failureLog === undefined
69
127
  ? {}
70
128
  : { failureLog: this.#failureLog }),
71
129
  diagnosticContext: modelDiagnosticContext(request),
72
130
  ...(onDelta === undefined ? {} : { onDelta }),
131
+ ...(signal === undefined ? {} : { signal }),
73
132
  });
74
133
  }
75
- async #chat(request, onDelta) {
76
- const messages = [
77
- ...request.bootstrap.provider.messages,
78
- ...chatAppend(request),
79
- ];
80
- const tools = providerToolDefinitions(request);
81
- const allowedTools = providerAllowedTools(request);
82
- const hasPolicy = hasFrozenToolPolicy(request);
83
- const body = {
84
- model: this.#connection.modelId,
85
- messages,
86
- ...(tools.length === 0
87
- ? {}
88
- : {
89
- tools: tools.map((tool) => ({
90
- type: "function",
91
- function: {
92
- name: tool.name,
93
- description: tool.description,
94
- parameters: tool.inputSchema,
95
- },
96
- })),
97
- }),
98
- ...(hasPolicy && allowedTools.length === 0
99
- ? { tool_choice: "none" }
100
- : tools.length === 0
101
- ? {}
102
- : { tool_choice: "auto" }),
103
- max_tokens: request.maxOutputTokens,
104
- stream: true,
105
- stream_options: { include_usage: true },
106
- };
134
+ async #chat(request, onDelta, signal) {
135
+ const body = chatRequestBody(this.#connection, request);
107
136
  const bodyJson = JSON.stringify(body);
108
137
  await trace("chat_request", body);
109
138
  const url = providerUrl(this.#connection);
@@ -118,11 +147,12 @@ export class FileNativeModelHost {
118
147
  "Content-Type": "application/json",
119
148
  },
120
149
  body: bodyJson,
150
+ ...(signal === undefined ? {} : { signal }),
121
151
  }, "Chat Completions"));
122
152
  if (!response.ok)
123
153
  throw await providerError(response);
124
154
  if (isProviderEventStream(response)) {
125
- const streamed = await providerStreamResult(response, "Chat Completions", (body) => aggregateChatModelStream(body, diagnosticDelta));
155
+ const streamed = await providerStreamResult(response, "Chat Completions", (body) => aggregateChatModelStream(body, diagnosticDelta), signal);
126
156
  capture?.setReasoning(streamed.reasoningContent);
127
157
  await trace("chat_response", streamed);
128
158
  return {
@@ -135,6 +165,9 @@ export class FileNativeModelHost {
135
165
  assistantMessage: cloneProviderValue(streamed.assistantMessage),
136
166
  },
137
167
  usage: normalizeChatUsage(streamed.usage),
168
+ ...(streamed.stopReason === undefined
169
+ ? {}
170
+ : { stopReason: streamed.stopReason }),
138
171
  toolCalls: streamed.toolCalls.map((call) => ({
139
172
  id: call.id,
140
173
  name: call.name,
@@ -151,21 +184,14 @@ export class FileNativeModelHost {
151
184
  const payload = rawPayload;
152
185
  await trace("chat_response", payload);
153
186
  const message = payload.choices?.[0]?.message;
187
+ const finishReason = payload.choices?.[0]?.finish_reason;
154
188
  if (message === undefined)
155
189
  throw unknownProviderResponse("Chat Completions");
156
- if ((message.content !== undefined &&
157
- message.content !== null &&
158
- typeof message.content !== "string") ||
159
- (message.reasoning_content !== undefined &&
160
- message.reasoning_content !== null &&
161
- typeof message.reasoning_content !== "string") ||
162
- (message.tool_calls !== undefined &&
163
- (!Array.isArray(message.tool_calls) ||
164
- message.tool_calls.some((call) => !isRecord(call) ||
165
- typeof call.id !== "string" ||
166
- !isRecord(call.function) ||
167
- typeof call.function.name !== "string" ||
168
- typeof call.function.arguments !== "string"))))
190
+ if (finishReason !== undefined &&
191
+ finishReason !== null &&
192
+ typeof finishReason !== "string")
193
+ throw unknownProviderResponse("Chat Completions");
194
+ if (!validChatAssistantMessage(message))
169
195
  throw unknownProviderResponse("Chat Completions");
170
196
  capture?.setReasoning(message.reasoning_content ?? undefined);
171
197
  return {
@@ -178,6 +204,9 @@ export class FileNativeModelHost {
178
204
  assistantMessage: cloneProviderValue(message),
179
205
  },
180
206
  usage: normalizeChatUsage(payload.usage),
207
+ ...(typeof finishReason === "string"
208
+ ? { stopReason: finishReason }
209
+ : {}),
181
210
  toolCalls: (message.tool_calls ?? []).map((call) => ({
182
211
  id: call.id,
183
212
  name: call.function.name,
@@ -186,27 +215,8 @@ export class FileNativeModelHost {
186
215
  };
187
216
  });
188
217
  }
189
- async #anthropic(request, onDelta) {
190
- const tools = providerToolDefinitions(request);
191
- const body = {
192
- model: this.#connection.modelId,
193
- system: request.bootstrap.provider.system,
194
- messages: [
195
- ...request.bootstrap.provider.messages.filter(({ role }) => role !== "system"),
196
- ...anthropicAppend(request),
197
- ],
198
- ...(tools.length === 0
199
- ? {}
200
- : {
201
- tools: tools.map((tool) => ({
202
- name: tool.name,
203
- description: tool.description,
204
- input_schema: tool.inputSchema,
205
- })),
206
- }),
207
- max_tokens: request.maxOutputTokens,
208
- stream: true,
209
- };
218
+ async #anthropic(request, onDelta, signal) {
219
+ const body = anthropicRequestBody(this.#connection, request);
210
220
  const bodyJson = JSON.stringify(body);
211
221
  await trace("anthropic_request", body);
212
222
  const url = providerUrl(this.#connection);
@@ -222,11 +232,12 @@ export class FileNativeModelHost {
222
232
  "Content-Type": "application/json",
223
233
  },
224
234
  body: bodyJson,
235
+ ...(signal === undefined ? {} : { signal }),
225
236
  }, "Anthropic Messages"));
226
237
  if (!response.ok)
227
238
  throw await providerError(response);
228
239
  if (isProviderEventStream(response)) {
229
- const streamed = await providerStreamResult(response, "Anthropic Messages", (body) => aggregateAnthropicModelStream(body, diagnosticDelta));
240
+ const streamed = await providerStreamResult(response, "Anthropic Messages", (body) => aggregateAnthropicModelStream(body, diagnosticDelta), signal);
230
241
  capture?.setReasoning(streamed.reasoningContent);
231
242
  await trace("anthropic_response", streamed);
232
243
  return {
@@ -253,6 +264,9 @@ export class FileNativeModelHost {
253
264
  : { stopReason: streamed.stopReason }),
254
265
  },
255
266
  usage: normalizeAnthropicUsage(streamed.usage),
267
+ ...(typeof streamed.stopReason === "string"
268
+ ? { stopReason: streamed.stopReason }
269
+ : {}),
256
270
  };
257
271
  }
258
272
  const rawPayload = await providerJson(response, "Anthropic Messages");
@@ -260,14 +274,7 @@ export class FileNativeModelHost {
260
274
  throw unknownProviderResponse("Anthropic Messages");
261
275
  const payload = rawPayload;
262
276
  const content = payload.content;
263
- if (content.some((item) => !isRecord(item) ||
264
- typeof item.type !== "string" ||
265
- (item.type === "text" && typeof item.text !== "string") ||
266
- (item.type === "thinking" && typeof item.thinking !== "string") ||
267
- (item.type === "tool_use" &&
268
- (typeof item.id !== "string" ||
269
- typeof item.name !== "string" ||
270
- !("input" in item)))))
277
+ if (!validAnthropicContent(content))
271
278
  throw unknownProviderResponse("Anthropic Messages");
272
279
  await trace("anthropic_response", payload);
273
280
  const reasoningContent = content
@@ -298,10 +305,21 @@ export class FileNativeModelHost {
298
305
  : { stopReason: payload.stop_reason }),
299
306
  },
300
307
  usage: normalizeAnthropicUsage(payload.usage),
308
+ ...(typeof payload.stop_reason === "string"
309
+ ? { stopReason: payload.stop_reason }
310
+ : {}),
301
311
  };
302
312
  });
303
313
  }
304
314
  }
315
+ function assertExchangeCompatibility(connection, request) {
316
+ if (request.bootstrap.provider.protocol !== connection.provider)
317
+ throw new ModelHostBindingMismatchError("The compiled prompt protocol does not match the selected model host");
318
+ if (request.bootstrap.cache.strategy !== modelCacheStrategy(connection))
319
+ throw new ModelHostBindingMismatchError("The compiled prompt cache policy does not match the selected model host");
320
+ if (request.maxOutputTokens > connection.maxOutputTokens)
321
+ throw new ModelHostBindingMismatchError("The request output limit exceeds the selected model host binding");
322
+ }
305
323
  function modelHostTrace(request) {
306
324
  return {
307
325
  requestId: request.requestId,
@@ -376,6 +394,8 @@ async function capturedProviderOperation(failureLog, capture, operation) {
376
394
  return { ...value, diagnostics: capture.snapshot() };
377
395
  }
378
396
  catch (error) {
397
+ if (error instanceof ModelHostCancelledError)
398
+ throw error;
379
399
  if (capture !== undefined && failureLog !== undefined) {
380
400
  const exchange = capture.snapshot();
381
401
  const failure = {
@@ -420,7 +440,7 @@ export class FileNativeSettingAuthorProvider {
420
440
  #locale;
421
441
  #exchange = 0;
422
442
  constructor(connection, fetchImplementation = fetch, diagnostics) {
423
- this.#connection = structuredClone(connection);
443
+ this.#connection = resolveConnection(connection);
424
444
  this.#fetch = fetchImplementation;
425
445
  this.#failureLog = diagnostics?.failureLog;
426
446
  this.#operationId = diagnostics?.operationId;
@@ -441,7 +461,13 @@ export class FileNativeSettingAuthorProvider {
441
461
  if (this.#connection.provider === "chat_completions") {
442
462
  const body = {
443
463
  model: this.#connection.modelId,
444
- messages: request.messages.map(chatSettingMessage),
464
+ ...chatReasoningRequest(this.#connection),
465
+ messages: chatSettingMessages(this.#connection, request.messages),
466
+ ...(this.#connection.dialect === "cliproxyapi"
467
+ ? {
468
+ prompt_cache_key: settingPromptCacheKey(this.#connection, request.messages, this.#operationId),
469
+ }
470
+ : {}),
445
471
  ...(tools.length === 0
446
472
  ? {}
447
473
  : {
@@ -485,6 +511,10 @@ export class FileNativeSettingAuthorProvider {
485
511
  ...(streamed.reasoningContent === ""
486
512
  ? {}
487
513
  : { reasoningContent: streamed.reasoningContent }),
514
+ providerState: {
515
+ protocol: "chat_completions",
516
+ assistantMessage: cloneProviderValue(streamed.assistantMessage),
517
+ },
488
518
  toolCalls: streamed.toolCalls.map((call) => ({
489
519
  id: call.id,
490
520
  name: call.name,
@@ -498,13 +528,14 @@ export class FileNativeSettingAuthorProvider {
498
528
  const response = await openAIResponsesRequest({
499
529
  connection: this.#connection,
500
530
  fetchImplementation: this.#fetch,
501
- input: request.messages.flatMap(responsesSettingMessage),
531
+ input: responsesSettingInput(this.#connection, request.messages),
502
532
  tools: tools.map((tool) => ({
503
533
  name: tool.name,
504
534
  description: tool.description,
505
535
  parameters: tool.schema,
506
536
  })),
507
537
  maxOutputTokens: Math.min(request.maxOutputTokens, this.#connection.maxOutputTokens),
538
+ promptCacheKey: settingPromptCacheKey(this.#connection, request.messages, this.#operationId),
508
539
  tracePrefix: "setting_responses",
509
540
  ...(this.#failureLog === undefined
510
541
  ? {}
@@ -515,6 +546,9 @@ export class FileNativeSettingAuthorProvider {
515
546
  return {
516
547
  role: "assistant",
517
548
  content: response.text ?? "",
549
+ ...(response.reasoningContent === undefined
550
+ ? {}
551
+ : { reasoningContent: response.reasoningContent }),
518
552
  providerState: response.providerState,
519
553
  toolCalls: (response.toolCalls ?? []).map((call) => ({
520
554
  ...call,
@@ -528,14 +562,18 @@ export class FileNativeSettingAuthorProvider {
528
562
  }
529
563
  const system = request.messages
530
564
  .filter(({ role }) => role === "system")
531
- .map(({ content }) => content)
532
- .join("\n\n");
565
+ .map(({ content }, index, messages) => ({
566
+ type: "text",
567
+ text: content,
568
+ ...(index === messages.length - 1
569
+ ? { cache_control: { type: "ephemeral" } }
570
+ : {}),
571
+ }));
533
572
  const body = {
534
573
  model: this.#connection.modelId,
574
+ ...anthropicReasoningRequest(this.#connection),
535
575
  ...(system.length === 0 ? {} : { system }),
536
- messages: request.messages
537
- .filter(({ role }) => role !== "system")
538
- .map(anthropicSettingMessage),
576
+ messages: anthropicSettingMessages(request.messages),
539
577
  ...(tools.length === 0
540
578
  ? {}
541
579
  : {
@@ -576,25 +614,41 @@ export class FileNativeSettingAuthorProvider {
576
614
  ...(streamed.reasoningContent === ""
577
615
  ? {}
578
616
  : { reasoningContent: streamed.reasoningContent }),
617
+ providerState: {
618
+ protocol: "anthropic_messages",
619
+ content: cloneProviderValue(streamed.providerContent),
620
+ ...(streamed.responseId === undefined
621
+ ? {}
622
+ : { responseId: streamed.responseId }),
623
+ ...(streamed.model === undefined ? {} : { model: streamed.model }),
624
+ ...(streamed.stopReason === undefined
625
+ ? {}
626
+ : { stopReason: streamed.stopReason }),
627
+ },
579
628
  toolCalls: streamed.toolCalls.map((call) => ({
580
629
  id: call.id,
581
630
  name: call.name,
582
- arguments: asArguments(parseArguments(call.arguments)),
631
+ arguments: asArguments(structuredClone(call.arguments)),
583
632
  })),
584
633
  usage: settingAuthorUsage(normalizeAnthropicUsage(streamed.usage)),
585
634
  };
586
635
  });
587
636
  }
588
637
  }
589
- // The progress projection only counts tokens, so the nullable per-field
590
- // provenance of ModelHostUsage collapses to two numbers here.
638
+ // Setting authoring uses the same complete usage projection as play. Missing
639
+ // Provider usage stays unavailable instead of being counted as zero.
591
640
  function settingAuthorUsage(usage) {
592
- if (usage === undefined)
593
- return { inputTokens: 0, outputTokens: 0 };
594
- return {
595
- inputTokens: usage.inputTokens ?? 0,
596
- outputTokens: usage.outputTokens ?? 0,
597
- };
641
+ if (usage !== undefined)
642
+ return structuredClone(usage);
643
+ return usageRecord({
644
+ inputTokens: null,
645
+ uncachedInputTokens: null,
646
+ cacheReadTokens: null,
647
+ cacheWriteTokens: null,
648
+ reasoningTokens: null,
649
+ outputTokens: null,
650
+ totalTokens: null,
651
+ });
598
652
  }
599
653
  function settingToolDefinition(name, locale = defaultAppLocale) {
600
654
  const descriptions = settingToolDescriptions[locale];
@@ -698,6 +752,126 @@ const settingToolDescriptions = {
698
752
  function items(request) {
699
753
  return request.appended;
700
754
  }
755
+ function chatRequestBody(connection, request) {
756
+ const tools = providerToolDefinitions(request);
757
+ const allowedTools = providerAllowedTools(request);
758
+ const hasPolicy = hasFrozenToolPolicy(request);
759
+ return {
760
+ model: connection.modelId,
761
+ ...chatReasoningRequest(connection),
762
+ messages: [
763
+ ...chatBootstrapMessages(connection, request),
764
+ ...chatAppend(request),
765
+ ],
766
+ ...(connection.dialect === "cliproxyapi"
767
+ ? { prompt_cache_key: promptCacheKey(connection, request) }
768
+ : {}),
769
+ ...(tools.length === 0
770
+ ? {}
771
+ : {
772
+ tools: tools.map((tool) => ({
773
+ type: "function",
774
+ function: {
775
+ name: tool.name,
776
+ description: tool.description,
777
+ parameters: tool.inputSchema,
778
+ },
779
+ })),
780
+ }),
781
+ ...(hasPolicy && allowedTools.length === 0
782
+ ? { tool_choice: "none" }
783
+ : tools.length === 0
784
+ ? {}
785
+ : { tool_choice: "auto" }),
786
+ max_tokens: request.maxOutputTokens,
787
+ stream: true,
788
+ stream_options: { include_usage: true },
789
+ };
790
+ }
791
+ function anthropicRequestBody(connection, request) {
792
+ const tools = providerToolDefinitions(request);
793
+ return {
794
+ model: connection.modelId,
795
+ ...anthropicReasoningRequest(connection),
796
+ system: request.bootstrap.provider.system,
797
+ messages: [
798
+ ...request.bootstrap.provider.messages.filter(({ role }) => role !== "system"),
799
+ ...anthropicAppend(request),
800
+ ],
801
+ ...(tools.length === 0
802
+ ? {}
803
+ : {
804
+ tools: tools.map((tool) => ({
805
+ name: tool.name,
806
+ description: tool.description,
807
+ input_schema: tool.inputSchema,
808
+ })),
809
+ }),
810
+ ...(hasFrozenToolPolicy(request) &&
811
+ providerAllowedTools(request).length === 0
812
+ ? { tool_choice: { type: "none" } }
813
+ : {}),
814
+ max_tokens: request.maxOutputTokens,
815
+ stream: true,
816
+ };
817
+ }
818
+ function responsesRequestInput(connection, request) {
819
+ return {
820
+ connection,
821
+ input: [
822
+ ...responsesBootstrapMessages(connection, request),
823
+ ...responsesAppend(request),
824
+ ],
825
+ tools: providerToolDefinitions(request).map((tool) => ({
826
+ name: tool.name,
827
+ description: tool.description,
828
+ parameters: tool.inputSchema,
829
+ })),
830
+ allowedTools: providerAllowedTools(request),
831
+ toolStrategy: providerToolStrategy(request, connection.provider),
832
+ hasFrozenToolPolicy: hasFrozenToolPolicy(request),
833
+ maxOutputTokens: request.maxOutputTokens,
834
+ promptCacheKey: promptCacheKey(connection, request),
835
+ };
836
+ }
837
+ function responsesRequestBody(input) {
838
+ const allowedTools = input.allowedTools ?? input.tools.map((tool) => tool.name);
839
+ const toolChoice = input.hasFrozenToolPolicy === true && allowedTools.length === 0
840
+ ? "none"
841
+ : input.hasFrozenToolPolicy === true &&
842
+ input.toolStrategy === "native_allowed_subset"
843
+ ? {
844
+ type: "allowed_tools",
845
+ mode: "auto",
846
+ tools: allowedTools.map((name) => ({ type: "function", name })),
847
+ }
848
+ : "auto";
849
+ return {
850
+ model: input.connection.modelId,
851
+ ...responsesReasoningRequest(resolveConnection(input.connection)),
852
+ input: input.input,
853
+ ...(input.tools.length === 0
854
+ ? input.hasFrozenToolPolicy === true && allowedTools.length === 0
855
+ ? { tool_choice: "none" }
856
+ : {}
857
+ : {
858
+ tools: input.tools.map((tool) => ({
859
+ type: "function",
860
+ name: tool.name,
861
+ description: tool.description,
862
+ parameters: tool.parameters,
863
+ })),
864
+ tool_choice: toolChoice,
865
+ }),
866
+ max_output_tokens: input.maxOutputTokens,
867
+ ...(input.promptCacheKey === undefined
868
+ ? {}
869
+ : { prompt_cache_key: input.promptCacheKey }),
870
+ store: false,
871
+ include: ["reasoning.encrypted_content"],
872
+ stream: true,
873
+ };
874
+ }
701
875
  /**
702
876
  * A frozen policy is present when the caller supplies the logical operation's
703
877
  * complete tool universe. All play lifecycles share this one ModelHost seam.
@@ -712,7 +886,26 @@ function frozenToolPolicy(request) {
712
886
  }
713
887
  function providerToolDefinitions(request) {
714
888
  const policy = frozenToolPolicy(request);
715
- return policy?.toolUniverse ?? request.tools;
889
+ return (policy?.toolUniverse ?? request.tools).map((tool) => {
890
+ if (!hasUnsupportedProviderToolSchemaRoot(tool.inputSchema))
891
+ return tool;
892
+ const portableInputSchema = portableRuntimeToolInputSchema(tool.name);
893
+ if (portableInputSchema === null)
894
+ throw new Error(`Provider tool ${tool.name} uses oneOf, allOf, or anyOf at the input schema root`);
895
+ if (hasUnsupportedProviderToolSchemaRoot(portableInputSchema))
896
+ throw new Error(`Provider tool ${tool.name} has no portable root-object input schema`);
897
+ // Released play contexts freeze their logical tool universe. Migrate only
898
+ // the Provider wire projection of the old root-union schema; Runtime keeps
899
+ // validating the original source-specific contract and the persisted
900
+ // transcript remains untouched.
901
+ return {
902
+ ...tool,
903
+ inputSchema: portableInputSchema,
904
+ };
905
+ });
906
+ }
907
+ function hasUnsupportedProviderToolSchemaRoot(schema) {
908
+ return ["oneOf", "allOf", "anyOf"].some((keyword) => Object.prototype.hasOwnProperty.call(schema, keyword));
716
909
  }
717
910
  function providerAllowedTools(request) {
718
911
  const policy = frozenToolPolicy(request);
@@ -726,82 +919,127 @@ function providerToolStrategy(request, provider) {
726
919
  return policy.toolStrategy;
727
920
  return defaultRuntimeToolDefinitionStrategy(provider);
728
921
  }
922
+ function chatBootstrapMessages(connection, request) {
923
+ const messages = request.bootstrap.provider.messages.map((message) => cloneProviderValue(message));
924
+ return connection.dialect === "cliproxyapi"
925
+ ? attachCacheControlToLastRole(messages, "system")
926
+ : messages;
927
+ }
928
+ function responsesBootstrapMessages(connection, request) {
929
+ const messages = request.bootstrap.provider.messages.map((message) => responsesBootstrapMessage(message, connection.dialect === "cliproxyapi"));
930
+ return connection.dialect === "cliproxyapi"
931
+ ? attachCacheControlToLastRole(messages, "system")
932
+ : messages;
933
+ }
934
+ function attachCacheControlToLastRole(messages, role) {
935
+ const result = cloneProviderValue(messages);
936
+ let target = -1;
937
+ for (const [index, message] of result.entries())
938
+ if (isRecord(message) && message.role === role)
939
+ target = index;
940
+ if (target < 0)
941
+ return result;
942
+ const message = result[target];
943
+ if (!isRecord(message))
944
+ return result;
945
+ result[target] = {
946
+ ...message,
947
+ cache_control: { type: "ephemeral" },
948
+ };
949
+ return result;
950
+ }
951
+ function promptCacheKey(connection, request) {
952
+ const stablePrefixScope = request.bootstrap.cache.stablePrefixFingerprint ??
953
+ createHash("sha256")
954
+ .update(JSON.stringify({
955
+ provider: request.bootstrap.provider,
956
+ tools: request.toolUniverse ?? request.tools,
957
+ toolStrategy: request.toolStrategy,
958
+ }))
959
+ .digest("hex");
960
+ // OpenAI uses this as a cache-routing hint, so equal frozen prefixes should
961
+ // share it. CLIProxyAPI additionally treats it as execution-session identity;
962
+ // there it must stay stable within one chain and isolated across chains.
963
+ const scope = connection.dialect === "cliproxyapi"
964
+ ? (request.operationId ?? stablePrefixScope)
965
+ : stablePrefixScope;
966
+ return createHash("sha256")
967
+ .update(`${connection.provider}\0${connection.dialect}\0${connection.modelId}\0${scope}`)
968
+ .digest("hex");
969
+ }
970
+ function settingPromptCacheKey(connection, messages, operationId) {
971
+ const firstAssistant = messages.findIndex(({ role }) => role === "assistant");
972
+ const initialPrefix = messages.slice(0, firstAssistant < 0 ? messages.length : firstAssistant);
973
+ const stablePrefixScope = JSON.stringify(initialPrefix);
974
+ const scope = connection.dialect === "cliproxyapi"
975
+ ? (operationId ?? stablePrefixScope)
976
+ : stablePrefixScope;
977
+ return createHash("sha256")
978
+ .update(`${connection.provider}\0${connection.dialect}\0${connection.modelId}\0${scope}`)
979
+ .digest("hex");
980
+ }
729
981
  function chatAppend(request) {
730
982
  return items(request).flatMap((item) => {
731
983
  if (item.kind === "player")
732
- return { role: "user", content: item.text };
984
+ return [{ role: "user", content: item.text }];
733
985
  if (item.kind === "prompt_delta")
734
986
  return promptDeltaProviderMessages(item.logicalMessages);
735
987
  if (item.kind === "tool")
736
- return {
737
- role: "tool",
738
- tool_call_id: item.toolCallId,
739
- content: item.markdown,
740
- };
988
+ return [
989
+ {
990
+ role: "tool",
991
+ tool_call_id: item.toolCallId,
992
+ content: item.markdown,
993
+ },
994
+ ];
741
995
  if (item.providerState?.protocol === "chat_completions") {
742
996
  const message = item.providerState.assistantMessage;
743
- if (isRecord(message))
997
+ if (validChatAssistantMessage(message))
744
998
  return [cloneProviderValue(message)];
745
999
  }
746
- const assistant = {
747
- role: "assistant",
748
- content: item.text,
749
- ...(item.reasoningContent === undefined
750
- ? {}
751
- : { reasoning_content: item.reasoningContent }),
752
- ...(item.toolCalls.length === 0
753
- ? {}
754
- : {
755
- tool_calls: item.toolCalls.map((call) => ({
756
- id: call.id,
757
- type: "function",
758
- function: {
759
- name: call.name,
760
- arguments: JSON.stringify(call.arguments),
761
- },
762
- })),
763
- }),
764
- };
765
- return assistant;
1000
+ throw continuationError("Chat Completions");
766
1001
  });
767
1002
  }
768
1003
  function anthropicAppend(request) {
769
- return items(request).flatMap((item) => {
770
- if (item.kind === "player")
771
- return { role: "user", content: item.text };
772
- if (item.kind === "prompt_delta")
773
- return promptDeltaProviderMessages(item.logicalMessages);
774
- if (item.kind === "tool")
775
- return {
776
- role: "user",
777
- content: [
778
- {
779
- type: "tool_result",
780
- tool_use_id: item.toolCallId,
781
- content: item.markdown,
782
- },
783
- ],
784
- };
785
- if (item.providerState?.protocol === "anthropic_messages")
786
- return [
787
- {
788
- role: "assistant",
789
- content: cloneProviderValue(item.providerState.content),
790
- },
791
- ];
792
- return {
793
- role: "assistant",
794
- content: [
795
- ...(item.text.length === 0 ? [] : [{ type: "text", text: item.text }]),
796
- ...item.toolCalls.map((call) => ({
797
- type: "tool_use",
798
- id: call.id,
799
- name: call.name,
800
- input: call.arguments,
801
- })),
802
- ],
803
- };
804
- });
1004
+ const messages = [];
1005
+ let toolResults = [];
1006
+ const flushToolResults = () => {
1007
+ if (toolResults.length === 0)
1008
+ return;
1009
+ messages.push({ role: "user", content: toolResults });
1010
+ toolResults = [];
1011
+ };
1012
+ for (const item of items(request)) {
1013
+ if (item.kind === "tool") {
1014
+ toolResults.push({
1015
+ type: "tool_result",
1016
+ tool_use_id: item.toolCallId,
1017
+ content: item.markdown,
1018
+ ...(item.isError === true ? { is_error: true } : {}),
1019
+ });
1020
+ continue;
1021
+ }
1022
+ flushToolResults();
1023
+ if (item.kind === "player") {
1024
+ messages.push({ role: "user", content: item.text });
1025
+ continue;
1026
+ }
1027
+ if (item.kind === "prompt_delta") {
1028
+ messages.push(...promptDeltaProviderMessages(item.logicalMessages));
1029
+ continue;
1030
+ }
1031
+ if (item.providerState?.protocol === "anthropic_messages" &&
1032
+ validAnthropicContent(item.providerState.content)) {
1033
+ messages.push({
1034
+ role: "assistant",
1035
+ content: cloneProviderValue(item.providerState.content),
1036
+ });
1037
+ continue;
1038
+ }
1039
+ throw continuationError("Anthropic Messages");
1040
+ }
1041
+ flushToolResults();
1042
+ return messages;
805
1043
  }
806
1044
  function responsesAppend(request) {
807
1045
  return items(request).flatMap((item) => {
@@ -817,9 +1055,10 @@ function responsesAppend(request) {
817
1055
  output: item.markdown,
818
1056
  },
819
1057
  ];
820
- if (item.providerState?.protocol === "openai_responses")
1058
+ if (item.providerState?.protocol === "openai_responses" &&
1059
+ validResponsesOutput(item.providerState.output))
821
1060
  return cloneResponseOutput(item.providerState.output);
822
- return fallbackResponsesAssistant(item.text, item.toolCalls);
1061
+ throw continuationError("OpenAI Responses");
823
1062
  });
824
1063
  }
825
1064
  function promptDeltaProviderMessages(messages) {
@@ -828,6 +1067,77 @@ function promptDeltaProviderMessages(messages) {
828
1067
  content: renderPromptDeltaMessage(message.role, message.markdown),
829
1068
  }));
830
1069
  }
1070
+ function chatReasoningRequest(connection) {
1071
+ return {
1072
+ ...(connection.reasoningEffort === "provider_default"
1073
+ ? {}
1074
+ : { reasoning_effort: connection.reasoningEffort }),
1075
+ ...(connection.dialect !== "cliproxyapi" ||
1076
+ connection.reasoningSummary === "provider_default"
1077
+ ? {}
1078
+ : {
1079
+ // Chat Completions has no standard summary switch. CLIProxyAPI
1080
+ // deliberately accepts the OpenRouter-compatible visibility bit and
1081
+ // maps it to the target protocol without changing effort.
1082
+ reasoning: {
1083
+ exclude: connection.reasoningSummary === "none",
1084
+ },
1085
+ }),
1086
+ };
1087
+ }
1088
+ function responsesReasoningRequest(connection) {
1089
+ const reasoning = {
1090
+ ...(connection.reasoningEffort === "provider_default"
1091
+ ? {}
1092
+ : { effort: connection.reasoningEffort }),
1093
+ ...(connection.reasoningSummary === "provider_default" ||
1094
+ (connection.reasoningSummary === "none" &&
1095
+ connection.dialect === "standard")
1096
+ ? {}
1097
+ : {
1098
+ summary: connection.reasoningSummary === "none"
1099
+ ? null
1100
+ : connection.reasoningSummary,
1101
+ }),
1102
+ };
1103
+ return Object.keys(reasoning).length === 0 ? {} : { reasoning };
1104
+ }
1105
+ function anthropicReasoningRequest(connection) {
1106
+ const display = connection.reasoningSummary === "provider_default"
1107
+ ? {}
1108
+ : {
1109
+ display: connection.reasoningSummary === "auto" ? "summarized" : "omitted",
1110
+ };
1111
+ const thinking = connection.thinkingMode === "provider_default"
1112
+ ? connection.dialect === "cliproxyapi" &&
1113
+ hasCLIProxyThinkingSuffix(connection.modelId) &&
1114
+ connection.reasoningSummary !== "provider_default"
1115
+ ? {
1116
+ // CLIProxyAPI extracts visibility from a valid source thinking
1117
+ // block before the model suffix replaces its mode. The suffix is
1118
+ // still the sole mode/effort authority; adaptive is only the
1119
+ // proxy's protocol-valid summary carrier.
1120
+ thinking: { type: "adaptive", ...display },
1121
+ }
1122
+ : {}
1123
+ : connection.thinkingMode === "disabled"
1124
+ ? { thinking: { type: "disabled" } }
1125
+ : connection.thinkingMode === "adaptive"
1126
+ ? { thinking: { type: "adaptive", ...display } }
1127
+ : {
1128
+ thinking: {
1129
+ type: "enabled",
1130
+ budget_tokens: connection.thinkingBudgetTokens,
1131
+ ...display,
1132
+ },
1133
+ };
1134
+ return {
1135
+ ...thinking,
1136
+ ...(connection.reasoningEffort === "provider_default"
1137
+ ? {}
1138
+ : { output_config: { effort: connection.reasoningEffort } }),
1139
+ };
1140
+ }
831
1141
  function providerUrl(connection) {
832
1142
  const url = new URL(connection.baseUrl);
833
1143
  const suffix = connection.provider === "chat_completions"
@@ -863,6 +1173,12 @@ async function providerError(response) {
863
1173
  });
864
1174
  return new ModelHostFailureError(`Provider request failed: ${response.status} ${text}`);
865
1175
  }
1176
+ function chatSettingMessages(connection, messages) {
1177
+ const translated = messages.map(chatSettingMessage);
1178
+ if (connection.dialect !== "cliproxyapi")
1179
+ return translated;
1180
+ return attachCacheControlAtIndex(attachCacheControlToLastRole(translated, "system"), settingPrefixUserIndex(messages));
1181
+ }
866
1182
  function chatSettingMessage(message) {
867
1183
  if (message.role === "tool")
868
1184
  return {
@@ -870,58 +1186,91 @@ function chatSettingMessage(message) {
870
1186
  tool_call_id: message.toolCallId,
871
1187
  content: message.content,
872
1188
  };
1189
+ if (message.role === "assistant") {
1190
+ const state = message.providerState;
1191
+ if (state?.protocol === "chat_completions" &&
1192
+ validChatAssistantMessage(state.assistantMessage))
1193
+ return cloneProviderValue(state.assistantMessage);
1194
+ throw continuationError("Chat Completions setting improvement");
1195
+ }
873
1196
  return {
874
- role: message.role === "assistant"
875
- ? "assistant"
876
- : message.role === "system"
877
- ? "system"
878
- : "user",
1197
+ role: message.role === "system" ? "system" : "user",
879
1198
  content: message.content,
880
- ...(message.role !== "assistant" || message.reasoningContent === undefined
881
- ? {}
882
- : { reasoning_content: message.reasoningContent }),
883
- ...(message.role !== "assistant" || (message.toolCalls?.length ?? 0) === 0
884
- ? {}
885
- : {
886
- tool_calls: message.toolCalls.map((call) => ({
887
- id: call.id,
888
- type: "function",
889
- function: {
890
- name: call.name,
891
- arguments: JSON.stringify(call.arguments),
892
- },
893
- })),
894
- }),
895
1199
  };
896
1200
  }
897
- function anthropicSettingMessage(message) {
898
- if (message.role === "tool")
899
- return {
1201
+ function anthropicSettingMessages(source) {
1202
+ const messages = [];
1203
+ const cacheableUserIndex = settingPrefixUserIndex(source);
1204
+ let toolResults = [];
1205
+ const flushToolResults = () => {
1206
+ if (toolResults.length === 0)
1207
+ return;
1208
+ messages.push({ role: "user", content: toolResults });
1209
+ toolResults = [];
1210
+ };
1211
+ for (const [index, message] of source.entries()) {
1212
+ if (message.role === "system")
1213
+ continue;
1214
+ if (message.role === "tool") {
1215
+ toolResults.push({
1216
+ type: "tool_result",
1217
+ tool_use_id: message.toolCallId,
1218
+ content: message.content,
1219
+ ...(message.isError === true ? { is_error: true } : {}),
1220
+ });
1221
+ continue;
1222
+ }
1223
+ flushToolResults();
1224
+ if (message.role === "assistant") {
1225
+ if (message.providerState?.protocol !== "anthropic_messages" ||
1226
+ !validAnthropicContent(message.providerState.content))
1227
+ throw continuationError("Anthropic Messages setting improvement");
1228
+ messages.push({
1229
+ role: "assistant",
1230
+ content: cloneProviderValue(message.providerState.content),
1231
+ });
1232
+ continue;
1233
+ }
1234
+ messages.push({
900
1235
  role: "user",
901
- content: [
902
- {
903
- type: "tool_result",
904
- tool_use_id: message.toolCallId,
905
- content: message.content,
906
- },
907
- ],
908
- };
909
- if (message.role === "assistant")
910
- return {
911
- role: "assistant",
912
- content: [
913
- ...(message.content.length === 0
914
- ? []
915
- : [{ type: "text", text: message.content }]),
916
- ...(message.toolCalls ?? []).map((call) => ({
917
- type: "tool_use",
918
- id: call.id,
919
- name: call.name,
920
- input: call.arguments,
921
- })),
922
- ],
923
- };
924
- return { role: "user", content: message.content };
1236
+ content: index === cacheableUserIndex
1237
+ ? [
1238
+ {
1239
+ type: "text",
1240
+ text: message.content,
1241
+ cache_control: { type: "ephemeral" },
1242
+ },
1243
+ ]
1244
+ : message.content,
1245
+ });
1246
+ }
1247
+ flushToolResults();
1248
+ return messages;
1249
+ }
1250
+ function responsesSettingInput(connection, messages) {
1251
+ const translated = messages.flatMap(responsesSettingMessage);
1252
+ if (connection.dialect !== "cliproxyapi")
1253
+ return translated;
1254
+ return attachCacheControlAtIndex(attachCacheControlToLastRole(translated, "system"), settingPrefixUserIndex(messages));
1255
+ }
1256
+ function settingPrefixUserIndex(messages) {
1257
+ const firstAssistant = messages.findIndex(({ role }) => role === "assistant");
1258
+ const end = firstAssistant < 0 ? messages.length : firstAssistant;
1259
+ let target = -1;
1260
+ for (let index = 0; index < end; index += 1)
1261
+ if (messages[index]?.role === "user")
1262
+ target = index;
1263
+ return target;
1264
+ }
1265
+ function attachCacheControlAtIndex(messages, index) {
1266
+ const result = cloneProviderValue(messages);
1267
+ if (index < 0 || !isRecord(result[index]))
1268
+ return result;
1269
+ result[index] = {
1270
+ ...result[index],
1271
+ cache_control: { type: "ephemeral" },
1272
+ };
1273
+ return result;
925
1274
  }
926
1275
  function responsesSettingMessage(message) {
927
1276
  if (message.role === "tool")
@@ -933,14 +1282,32 @@ function responsesSettingMessage(message) {
933
1282
  },
934
1283
  ];
935
1284
  if (message.role === "assistant" &&
936
- message.providerState?.protocol === "openai_responses")
1285
+ message.providerState?.protocol === "openai_responses" &&
1286
+ validResponsesOutput(message.providerState.output))
937
1287
  return cloneResponseOutput(message.providerState.output);
938
1288
  if (message.role === "assistant")
939
- return fallbackResponsesAssistant(message.content, message.toolCalls ?? []);
1289
+ throw continuationError("OpenAI Responses setting improvement");
940
1290
  return [{ role: message.role, content: message.content }];
941
1291
  }
942
- function responsesBootstrapMessage(message) {
943
- return { role: message.role, content: providerText(message.content) };
1292
+ function responsesBootstrapMessage(message, preserveCacheControl = false) {
1293
+ return {
1294
+ role: message.role,
1295
+ content: preserveCacheControl && Array.isArray(message.content)
1296
+ ? message.content.flatMap((item) => {
1297
+ if (!isRecord(item) || typeof item.text !== "string")
1298
+ return [];
1299
+ return [
1300
+ {
1301
+ type: "input_text",
1302
+ text: item.text,
1303
+ ...(isRecord(item.cache_control)
1304
+ ? { cache_control: cloneProviderValue(item.cache_control) }
1305
+ : {}),
1306
+ },
1307
+ ];
1308
+ })
1309
+ : providerText(message.content),
1310
+ };
944
1311
  }
945
1312
  function providerText(content) {
946
1313
  if (typeof content === "string")
@@ -953,50 +1320,71 @@ function providerText(content) {
953
1320
  }
954
1321
  return JSON.stringify(content) ?? "";
955
1322
  }
956
- function fallbackResponsesAssistant(text, toolCalls) {
957
- return [
958
- ...(text.length === 0 ? [] : [{ role: "assistant", content: text }]),
959
- ...toolCalls.map((call) => ({
960
- type: "function_call",
961
- call_id: call.id,
962
- name: call.name,
963
- arguments: JSON.stringify(call.arguments),
964
- })),
965
- ];
1323
+ function validChatAssistantMessage(value) {
1324
+ if (!isRecord(value) || value.role !== "assistant")
1325
+ return false;
1326
+ if (value.content !== undefined &&
1327
+ value.content !== null &&
1328
+ typeof value.content !== "string")
1329
+ return false;
1330
+ if (value.reasoning_content !== undefined &&
1331
+ value.reasoning_content !== null &&
1332
+ typeof value.reasoning_content !== "string")
1333
+ return false;
1334
+ return (value.tool_calls === undefined ||
1335
+ (Array.isArray(value.tool_calls) &&
1336
+ value.tool_calls.every((call) => isRecord(call) &&
1337
+ typeof call.id === "string" &&
1338
+ isRecord(call.function) &&
1339
+ typeof call.function.name === "string" &&
1340
+ typeof call.function.arguments === "string")));
1341
+ }
1342
+ function validAnthropicContent(value) {
1343
+ return (Array.isArray(value) &&
1344
+ value.every((item) => {
1345
+ if (!isRecord(item) || typeof item.type !== "string")
1346
+ return false;
1347
+ if (item.type === "text")
1348
+ return typeof item.text === "string";
1349
+ if (item.type === "thinking")
1350
+ return (typeof item.thinking === "string" &&
1351
+ (item.signature === undefined || typeof item.signature === "string"));
1352
+ if (item.type === "redacted_thinking")
1353
+ return typeof item.data === "string";
1354
+ if (item.type === "tool_use")
1355
+ return (typeof item.id === "string" &&
1356
+ typeof item.name === "string" &&
1357
+ isRecord(item.input));
1358
+ return true;
1359
+ }));
1360
+ }
1361
+ function validResponsesOutput(value) {
1362
+ return (Array.isArray(value) &&
1363
+ value.every((item) => {
1364
+ if (!isRecord(item) || typeof item.type !== "string")
1365
+ return false;
1366
+ if (item.type === "message")
1367
+ return Array.isArray(item.content);
1368
+ if (item.type === "function_call")
1369
+ return (typeof item.call_id === "string" &&
1370
+ typeof item.name === "string" &&
1371
+ typeof item.arguments === "string");
1372
+ if (item.type === "reasoning")
1373
+ return ((item.summary === undefined || Array.isArray(item.summary)) &&
1374
+ (item.content === undefined ||
1375
+ item.content === null ||
1376
+ Array.isArray(item.content)) &&
1377
+ (item.encrypted_content === undefined ||
1378
+ item.encrypted_content === null ||
1379
+ typeof item.encrypted_content === "string"));
1380
+ return true;
1381
+ }));
1382
+ }
1383
+ function continuationError(protocol) {
1384
+ return new ModelHostContinuationError(`${protocol} continuation payload is missing or incompatible; start a fresh model context instead of rebuilding it from parsed text, reasoning, or tool calls`);
966
1385
  }
967
1386
  async function openAIResponsesRequest(input) {
968
- const allowedTools = input.allowedTools ?? input.tools.map((tool) => tool.name);
969
- const toolChoice = input.hasFrozenToolPolicy === true && allowedTools.length === 0
970
- ? "none"
971
- : input.hasFrozenToolPolicy === true &&
972
- input.toolStrategy === "native_allowed_subset"
973
- ? {
974
- type: "allowed_tools",
975
- mode: "auto",
976
- tools: allowedTools.map((name) => ({ type: "function", name })),
977
- }
978
- : "auto";
979
- const body = {
980
- model: input.connection.modelId,
981
- input: input.input,
982
- ...(input.tools.length === 0
983
- ? input.hasFrozenToolPolicy === true && allowedTools.length === 0
984
- ? { tool_choice: "none" }
985
- : {}
986
- : {
987
- tools: input.tools.map((tool) => ({
988
- type: "function",
989
- name: tool.name,
990
- description: tool.description,
991
- parameters: tool.parameters,
992
- })),
993
- tool_choice: toolChoice,
994
- }),
995
- max_output_tokens: input.maxOutputTokens,
996
- store: false,
997
- include: ["reasoning.encrypted_content"],
998
- stream: true,
999
- };
1387
+ const body = responsesRequestBody(input);
1000
1388
  await trace(`${input.tracePrefix}_request`, body);
1001
1389
  const bodyJson = JSON.stringify(body);
1002
1390
  const url = providerUrl(input.connection);
@@ -1011,23 +1399,18 @@ async function openAIResponsesRequest(input) {
1011
1399
  "Content-Type": "application/json",
1012
1400
  },
1013
1401
  body: bodyJson,
1402
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
1014
1403
  }, "OpenAI Responses"));
1015
1404
  if (!response.ok)
1016
1405
  throw await providerError(response);
1017
1406
  const payload = isProviderEventStream(response)
1018
- ? await providerStreamResult(response, "Responses API", (body) => aggregateResponsesModelStream(body, diagnosticDelta))
1407
+ ? await providerStreamResult(response, "Responses API", (body) => aggregateResponsesModelStream(body, diagnosticDelta), input.signal)
1019
1408
  : await providerJson(response, "Responses API");
1020
1409
  await trace(`${input.tracePrefix}_response`, payload);
1021
1410
  if (!isRecord(payload) || !Array.isArray(payload.output))
1022
1411
  throw unknownProviderResponse("Responses API");
1023
1412
  const output = cloneResponseOutput(payload.output);
1024
- if (output.some((item) => !isRecord(item) ||
1025
- typeof item.type !== "string" ||
1026
- (item.type === "message" && !Array.isArray(item.content)) ||
1027
- (item.type === "function_call" &&
1028
- (typeof item.call_id !== "string" ||
1029
- typeof item.name !== "string" ||
1030
- typeof item.arguments !== "string"))))
1413
+ if (!validResponsesOutput(output))
1031
1414
  throw unknownProviderResponse("Responses API");
1032
1415
  const text = output
1033
1416
  .flatMap((item) => {
@@ -1042,6 +1425,7 @@ async function openAIResponsesRequest(input) {
1042
1425
  : []);
1043
1426
  })
1044
1427
  .join("");
1428
+ const reasoningContent = responsesReturnedReasoning(output);
1045
1429
  const toolCalls = output.flatMap((item) => {
1046
1430
  if (!isRecord(item) ||
1047
1431
  item.type !== "function_call" ||
@@ -1057,18 +1441,61 @@ async function openAIResponsesRequest(input) {
1057
1441
  },
1058
1442
  ];
1059
1443
  });
1444
+ const stopReason = responsesStopReason(payload);
1060
1445
  return {
1061
1446
  ...(text.length === 0 ? {} : { text }),
1447
+ ...(reasoningContent.length === 0 ? {} : { reasoningContent }),
1062
1448
  providerState: {
1063
1449
  protocol: "openai_responses",
1064
1450
  output,
1065
1451
  ...(typeof payload.id === "string" ? { responseId: payload.id } : {}),
1066
1452
  },
1067
1453
  usage: normalizeResponsesUsage(payload.usage),
1454
+ ...(stopReason === undefined ? {} : { stopReason }),
1068
1455
  toolCalls,
1069
1456
  };
1070
1457
  });
1071
1458
  }
1459
+ function responsesStopReason(payload) {
1460
+ const status = typeof payload.status === "string" && payload.status !== ""
1461
+ ? payload.status
1462
+ : undefined;
1463
+ const details = isRecord(payload.incomplete_details)
1464
+ ? payload.incomplete_details
1465
+ : undefined;
1466
+ const reason = details !== undefined &&
1467
+ typeof details.reason === "string" &&
1468
+ details.reason !== ""
1469
+ ? details.reason
1470
+ : undefined;
1471
+ if (status !== undefined && reason !== undefined)
1472
+ return `${status}:${reason}`;
1473
+ return reason ?? status;
1474
+ }
1475
+ function responsesReturnedReasoning(output) {
1476
+ return output
1477
+ .flatMap((item) => {
1478
+ if (!isRecord(item) || item.type !== "reasoning")
1479
+ return [];
1480
+ const summary = Array.isArray(item.summary)
1481
+ ? item.summary.flatMap((part) => isRecord(part) &&
1482
+ part.type === "summary_text" &&
1483
+ typeof part.text === "string"
1484
+ ? [part.text]
1485
+ : [])
1486
+ : [];
1487
+ if (summary.length > 0)
1488
+ return summary;
1489
+ return Array.isArray(item.content)
1490
+ ? item.content.flatMap((part) => isRecord(part) &&
1491
+ part.type === "reasoning_text" &&
1492
+ typeof part.text === "string"
1493
+ ? [part.text]
1494
+ : [])
1495
+ : [];
1496
+ })
1497
+ .join("");
1498
+ }
1072
1499
  function cloneResponseOutput(value) {
1073
1500
  if (!Array.isArray(value))
1074
1501
  throw new Error("Responses output must be an array");
@@ -1094,7 +1521,8 @@ function normalizeChatUsage(value) {
1094
1521
  : {};
1095
1522
  const inputTokens = providerNumber(usage.prompt_tokens);
1096
1523
  const cacheReadTokens = providerNumber(promptDetails.cached_tokens);
1097
- const cacheWriteTokens = providerNumber(promptDetails.cache_write_tokens);
1524
+ const cacheWriteTokens = providerNumber(promptDetails.cache_write_tokens) ??
1525
+ providerNumber(promptDetails.cached_creation_tokens);
1098
1526
  return usageRecord({
1099
1527
  inputTokens,
1100
1528
  uncachedInputTokens: subtractCachedInput(inputTokens, cacheReadTokens, cacheWriteTokens),
@@ -1107,6 +1535,9 @@ function normalizeChatUsage(value) {
1107
1535
  }
1108
1536
  function normalizeAnthropicUsage(value) {
1109
1537
  const usage = isRecord(value) ? value : {};
1538
+ const outputDetails = isRecord(usage.output_tokens_details)
1539
+ ? usage.output_tokens_details
1540
+ : {};
1110
1541
  const uncachedInputTokens = providerNumber(usage.input_tokens);
1111
1542
  const cacheReadTokens = providerNumber(usage.cache_read_input_tokens);
1112
1543
  const cacheWriteTokens = providerNumber(usage.cache_creation_input_tokens);
@@ -1117,7 +1548,8 @@ function normalizeAnthropicUsage(value) {
1117
1548
  uncachedInputTokens,
1118
1549
  cacheReadTokens,
1119
1550
  cacheWriteTokens,
1120
- reasoningTokens: null,
1551
+ reasoningTokens: providerNumber(outputDetails.thinking_tokens) ??
1552
+ providerNumber(usage.thinking_tokens),
1121
1553
  outputTokens,
1122
1554
  totalTokens: inputTokens === null || outputTokens === null
1123
1555
  ? null
@@ -1134,7 +1566,8 @@ function normalizeResponsesUsage(value) {
1134
1566
  : {};
1135
1567
  const inputTokens = providerNumber(usage.input_tokens);
1136
1568
  const cacheReadTokens = providerNumber(inputDetails.cached_tokens);
1137
- const cacheWriteTokens = providerNumber(inputDetails.cache_write_tokens);
1569
+ const cacheWriteTokens = providerNumber(inputDetails.cache_write_tokens) ??
1570
+ providerNumber(inputDetails.cached_creation_tokens);
1138
1571
  return usageRecord({
1139
1572
  inputTokens,
1140
1573
  uncachedInputTokens: subtractCachedInput(inputTokens, cacheReadTokens, cacheWriteTokens),
@@ -1210,13 +1643,15 @@ function isProviderEventStream(response) {
1210
1643
  ?.toLowerCase()
1211
1644
  .includes("text/event-stream") === true);
1212
1645
  }
1213
- async function providerStreamResult(response, label, read) {
1646
+ async function providerStreamResult(response, label, read, signal) {
1214
1647
  if (response.body === null)
1215
1648
  throw new ModelHostOutcomeUnknownError(`${label} was dispatched but its streaming response has no body`);
1216
1649
  try {
1217
1650
  return await read(response.body);
1218
1651
  }
1219
1652
  catch (error) {
1653
+ if (signal?.aborted === true)
1654
+ throw cancelledModelRequest(error);
1220
1655
  if (error instanceof ModelHostFailureError ||
1221
1656
  error instanceof ModelHostOutcomeUnknownError)
1222
1657
  throw error;
@@ -1228,11 +1663,19 @@ async function dispatchProviderRequest(fetchImplementation, url, init, label) {
1228
1663
  return await fetchImplementation(url, init);
1229
1664
  }
1230
1665
  catch (error) {
1666
+ if (init.signal?.aborted === true)
1667
+ throw cancelledModelRequest(error);
1231
1668
  throw new ModelHostOutcomeUnknownError(`${label} request was dispatched but its outcome is unknown`, {
1232
1669
  cause: error,
1233
1670
  });
1234
1671
  }
1235
1672
  }
1673
+ function cancelledModelRequest(cause) {
1674
+ return new ModelHostCancelledError("The player cancelled the dispatched model request; its partial response was not committed and cannot be replayed.", cause === undefined ? undefined : { cause });
1675
+ }
1676
+ function signalWasAborted(signal) {
1677
+ return signal?.aborted === true;
1678
+ }
1236
1679
  function unknownProviderResponse(label) {
1237
1680
  return new ModelHostOutcomeUnknownError(`${label} was dispatched but its response shape could not be confirmed`);
1238
1681
  }