narraeon 0.1.1 → 0.2.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.
@@ -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, 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,11 +68,19 @@ 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
85
  if (this.#connection.provider === "chat_completions")
45
86
  return this.#chat(request, observer?.onDelta);
@@ -47,23 +88,29 @@ export class FileNativeModelHost {
47
88
  return this.#responses(request, observer?.onDelta);
48
89
  return this.#anthropic(request, observer?.onDelta);
49
90
  }
91
+ /** Uses the production encoders and deliberately omits credentials. */
92
+ previewRequest(request) {
93
+ assertExchangeCompatibility(this.#connection, request);
94
+ const provider = this.#connection.provider;
95
+ const body = provider === "chat_completions"
96
+ ? chatRequestBody(this.#connection, request)
97
+ : provider === "openai_responses"
98
+ ? responsesRequestBody(responsesRequestInput(this.#connection, request))
99
+ : anthropicRequestBody(this.#connection, request);
100
+ return {
101
+ provider,
102
+ method: "POST",
103
+ endpointPath: providerUrl(this.#connection).pathname,
104
+ headerNames: provider === "anthropic_messages"
105
+ ? ["Accept", "anthropic-version", "x-api-key", "Content-Type"]
106
+ : ["Accept", "Authorization", "Content-Type"],
107
+ body: cloneProviderValue(body),
108
+ };
109
+ }
50
110
  async #responses(request, onDelta) {
51
111
  return openAIResponsesRequest({
52
- connection: this.#connection,
112
+ ...responsesRequestInput(this.#connection, request),
53
113
  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
114
  tracePrefix: "responses",
68
115
  ...(this.#failureLog === undefined
69
116
  ? {}
@@ -73,37 +120,7 @@ export class FileNativeModelHost {
73
120
  });
74
121
  }
75
122
  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
- };
123
+ const body = chatRequestBody(this.#connection, request);
107
124
  const bodyJson = JSON.stringify(body);
108
125
  await trace("chat_request", body);
109
126
  const url = providerUrl(this.#connection);
@@ -135,6 +152,9 @@ export class FileNativeModelHost {
135
152
  assistantMessage: cloneProviderValue(streamed.assistantMessage),
136
153
  },
137
154
  usage: normalizeChatUsage(streamed.usage),
155
+ ...(streamed.stopReason === undefined
156
+ ? {}
157
+ : { stopReason: streamed.stopReason }),
138
158
  toolCalls: streamed.toolCalls.map((call) => ({
139
159
  id: call.id,
140
160
  name: call.name,
@@ -151,21 +171,14 @@ export class FileNativeModelHost {
151
171
  const payload = rawPayload;
152
172
  await trace("chat_response", payload);
153
173
  const message = payload.choices?.[0]?.message;
174
+ const finishReason = payload.choices?.[0]?.finish_reason;
154
175
  if (message === undefined)
155
176
  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"))))
177
+ if (finishReason !== undefined &&
178
+ finishReason !== null &&
179
+ typeof finishReason !== "string")
180
+ throw unknownProviderResponse("Chat Completions");
181
+ if (!validChatAssistantMessage(message))
169
182
  throw unknownProviderResponse("Chat Completions");
170
183
  capture?.setReasoning(message.reasoning_content ?? undefined);
171
184
  return {
@@ -178,6 +191,9 @@ export class FileNativeModelHost {
178
191
  assistantMessage: cloneProviderValue(message),
179
192
  },
180
193
  usage: normalizeChatUsage(payload.usage),
194
+ ...(typeof finishReason === "string"
195
+ ? { stopReason: finishReason }
196
+ : {}),
181
197
  toolCalls: (message.tool_calls ?? []).map((call) => ({
182
198
  id: call.id,
183
199
  name: call.function.name,
@@ -187,26 +203,7 @@ export class FileNativeModelHost {
187
203
  });
188
204
  }
189
205
  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
- };
206
+ const body = anthropicRequestBody(this.#connection, request);
210
207
  const bodyJson = JSON.stringify(body);
211
208
  await trace("anthropic_request", body);
212
209
  const url = providerUrl(this.#connection);
@@ -253,6 +250,9 @@ export class FileNativeModelHost {
253
250
  : { stopReason: streamed.stopReason }),
254
251
  },
255
252
  usage: normalizeAnthropicUsage(streamed.usage),
253
+ ...(typeof streamed.stopReason === "string"
254
+ ? { stopReason: streamed.stopReason }
255
+ : {}),
256
256
  };
257
257
  }
258
258
  const rawPayload = await providerJson(response, "Anthropic Messages");
@@ -260,14 +260,7 @@ export class FileNativeModelHost {
260
260
  throw unknownProviderResponse("Anthropic Messages");
261
261
  const payload = rawPayload;
262
262
  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)))))
263
+ if (!validAnthropicContent(content))
271
264
  throw unknownProviderResponse("Anthropic Messages");
272
265
  await trace("anthropic_response", payload);
273
266
  const reasoningContent = content
@@ -298,10 +291,21 @@ export class FileNativeModelHost {
298
291
  : { stopReason: payload.stop_reason }),
299
292
  },
300
293
  usage: normalizeAnthropicUsage(payload.usage),
294
+ ...(typeof payload.stop_reason === "string"
295
+ ? { stopReason: payload.stop_reason }
296
+ : {}),
301
297
  };
302
298
  });
303
299
  }
304
300
  }
301
+ function assertExchangeCompatibility(connection, request) {
302
+ if (request.bootstrap.provider.protocol !== connection.provider)
303
+ throw new ModelHostBindingMismatchError("The compiled prompt protocol does not match the selected model host");
304
+ if (request.bootstrap.cache.strategy !== modelCacheStrategy(connection))
305
+ throw new ModelHostBindingMismatchError("The compiled prompt cache policy does not match the selected model host");
306
+ if (request.maxOutputTokens > connection.maxOutputTokens)
307
+ throw new ModelHostBindingMismatchError("The request output limit exceeds the selected model host binding");
308
+ }
305
309
  function modelHostTrace(request) {
306
310
  return {
307
311
  requestId: request.requestId,
@@ -420,7 +424,7 @@ export class FileNativeSettingAuthorProvider {
420
424
  #locale;
421
425
  #exchange = 0;
422
426
  constructor(connection, fetchImplementation = fetch, diagnostics) {
423
- this.#connection = structuredClone(connection);
427
+ this.#connection = resolveConnection(connection);
424
428
  this.#fetch = fetchImplementation;
425
429
  this.#failureLog = diagnostics?.failureLog;
426
430
  this.#operationId = diagnostics?.operationId;
@@ -441,7 +445,13 @@ export class FileNativeSettingAuthorProvider {
441
445
  if (this.#connection.provider === "chat_completions") {
442
446
  const body = {
443
447
  model: this.#connection.modelId,
444
- messages: request.messages.map(chatSettingMessage),
448
+ ...chatReasoningRequest(this.#connection),
449
+ messages: chatSettingMessages(this.#connection, request.messages),
450
+ ...(this.#connection.dialect === "cliproxyapi"
451
+ ? {
452
+ prompt_cache_key: settingPromptCacheKey(this.#connection, request.messages, this.#operationId),
453
+ }
454
+ : {}),
445
455
  ...(tools.length === 0
446
456
  ? {}
447
457
  : {
@@ -485,6 +495,10 @@ export class FileNativeSettingAuthorProvider {
485
495
  ...(streamed.reasoningContent === ""
486
496
  ? {}
487
497
  : { reasoningContent: streamed.reasoningContent }),
498
+ providerState: {
499
+ protocol: "chat_completions",
500
+ assistantMessage: cloneProviderValue(streamed.assistantMessage),
501
+ },
488
502
  toolCalls: streamed.toolCalls.map((call) => ({
489
503
  id: call.id,
490
504
  name: call.name,
@@ -498,13 +512,14 @@ export class FileNativeSettingAuthorProvider {
498
512
  const response = await openAIResponsesRequest({
499
513
  connection: this.#connection,
500
514
  fetchImplementation: this.#fetch,
501
- input: request.messages.flatMap(responsesSettingMessage),
515
+ input: responsesSettingInput(this.#connection, request.messages),
502
516
  tools: tools.map((tool) => ({
503
517
  name: tool.name,
504
518
  description: tool.description,
505
519
  parameters: tool.schema,
506
520
  })),
507
521
  maxOutputTokens: Math.min(request.maxOutputTokens, this.#connection.maxOutputTokens),
522
+ promptCacheKey: settingPromptCacheKey(this.#connection, request.messages, this.#operationId),
508
523
  tracePrefix: "setting_responses",
509
524
  ...(this.#failureLog === undefined
510
525
  ? {}
@@ -515,6 +530,9 @@ export class FileNativeSettingAuthorProvider {
515
530
  return {
516
531
  role: "assistant",
517
532
  content: response.text ?? "",
533
+ ...(response.reasoningContent === undefined
534
+ ? {}
535
+ : { reasoningContent: response.reasoningContent }),
518
536
  providerState: response.providerState,
519
537
  toolCalls: (response.toolCalls ?? []).map((call) => ({
520
538
  ...call,
@@ -528,14 +546,18 @@ export class FileNativeSettingAuthorProvider {
528
546
  }
529
547
  const system = request.messages
530
548
  .filter(({ role }) => role === "system")
531
- .map(({ content }) => content)
532
- .join("\n\n");
549
+ .map(({ content }, index, messages) => ({
550
+ type: "text",
551
+ text: content,
552
+ ...(index === messages.length - 1
553
+ ? { cache_control: { type: "ephemeral" } }
554
+ : {}),
555
+ }));
533
556
  const body = {
534
557
  model: this.#connection.modelId,
558
+ ...anthropicReasoningRequest(this.#connection),
535
559
  ...(system.length === 0 ? {} : { system }),
536
- messages: request.messages
537
- .filter(({ role }) => role !== "system")
538
- .map(anthropicSettingMessage),
560
+ messages: anthropicSettingMessages(request.messages),
539
561
  ...(tools.length === 0
540
562
  ? {}
541
563
  : {
@@ -576,25 +598,41 @@ export class FileNativeSettingAuthorProvider {
576
598
  ...(streamed.reasoningContent === ""
577
599
  ? {}
578
600
  : { reasoningContent: streamed.reasoningContent }),
601
+ providerState: {
602
+ protocol: "anthropic_messages",
603
+ content: cloneProviderValue(streamed.providerContent),
604
+ ...(streamed.responseId === undefined
605
+ ? {}
606
+ : { responseId: streamed.responseId }),
607
+ ...(streamed.model === undefined ? {} : { model: streamed.model }),
608
+ ...(streamed.stopReason === undefined
609
+ ? {}
610
+ : { stopReason: streamed.stopReason }),
611
+ },
579
612
  toolCalls: streamed.toolCalls.map((call) => ({
580
613
  id: call.id,
581
614
  name: call.name,
582
- arguments: asArguments(parseArguments(call.arguments)),
615
+ arguments: asArguments(structuredClone(call.arguments)),
583
616
  })),
584
617
  usage: settingAuthorUsage(normalizeAnthropicUsage(streamed.usage)),
585
618
  };
586
619
  });
587
620
  }
588
621
  }
589
- // The progress projection only counts tokens, so the nullable per-field
590
- // provenance of ModelHostUsage collapses to two numbers here.
622
+ // Setting authoring uses the same complete usage projection as play. Missing
623
+ // Provider usage stays unavailable instead of being counted as zero.
591
624
  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
- };
625
+ if (usage !== undefined)
626
+ return structuredClone(usage);
627
+ return usageRecord({
628
+ inputTokens: null,
629
+ uncachedInputTokens: null,
630
+ cacheReadTokens: null,
631
+ cacheWriteTokens: null,
632
+ reasoningTokens: null,
633
+ outputTokens: null,
634
+ totalTokens: null,
635
+ });
598
636
  }
599
637
  function settingToolDefinition(name, locale = defaultAppLocale) {
600
638
  const descriptions = settingToolDescriptions[locale];
@@ -698,6 +736,126 @@ const settingToolDescriptions = {
698
736
  function items(request) {
699
737
  return request.appended;
700
738
  }
739
+ function chatRequestBody(connection, request) {
740
+ const tools = providerToolDefinitions(request);
741
+ const allowedTools = providerAllowedTools(request);
742
+ const hasPolicy = hasFrozenToolPolicy(request);
743
+ return {
744
+ model: connection.modelId,
745
+ ...chatReasoningRequest(connection),
746
+ messages: [
747
+ ...chatBootstrapMessages(connection, request),
748
+ ...chatAppend(request),
749
+ ],
750
+ ...(connection.dialect === "cliproxyapi"
751
+ ? { prompt_cache_key: promptCacheKey(connection, request) }
752
+ : {}),
753
+ ...(tools.length === 0
754
+ ? {}
755
+ : {
756
+ tools: tools.map((tool) => ({
757
+ type: "function",
758
+ function: {
759
+ name: tool.name,
760
+ description: tool.description,
761
+ parameters: tool.inputSchema,
762
+ },
763
+ })),
764
+ }),
765
+ ...(hasPolicy && allowedTools.length === 0
766
+ ? { tool_choice: "none" }
767
+ : tools.length === 0
768
+ ? {}
769
+ : { tool_choice: "auto" }),
770
+ max_tokens: request.maxOutputTokens,
771
+ stream: true,
772
+ stream_options: { include_usage: true },
773
+ };
774
+ }
775
+ function anthropicRequestBody(connection, request) {
776
+ const tools = providerToolDefinitions(request);
777
+ return {
778
+ model: connection.modelId,
779
+ ...anthropicReasoningRequest(connection),
780
+ system: request.bootstrap.provider.system,
781
+ messages: [
782
+ ...request.bootstrap.provider.messages.filter(({ role }) => role !== "system"),
783
+ ...anthropicAppend(request),
784
+ ],
785
+ ...(tools.length === 0
786
+ ? {}
787
+ : {
788
+ tools: tools.map((tool) => ({
789
+ name: tool.name,
790
+ description: tool.description,
791
+ input_schema: tool.inputSchema,
792
+ })),
793
+ }),
794
+ ...(hasFrozenToolPolicy(request) &&
795
+ providerAllowedTools(request).length === 0
796
+ ? { tool_choice: { type: "none" } }
797
+ : {}),
798
+ max_tokens: request.maxOutputTokens,
799
+ stream: true,
800
+ };
801
+ }
802
+ function responsesRequestInput(connection, request) {
803
+ return {
804
+ connection,
805
+ input: [
806
+ ...responsesBootstrapMessages(connection, request),
807
+ ...responsesAppend(request),
808
+ ],
809
+ tools: providerToolDefinitions(request).map((tool) => ({
810
+ name: tool.name,
811
+ description: tool.description,
812
+ parameters: tool.inputSchema,
813
+ })),
814
+ allowedTools: providerAllowedTools(request),
815
+ toolStrategy: providerToolStrategy(request, connection.provider),
816
+ hasFrozenToolPolicy: hasFrozenToolPolicy(request),
817
+ maxOutputTokens: request.maxOutputTokens,
818
+ promptCacheKey: promptCacheKey(connection, request),
819
+ };
820
+ }
821
+ function responsesRequestBody(input) {
822
+ const allowedTools = input.allowedTools ?? input.tools.map((tool) => tool.name);
823
+ const toolChoice = input.hasFrozenToolPolicy === true && allowedTools.length === 0
824
+ ? "none"
825
+ : input.hasFrozenToolPolicy === true &&
826
+ input.toolStrategy === "native_allowed_subset"
827
+ ? {
828
+ type: "allowed_tools",
829
+ mode: "auto",
830
+ tools: allowedTools.map((name) => ({ type: "function", name })),
831
+ }
832
+ : "auto";
833
+ return {
834
+ model: input.connection.modelId,
835
+ ...responsesReasoningRequest(resolveConnection(input.connection)),
836
+ input: input.input,
837
+ ...(input.tools.length === 0
838
+ ? input.hasFrozenToolPolicy === true && allowedTools.length === 0
839
+ ? { tool_choice: "none" }
840
+ : {}
841
+ : {
842
+ tools: input.tools.map((tool) => ({
843
+ type: "function",
844
+ name: tool.name,
845
+ description: tool.description,
846
+ parameters: tool.parameters,
847
+ })),
848
+ tool_choice: toolChoice,
849
+ }),
850
+ max_output_tokens: input.maxOutputTokens,
851
+ ...(input.promptCacheKey === undefined
852
+ ? {}
853
+ : { prompt_cache_key: input.promptCacheKey }),
854
+ store: false,
855
+ include: ["reasoning.encrypted_content"],
856
+ stream: true,
857
+ };
858
+ }
701
859
  /**
702
860
  * A frozen policy is present when the caller supplies the logical operation's
703
861
  * complete tool universe. All play lifecycles share this one ModelHost seam.
@@ -712,7 +870,26 @@ function frozenToolPolicy(request) {
712
870
  }
713
871
  function providerToolDefinitions(request) {
714
872
  const policy = frozenToolPolicy(request);
715
- return policy?.toolUniverse ?? request.tools;
873
+ return (policy?.toolUniverse ?? request.tools).map((tool) => {
874
+ if (!hasUnsupportedProviderToolSchemaRoot(tool.inputSchema))
875
+ return tool;
876
+ const portableInputSchema = portableRuntimeToolInputSchema(tool.name);
877
+ if (portableInputSchema === null)
878
+ throw new Error(`Provider tool ${tool.name} uses oneOf, allOf, or anyOf at the input schema root`);
879
+ if (hasUnsupportedProviderToolSchemaRoot(portableInputSchema))
880
+ throw new Error(`Provider tool ${tool.name} has no portable root-object input schema`);
881
+ // Released play contexts freeze their logical tool universe. Migrate only
882
+ // the Provider wire projection of the old root-union schema; Runtime keeps
883
+ // validating the original source-specific contract and the persisted
884
+ // transcript remains untouched.
885
+ return {
886
+ ...tool,
887
+ inputSchema: portableInputSchema,
888
+ };
889
+ });
890
+ }
891
+ function hasUnsupportedProviderToolSchemaRoot(schema) {
892
+ return ["oneOf", "allOf", "anyOf"].some((keyword) => Object.prototype.hasOwnProperty.call(schema, keyword));
716
893
  }
717
894
  function providerAllowedTools(request) {
718
895
  const policy = frozenToolPolicy(request);
@@ -726,82 +903,127 @@ function providerToolStrategy(request, provider) {
726
903
  return policy.toolStrategy;
727
904
  return defaultRuntimeToolDefinitionStrategy(provider);
728
905
  }
906
+ function chatBootstrapMessages(connection, request) {
907
+ const messages = request.bootstrap.provider.messages.map((message) => cloneProviderValue(message));
908
+ return connection.dialect === "cliproxyapi"
909
+ ? attachCacheControlToLastRole(messages, "system")
910
+ : messages;
911
+ }
912
+ function responsesBootstrapMessages(connection, request) {
913
+ const messages = request.bootstrap.provider.messages.map((message) => responsesBootstrapMessage(message, connection.dialect === "cliproxyapi"));
914
+ return connection.dialect === "cliproxyapi"
915
+ ? attachCacheControlToLastRole(messages, "system")
916
+ : messages;
917
+ }
918
+ function attachCacheControlToLastRole(messages, role) {
919
+ const result = cloneProviderValue(messages);
920
+ let target = -1;
921
+ for (const [index, message] of result.entries())
922
+ if (isRecord(message) && message.role === role)
923
+ target = index;
924
+ if (target < 0)
925
+ return result;
926
+ const message = result[target];
927
+ if (!isRecord(message))
928
+ return result;
929
+ result[target] = {
930
+ ...message,
931
+ cache_control: { type: "ephemeral" },
932
+ };
933
+ return result;
934
+ }
935
+ function promptCacheKey(connection, request) {
936
+ const stablePrefixScope = request.bootstrap.cache.stablePrefixFingerprint ??
937
+ createHash("sha256")
938
+ .update(JSON.stringify({
939
+ provider: request.bootstrap.provider,
940
+ tools: request.toolUniverse ?? request.tools,
941
+ toolStrategy: request.toolStrategy,
942
+ }))
943
+ .digest("hex");
944
+ // OpenAI uses this as a cache-routing hint, so equal frozen prefixes should
945
+ // share it. CLIProxyAPI additionally treats it as execution-session identity;
946
+ // there it must stay stable within one chain and isolated across chains.
947
+ const scope = connection.dialect === "cliproxyapi"
948
+ ? (request.operationId ?? stablePrefixScope)
949
+ : stablePrefixScope;
950
+ return createHash("sha256")
951
+ .update(`${connection.provider}\0${connection.dialect}\0${connection.modelId}\0${scope}`)
952
+ .digest("hex");
953
+ }
954
+ function settingPromptCacheKey(connection, messages, operationId) {
955
+ const firstAssistant = messages.findIndex(({ role }) => role === "assistant");
956
+ const initialPrefix = messages.slice(0, firstAssistant < 0 ? messages.length : firstAssistant);
957
+ const stablePrefixScope = JSON.stringify(initialPrefix);
958
+ const scope = connection.dialect === "cliproxyapi"
959
+ ? (operationId ?? stablePrefixScope)
960
+ : stablePrefixScope;
961
+ return createHash("sha256")
962
+ .update(`${connection.provider}\0${connection.dialect}\0${connection.modelId}\0${scope}`)
963
+ .digest("hex");
964
+ }
729
965
  function chatAppend(request) {
730
966
  return items(request).flatMap((item) => {
731
967
  if (item.kind === "player")
732
- return { role: "user", content: item.text };
968
+ return [{ role: "user", content: item.text }];
733
969
  if (item.kind === "prompt_delta")
734
970
  return promptDeltaProviderMessages(item.logicalMessages);
735
971
  if (item.kind === "tool")
736
- return {
737
- role: "tool",
738
- tool_call_id: item.toolCallId,
739
- content: item.markdown,
740
- };
972
+ return [
973
+ {
974
+ role: "tool",
975
+ tool_call_id: item.toolCallId,
976
+ content: item.markdown,
977
+ },
978
+ ];
741
979
  if (item.providerState?.protocol === "chat_completions") {
742
980
  const message = item.providerState.assistantMessage;
743
- if (isRecord(message))
981
+ if (validChatAssistantMessage(message))
744
982
  return [cloneProviderValue(message)];
745
983
  }
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;
984
+ throw continuationError("Chat Completions");
766
985
  });
767
986
  }
768
987
  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
- });
988
+ const messages = [];
989
+ let toolResults = [];
990
+ const flushToolResults = () => {
991
+ if (toolResults.length === 0)
992
+ return;
993
+ messages.push({ role: "user", content: toolResults });
994
+ toolResults = [];
995
+ };
996
+ for (const item of items(request)) {
997
+ if (item.kind === "tool") {
998
+ toolResults.push({
999
+ type: "tool_result",
1000
+ tool_use_id: item.toolCallId,
1001
+ content: item.markdown,
1002
+ ...(item.isError === true ? { is_error: true } : {}),
1003
+ });
1004
+ continue;
1005
+ }
1006
+ flushToolResults();
1007
+ if (item.kind === "player") {
1008
+ messages.push({ role: "user", content: item.text });
1009
+ continue;
1010
+ }
1011
+ if (item.kind === "prompt_delta") {
1012
+ messages.push(...promptDeltaProviderMessages(item.logicalMessages));
1013
+ continue;
1014
+ }
1015
+ if (item.providerState?.protocol === "anthropic_messages" &&
1016
+ validAnthropicContent(item.providerState.content)) {
1017
+ messages.push({
1018
+ role: "assistant",
1019
+ content: cloneProviderValue(item.providerState.content),
1020
+ });
1021
+ continue;
1022
+ }
1023
+ throw continuationError("Anthropic Messages");
1024
+ }
1025
+ flushToolResults();
1026
+ return messages;
805
1027
  }
806
1028
  function responsesAppend(request) {
807
1029
  return items(request).flatMap((item) => {
@@ -817,9 +1039,10 @@ function responsesAppend(request) {
817
1039
  output: item.markdown,
818
1040
  },
819
1041
  ];
820
- if (item.providerState?.protocol === "openai_responses")
1042
+ if (item.providerState?.protocol === "openai_responses" &&
1043
+ validResponsesOutput(item.providerState.output))
821
1044
  return cloneResponseOutput(item.providerState.output);
822
- return fallbackResponsesAssistant(item.text, item.toolCalls);
1045
+ throw continuationError("OpenAI Responses");
823
1046
  });
824
1047
  }
825
1048
  function promptDeltaProviderMessages(messages) {
@@ -828,6 +1051,77 @@ function promptDeltaProviderMessages(messages) {
828
1051
  content: renderPromptDeltaMessage(message.role, message.markdown),
829
1052
  }));
830
1053
  }
1054
+ function chatReasoningRequest(connection) {
1055
+ return {
1056
+ ...(connection.reasoningEffort === "provider_default"
1057
+ ? {}
1058
+ : { reasoning_effort: connection.reasoningEffort }),
1059
+ ...(connection.dialect !== "cliproxyapi" ||
1060
+ connection.reasoningSummary === "provider_default"
1061
+ ? {}
1062
+ : {
1063
+ // Chat Completions has no standard summary switch. CLIProxyAPI
1064
+ // deliberately accepts the OpenRouter-compatible visibility bit and
1065
+ // maps it to the target protocol without changing effort.
1066
+ reasoning: {
1067
+ exclude: connection.reasoningSummary === "none",
1068
+ },
1069
+ }),
1070
+ };
1071
+ }
1072
+ function responsesReasoningRequest(connection) {
1073
+ const reasoning = {
1074
+ ...(connection.reasoningEffort === "provider_default"
1075
+ ? {}
1076
+ : { effort: connection.reasoningEffort }),
1077
+ ...(connection.reasoningSummary === "provider_default" ||
1078
+ (connection.reasoningSummary === "none" &&
1079
+ connection.dialect === "standard")
1080
+ ? {}
1081
+ : {
1082
+ summary: connection.reasoningSummary === "none"
1083
+ ? null
1084
+ : connection.reasoningSummary,
1085
+ }),
1086
+ };
1087
+ return Object.keys(reasoning).length === 0 ? {} : { reasoning };
1088
+ }
1089
+ function anthropicReasoningRequest(connection) {
1090
+ const display = connection.reasoningSummary === "provider_default"
1091
+ ? {}
1092
+ : {
1093
+ display: connection.reasoningSummary === "auto" ? "summarized" : "omitted",
1094
+ };
1095
+ const thinking = connection.thinkingMode === "provider_default"
1096
+ ? connection.dialect === "cliproxyapi" &&
1097
+ hasCLIProxyThinkingSuffix(connection.modelId) &&
1098
+ connection.reasoningSummary !== "provider_default"
1099
+ ? {
1100
+ // CLIProxyAPI extracts visibility from a valid source thinking
1101
+ // block before the model suffix replaces its mode. The suffix is
1102
+ // still the sole mode/effort authority; adaptive is only the
1103
+ // proxy's protocol-valid summary carrier.
1104
+ thinking: { type: "adaptive", ...display },
1105
+ }
1106
+ : {}
1107
+ : connection.thinkingMode === "disabled"
1108
+ ? { thinking: { type: "disabled" } }
1109
+ : connection.thinkingMode === "adaptive"
1110
+ ? { thinking: { type: "adaptive", ...display } }
1111
+ : {
1112
+ thinking: {
1113
+ type: "enabled",
1114
+ budget_tokens: connection.thinkingBudgetTokens,
1115
+ ...display,
1116
+ },
1117
+ };
1118
+ return {
1119
+ ...thinking,
1120
+ ...(connection.reasoningEffort === "provider_default"
1121
+ ? {}
1122
+ : { output_config: { effort: connection.reasoningEffort } }),
1123
+ };
1124
+ }
831
1125
  function providerUrl(connection) {
832
1126
  const url = new URL(connection.baseUrl);
833
1127
  const suffix = connection.provider === "chat_completions"
@@ -863,6 +1157,12 @@ async function providerError(response) {
863
1157
  });
864
1158
  return new ModelHostFailureError(`Provider request failed: ${response.status} ${text}`);
865
1159
  }
1160
+ function chatSettingMessages(connection, messages) {
1161
+ const translated = messages.map(chatSettingMessage);
1162
+ if (connection.dialect !== "cliproxyapi")
1163
+ return translated;
1164
+ return attachCacheControlAtIndex(attachCacheControlToLastRole(translated, "system"), settingPrefixUserIndex(messages));
1165
+ }
866
1166
  function chatSettingMessage(message) {
867
1167
  if (message.role === "tool")
868
1168
  return {
@@ -870,58 +1170,91 @@ function chatSettingMessage(message) {
870
1170
  tool_call_id: message.toolCallId,
871
1171
  content: message.content,
872
1172
  };
1173
+ if (message.role === "assistant") {
1174
+ const state = message.providerState;
1175
+ if (state?.protocol === "chat_completions" &&
1176
+ validChatAssistantMessage(state.assistantMessage))
1177
+ return cloneProviderValue(state.assistantMessage);
1178
+ throw continuationError("Chat Completions setting improvement");
1179
+ }
873
1180
  return {
874
- role: message.role === "assistant"
875
- ? "assistant"
876
- : message.role === "system"
877
- ? "system"
878
- : "user",
1181
+ role: message.role === "system" ? "system" : "user",
879
1182
  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
1183
  };
896
1184
  }
897
- function anthropicSettingMessage(message) {
898
- if (message.role === "tool")
899
- return {
1185
+ function anthropicSettingMessages(source) {
1186
+ const messages = [];
1187
+ const cacheableUserIndex = settingPrefixUserIndex(source);
1188
+ let toolResults = [];
1189
+ const flushToolResults = () => {
1190
+ if (toolResults.length === 0)
1191
+ return;
1192
+ messages.push({ role: "user", content: toolResults });
1193
+ toolResults = [];
1194
+ };
1195
+ for (const [index, message] of source.entries()) {
1196
+ if (message.role === "system")
1197
+ continue;
1198
+ if (message.role === "tool") {
1199
+ toolResults.push({
1200
+ type: "tool_result",
1201
+ tool_use_id: message.toolCallId,
1202
+ content: message.content,
1203
+ ...(message.isError === true ? { is_error: true } : {}),
1204
+ });
1205
+ continue;
1206
+ }
1207
+ flushToolResults();
1208
+ if (message.role === "assistant") {
1209
+ if (message.providerState?.protocol !== "anthropic_messages" ||
1210
+ !validAnthropicContent(message.providerState.content))
1211
+ throw continuationError("Anthropic Messages setting improvement");
1212
+ messages.push({
1213
+ role: "assistant",
1214
+ content: cloneProviderValue(message.providerState.content),
1215
+ });
1216
+ continue;
1217
+ }
1218
+ messages.push({
900
1219
  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 };
1220
+ content: index === cacheableUserIndex
1221
+ ? [
1222
+ {
1223
+ type: "text",
1224
+ text: message.content,
1225
+ cache_control: { type: "ephemeral" },
1226
+ },
1227
+ ]
1228
+ : message.content,
1229
+ });
1230
+ }
1231
+ flushToolResults();
1232
+ return messages;
1233
+ }
1234
+ function responsesSettingInput(connection, messages) {
1235
+ const translated = messages.flatMap(responsesSettingMessage);
1236
+ if (connection.dialect !== "cliproxyapi")
1237
+ return translated;
1238
+ return attachCacheControlAtIndex(attachCacheControlToLastRole(translated, "system"), settingPrefixUserIndex(messages));
1239
+ }
1240
+ function settingPrefixUserIndex(messages) {
1241
+ const firstAssistant = messages.findIndex(({ role }) => role === "assistant");
1242
+ const end = firstAssistant < 0 ? messages.length : firstAssistant;
1243
+ let target = -1;
1244
+ for (let index = 0; index < end; index += 1)
1245
+ if (messages[index]?.role === "user")
1246
+ target = index;
1247
+ return target;
1248
+ }
1249
+ function attachCacheControlAtIndex(messages, index) {
1250
+ const result = cloneProviderValue(messages);
1251
+ if (index < 0 || !isRecord(result[index]))
1252
+ return result;
1253
+ result[index] = {
1254
+ ...result[index],
1255
+ cache_control: { type: "ephemeral" },
1256
+ };
1257
+ return result;
925
1258
  }
926
1259
  function responsesSettingMessage(message) {
927
1260
  if (message.role === "tool")
@@ -933,14 +1266,32 @@ function responsesSettingMessage(message) {
933
1266
  },
934
1267
  ];
935
1268
  if (message.role === "assistant" &&
936
- message.providerState?.protocol === "openai_responses")
1269
+ message.providerState?.protocol === "openai_responses" &&
1270
+ validResponsesOutput(message.providerState.output))
937
1271
  return cloneResponseOutput(message.providerState.output);
938
1272
  if (message.role === "assistant")
939
- return fallbackResponsesAssistant(message.content, message.toolCalls ?? []);
1273
+ throw continuationError("OpenAI Responses setting improvement");
940
1274
  return [{ role: message.role, content: message.content }];
941
1275
  }
942
- function responsesBootstrapMessage(message) {
943
- return { role: message.role, content: providerText(message.content) };
1276
+ function responsesBootstrapMessage(message, preserveCacheControl = false) {
1277
+ return {
1278
+ role: message.role,
1279
+ content: preserveCacheControl && Array.isArray(message.content)
1280
+ ? message.content.flatMap((item) => {
1281
+ if (!isRecord(item) || typeof item.text !== "string")
1282
+ return [];
1283
+ return [
1284
+ {
1285
+ type: "input_text",
1286
+ text: item.text,
1287
+ ...(isRecord(item.cache_control)
1288
+ ? { cache_control: cloneProviderValue(item.cache_control) }
1289
+ : {}),
1290
+ },
1291
+ ];
1292
+ })
1293
+ : providerText(message.content),
1294
+ };
944
1295
  }
945
1296
  function providerText(content) {
946
1297
  if (typeof content === "string")
@@ -953,50 +1304,71 @@ function providerText(content) {
953
1304
  }
954
1305
  return JSON.stringify(content) ?? "";
955
1306
  }
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
- ];
1307
+ function validChatAssistantMessage(value) {
1308
+ if (!isRecord(value) || value.role !== "assistant")
1309
+ return false;
1310
+ if (value.content !== undefined &&
1311
+ value.content !== null &&
1312
+ typeof value.content !== "string")
1313
+ return false;
1314
+ if (value.reasoning_content !== undefined &&
1315
+ value.reasoning_content !== null &&
1316
+ typeof value.reasoning_content !== "string")
1317
+ return false;
1318
+ return (value.tool_calls === undefined ||
1319
+ (Array.isArray(value.tool_calls) &&
1320
+ value.tool_calls.every((call) => isRecord(call) &&
1321
+ typeof call.id === "string" &&
1322
+ isRecord(call.function) &&
1323
+ typeof call.function.name === "string" &&
1324
+ typeof call.function.arguments === "string")));
1325
+ }
1326
+ function validAnthropicContent(value) {
1327
+ return (Array.isArray(value) &&
1328
+ value.every((item) => {
1329
+ if (!isRecord(item) || typeof item.type !== "string")
1330
+ return false;
1331
+ if (item.type === "text")
1332
+ return typeof item.text === "string";
1333
+ if (item.type === "thinking")
1334
+ return (typeof item.thinking === "string" &&
1335
+ (item.signature === undefined || typeof item.signature === "string"));
1336
+ if (item.type === "redacted_thinking")
1337
+ return typeof item.data === "string";
1338
+ if (item.type === "tool_use")
1339
+ return (typeof item.id === "string" &&
1340
+ typeof item.name === "string" &&
1341
+ isRecord(item.input));
1342
+ return true;
1343
+ }));
1344
+ }
1345
+ function validResponsesOutput(value) {
1346
+ return (Array.isArray(value) &&
1347
+ value.every((item) => {
1348
+ if (!isRecord(item) || typeof item.type !== "string")
1349
+ return false;
1350
+ if (item.type === "message")
1351
+ return Array.isArray(item.content);
1352
+ if (item.type === "function_call")
1353
+ return (typeof item.call_id === "string" &&
1354
+ typeof item.name === "string" &&
1355
+ typeof item.arguments === "string");
1356
+ if (item.type === "reasoning")
1357
+ return ((item.summary === undefined || Array.isArray(item.summary)) &&
1358
+ (item.content === undefined ||
1359
+ item.content === null ||
1360
+ Array.isArray(item.content)) &&
1361
+ (item.encrypted_content === undefined ||
1362
+ item.encrypted_content === null ||
1363
+ typeof item.encrypted_content === "string"));
1364
+ return true;
1365
+ }));
1366
+ }
1367
+ function continuationError(protocol) {
1368
+ 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
1369
  }
967
1370
  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
- };
1371
+ const body = responsesRequestBody(input);
1000
1372
  await trace(`${input.tracePrefix}_request`, body);
1001
1373
  const bodyJson = JSON.stringify(body);
1002
1374
  const url = providerUrl(input.connection);
@@ -1021,13 +1393,7 @@ async function openAIResponsesRequest(input) {
1021
1393
  if (!isRecord(payload) || !Array.isArray(payload.output))
1022
1394
  throw unknownProviderResponse("Responses API");
1023
1395
  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"))))
1396
+ if (!validResponsesOutput(output))
1031
1397
  throw unknownProviderResponse("Responses API");
1032
1398
  const text = output
1033
1399
  .flatMap((item) => {
@@ -1042,6 +1408,7 @@ async function openAIResponsesRequest(input) {
1042
1408
  : []);
1043
1409
  })
1044
1410
  .join("");
1411
+ const reasoningContent = responsesReturnedReasoning(output);
1045
1412
  const toolCalls = output.flatMap((item) => {
1046
1413
  if (!isRecord(item) ||
1047
1414
  item.type !== "function_call" ||
@@ -1057,18 +1424,61 @@ async function openAIResponsesRequest(input) {
1057
1424
  },
1058
1425
  ];
1059
1426
  });
1427
+ const stopReason = responsesStopReason(payload);
1060
1428
  return {
1061
1429
  ...(text.length === 0 ? {} : { text }),
1430
+ ...(reasoningContent.length === 0 ? {} : { reasoningContent }),
1062
1431
  providerState: {
1063
1432
  protocol: "openai_responses",
1064
1433
  output,
1065
1434
  ...(typeof payload.id === "string" ? { responseId: payload.id } : {}),
1066
1435
  },
1067
1436
  usage: normalizeResponsesUsage(payload.usage),
1437
+ ...(stopReason === undefined ? {} : { stopReason }),
1068
1438
  toolCalls,
1069
1439
  };
1070
1440
  });
1071
1441
  }
1442
+ function responsesStopReason(payload) {
1443
+ const status = typeof payload.status === "string" && payload.status !== ""
1444
+ ? payload.status
1445
+ : undefined;
1446
+ const details = isRecord(payload.incomplete_details)
1447
+ ? payload.incomplete_details
1448
+ : undefined;
1449
+ const reason = details !== undefined &&
1450
+ typeof details.reason === "string" &&
1451
+ details.reason !== ""
1452
+ ? details.reason
1453
+ : undefined;
1454
+ if (status !== undefined && reason !== undefined)
1455
+ return `${status}:${reason}`;
1456
+ return reason ?? status;
1457
+ }
1458
+ function responsesReturnedReasoning(output) {
1459
+ return output
1460
+ .flatMap((item) => {
1461
+ if (!isRecord(item) || item.type !== "reasoning")
1462
+ return [];
1463
+ const summary = Array.isArray(item.summary)
1464
+ ? item.summary.flatMap((part) => isRecord(part) &&
1465
+ part.type === "summary_text" &&
1466
+ typeof part.text === "string"
1467
+ ? [part.text]
1468
+ : [])
1469
+ : [];
1470
+ if (summary.length > 0)
1471
+ return summary;
1472
+ return Array.isArray(item.content)
1473
+ ? item.content.flatMap((part) => isRecord(part) &&
1474
+ part.type === "reasoning_text" &&
1475
+ typeof part.text === "string"
1476
+ ? [part.text]
1477
+ : [])
1478
+ : [];
1479
+ })
1480
+ .join("");
1481
+ }
1072
1482
  function cloneResponseOutput(value) {
1073
1483
  if (!Array.isArray(value))
1074
1484
  throw new Error("Responses output must be an array");
@@ -1094,7 +1504,8 @@ function normalizeChatUsage(value) {
1094
1504
  : {};
1095
1505
  const inputTokens = providerNumber(usage.prompt_tokens);
1096
1506
  const cacheReadTokens = providerNumber(promptDetails.cached_tokens);
1097
- const cacheWriteTokens = providerNumber(promptDetails.cache_write_tokens);
1507
+ const cacheWriteTokens = providerNumber(promptDetails.cache_write_tokens) ??
1508
+ providerNumber(promptDetails.cached_creation_tokens);
1098
1509
  return usageRecord({
1099
1510
  inputTokens,
1100
1511
  uncachedInputTokens: subtractCachedInput(inputTokens, cacheReadTokens, cacheWriteTokens),
@@ -1107,6 +1518,9 @@ function normalizeChatUsage(value) {
1107
1518
  }
1108
1519
  function normalizeAnthropicUsage(value) {
1109
1520
  const usage = isRecord(value) ? value : {};
1521
+ const outputDetails = isRecord(usage.output_tokens_details)
1522
+ ? usage.output_tokens_details
1523
+ : {};
1110
1524
  const uncachedInputTokens = providerNumber(usage.input_tokens);
1111
1525
  const cacheReadTokens = providerNumber(usage.cache_read_input_tokens);
1112
1526
  const cacheWriteTokens = providerNumber(usage.cache_creation_input_tokens);
@@ -1117,7 +1531,8 @@ function normalizeAnthropicUsage(value) {
1117
1531
  uncachedInputTokens,
1118
1532
  cacheReadTokens,
1119
1533
  cacheWriteTokens,
1120
- reasoningTokens: null,
1534
+ reasoningTokens: providerNumber(outputDetails.thinking_tokens) ??
1535
+ providerNumber(usage.thinking_tokens),
1121
1536
  outputTokens,
1122
1537
  totalTokens: inputTokens === null || outputTokens === null
1123
1538
  ? null
@@ -1134,7 +1549,8 @@ function normalizeResponsesUsage(value) {
1134
1549
  : {};
1135
1550
  const inputTokens = providerNumber(usage.input_tokens);
1136
1551
  const cacheReadTokens = providerNumber(inputDetails.cached_tokens);
1137
- const cacheWriteTokens = providerNumber(inputDetails.cache_write_tokens);
1552
+ const cacheWriteTokens = providerNumber(inputDetails.cache_write_tokens) ??
1553
+ providerNumber(inputDetails.cached_creation_tokens);
1138
1554
  return usageRecord({
1139
1555
  inputTokens,
1140
1556
  uncachedInputTokens: subtractCachedInput(inputTokens, cacheReadTokens, cacheWriteTokens),