@sayknow-cli/ai 0.3.16 → 0.4.1

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 (214) hide show
  1. package/package.json +24 -25
  2. package/src/auth-gateway/server.ts +164 -22
  3. package/src/auth-storage.ts +76 -43
  4. package/src/context-cap-policy.ts +59 -0
  5. package/src/index.ts +2 -0
  6. package/src/model-cache.ts +10 -0
  7. package/src/model-manager.ts +30 -17
  8. package/src/model-thinking.ts +14 -8
  9. package/src/providers/amazon-bedrock.ts +9 -1
  10. package/src/providers/anthropic-messages-server.ts +178 -17
  11. package/src/providers/anthropic.ts +199 -213
  12. package/src/providers/azure-openai-responses.ts +6 -1
  13. package/src/providers/google-gemini-cli.ts +26 -13
  14. package/src/providers/google-shared.ts +7 -1
  15. package/src/providers/ollama.ts +7 -1
  16. package/src/providers/openai-chat-server.ts +93 -5
  17. package/src/providers/openai-codex/response-handler.ts +11 -3
  18. package/src/providers/openai-codex-responses.ts +107 -20
  19. package/src/providers/openai-responses-server.ts +111 -36
  20. package/src/providers/openai-responses-shared.ts +71 -12
  21. package/src/providers/openai-responses.ts +26 -3
  22. package/src/providers/pi-native-client.ts +24 -12
  23. package/src/providers/pi-native-server.ts +275 -21
  24. package/src/types.ts +53 -4
  25. package/src/utils/discovery/codex.ts +3 -12
  26. package/src/utils/event-stream.ts +146 -58
  27. package/src/utils/fallback-transport.ts +269 -0
  28. package/src/utils/overflow.ts +72 -31
  29. package/src/utils/retry.ts +2 -2
  30. package/src/utils/validation.ts +6 -1
  31. package/src/utils.ts +120 -12
  32. package/dist/types/api-registry.d.ts +0 -30
  33. package/dist/types/auth-broker/client.d.ts +0 -67
  34. package/dist/types/auth-broker/index.d.ts +0 -5
  35. package/dist/types/auth-broker/refresher.d.ts +0 -25
  36. package/dist/types/auth-broker/remote-store.d.ts +0 -99
  37. package/dist/types/auth-broker/server.d.ts +0 -32
  38. package/dist/types/auth-broker/types.d.ts +0 -110
  39. package/dist/types/auth-broker/wire-schemas.d.ts +0 -443
  40. package/dist/types/auth-gateway/http.d.ts +0 -40
  41. package/dist/types/auth-gateway/index.d.ts +0 -3
  42. package/dist/types/auth-gateway/server.d.ts +0 -17
  43. package/dist/types/auth-gateway/types.d.ts +0 -115
  44. package/dist/types/auth-storage.d.ts +0 -695
  45. package/dist/types/cli.d.ts +0 -2
  46. package/dist/types/index.d.ts +0 -51
  47. package/dist/types/model-cache.d.ts +0 -17
  48. package/dist/types/model-manager.d.ts +0 -62
  49. package/dist/types/model-retirements.d.ts +0 -6
  50. package/dist/types/model-thinking.d.ts +0 -74
  51. package/dist/types/models.d.ts +0 -21
  52. package/dist/types/provider-details.d.ts +0 -24
  53. package/dist/types/provider-models/bundled-references.d.ts +0 -4
  54. package/dist/types/provider-models/descriptors.d.ts +0 -48
  55. package/dist/types/provider-models/google.d.ts +0 -20
  56. package/dist/types/provider-models/index.d.ts +0 -5
  57. package/dist/types/provider-models/ollama.d.ts +0 -7
  58. package/dist/types/provider-models/openai-compat.d.ts +0 -256
  59. package/dist/types/provider-models/special.d.ts +0 -19
  60. package/dist/types/providers/amazon-bedrock.d.ts +0 -60
  61. package/dist/types/providers/anthropic-messages-server-schema.d.ts +0 -450
  62. package/dist/types/providers/anthropic-messages-server.d.ts +0 -17
  63. package/dist/types/providers/anthropic.d.ts +0 -208
  64. package/dist/types/providers/aws-credential-config.d.ts +0 -19
  65. package/dist/types/providers/aws-credentials.d.ts +0 -43
  66. package/dist/types/providers/aws-eventstream.d.ts +0 -38
  67. package/dist/types/providers/aws-sigv4.d.ts +0 -55
  68. package/dist/types/providers/azure-openai-responses.d.ts +0 -15
  69. package/dist/types/providers/composer-discipline.d.ts +0 -26
  70. package/dist/types/providers/cursor/client-version.d.ts +0 -10
  71. package/dist/types/providers/cursor/gen/agent_pb.d.ts +0 -13022
  72. package/dist/types/providers/cursor.d.ts +0 -45
  73. package/dist/types/providers/error-message.d.ts +0 -27
  74. package/dist/types/providers/github-copilot-headers.d.ts +0 -40
  75. package/dist/types/providers/gitlab-duo.d.ts +0 -27
  76. package/dist/types/providers/google-auth.d.ts +0 -24
  77. package/dist/types/providers/google-gemini-cli.d.ts +0 -75
  78. package/dist/types/providers/google-gemini-headers.d.ts +0 -43
  79. package/dist/types/providers/google-shared.d.ts +0 -179
  80. package/dist/types/providers/google-types.d.ts +0 -138
  81. package/dist/types/providers/google-vertex.d.ts +0 -7
  82. package/dist/types/providers/google.d.ts +0 -4
  83. package/dist/types/providers/grammar.d.ts +0 -1
  84. package/dist/types/providers/kimi.d.ts +0 -27
  85. package/dist/types/providers/mock.d.ts +0 -177
  86. package/dist/types/providers/ollama.d.ts +0 -41
  87. package/dist/types/providers/openai-anthropic-shim.d.ts +0 -31
  88. package/dist/types/providers/openai-bounded-rate-limits.d.ts +0 -3
  89. package/dist/types/providers/openai-chat-server-schema.d.ts +0 -815
  90. package/dist/types/providers/openai-chat-server.d.ts +0 -16
  91. package/dist/types/providers/openai-codex/constants.d.ts +0 -26
  92. package/dist/types/providers/openai-codex/request-transformer.d.ts +0 -50
  93. package/dist/types/providers/openai-codex/response-handler.d.ts +0 -17
  94. package/dist/types/providers/openai-codex-responses.d.ts +0 -67
  95. package/dist/types/providers/openai-completions-compat.d.ts +0 -27
  96. package/dist/types/providers/openai-completions.d.ts +0 -33
  97. package/dist/types/providers/openai-request-transform.d.ts +0 -4
  98. package/dist/types/providers/openai-responses-server-schema.d.ts +0 -392
  99. package/dist/types/providers/openai-responses-server.d.ts +0 -17
  100. package/dist/types/providers/openai-responses-shared.d.ts +0 -104
  101. package/dist/types/providers/openai-responses.d.ts +0 -32
  102. package/dist/types/providers/pi-native-client.d.ts +0 -13
  103. package/dist/types/providers/pi-native-server.d.ts +0 -68
  104. package/dist/types/providers/register-builtins.d.ts +0 -31
  105. package/dist/types/providers/synthetic.d.ts +0 -26
  106. package/dist/types/providers/transform-messages.d.ts +0 -14
  107. package/dist/types/providers/vision-guard.d.ts +0 -8
  108. package/dist/types/rate-limit-utils.d.ts +0 -19
  109. package/dist/types/stream.d.ts +0 -43
  110. package/dist/types/types.d.ts +0 -831
  111. package/dist/types/usage/claude.d.ts +0 -3
  112. package/dist/types/usage/gemini.d.ts +0 -2
  113. package/dist/types/usage/github-copilot.d.ts +0 -7
  114. package/dist/types/usage/google-antigravity.d.ts +0 -2
  115. package/dist/types/usage/grok-cli.d.ts +0 -10
  116. package/dist/types/usage/kimi.d.ts +0 -2
  117. package/dist/types/usage/minimax-code.d.ts +0 -2
  118. package/dist/types/usage/openai-codex.d.ts +0 -3
  119. package/dist/types/usage/shared.d.ts +0 -1
  120. package/dist/types/usage/zai.d.ts +0 -2
  121. package/dist/types/usage.d.ts +0 -258
  122. package/dist/types/utils/abort.d.ts +0 -19
  123. package/dist/types/utils/anthropic-auth.d.ts +0 -31
  124. package/dist/types/utils/discovery/antigravity.d.ts +0 -67
  125. package/dist/types/utils/discovery/codex.d.ts +0 -38
  126. package/dist/types/utils/discovery/cursor.d.ts +0 -23
  127. package/dist/types/utils/discovery/gemini.d.ts +0 -25
  128. package/dist/types/utils/discovery/index.d.ts +0 -4
  129. package/dist/types/utils/discovery/openai-compatible.d.ts +0 -74
  130. package/dist/types/utils/event-stream.d.ts +0 -33
  131. package/dist/types/utils/fireworks-model-id.d.ts +0 -10
  132. package/dist/types/utils/foundry.d.ts +0 -1
  133. package/dist/types/utils/h2-fetch.d.ts +0 -22
  134. package/dist/types/utils/http-inspector.d.ts +0 -35
  135. package/dist/types/utils/idle-iterator.d.ts +0 -67
  136. package/dist/types/utils/json-parse.d.ts +0 -18
  137. package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +0 -18
  138. package/dist/types/utils/oauth/anthropic.d.ts +0 -22
  139. package/dist/types/utils/oauth/api-key-login.d.ts +0 -35
  140. package/dist/types/utils/oauth/api-key-validation.d.ts +0 -27
  141. package/dist/types/utils/oauth/callback-server.d.ts +0 -60
  142. package/dist/types/utils/oauth/cerebras.d.ts +0 -1
  143. package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +0 -18
  144. package/dist/types/utils/oauth/cursor.d.ts +0 -15
  145. package/dist/types/utils/oauth/deepinfra.d.ts +0 -1
  146. package/dist/types/utils/oauth/deepseek.d.ts +0 -10
  147. package/dist/types/utils/oauth/firepass.d.ts +0 -1
  148. package/dist/types/utils/oauth/fireworks.d.ts +0 -1
  149. package/dist/types/utils/oauth/fugu.d.ts +0 -1
  150. package/dist/types/utils/oauth/github-copilot.d.ts +0 -38
  151. package/dist/types/utils/oauth/gitlab-duo.d.ts +0 -3
  152. package/dist/types/utils/oauth/glm-zcode.d.ts +0 -71
  153. package/dist/types/utils/oauth/google-antigravity.d.ts +0 -11
  154. package/dist/types/utils/oauth/google-gemini-cli.d.ts +0 -10
  155. package/dist/types/utils/oauth/google-oauth-shared.d.ts +0 -28
  156. package/dist/types/utils/oauth/huggingface.d.ts +0 -19
  157. package/dist/types/utils/oauth/index.d.ts +0 -39
  158. package/dist/types/utils/oauth/kagi.d.ts +0 -17
  159. package/dist/types/utils/oauth/kilo.d.ts +0 -5
  160. package/dist/types/utils/oauth/kimi.d.ts +0 -21
  161. package/dist/types/utils/oauth/litellm.d.ts +0 -18
  162. package/dist/types/utils/oauth/lm-studio.d.ts +0 -17
  163. package/dist/types/utils/oauth/minimax-code.d.ts +0 -28
  164. package/dist/types/utils/oauth/moonshot.d.ts +0 -1
  165. package/dist/types/utils/oauth/nanogpt.d.ts +0 -1
  166. package/dist/types/utils/oauth/nvidia.d.ts +0 -18
  167. package/dist/types/utils/oauth/ollama-cloud.d.ts +0 -2
  168. package/dist/types/utils/oauth/ollama.d.ts +0 -18
  169. package/dist/types/utils/oauth/openai-codex.d.ts +0 -21
  170. package/dist/types/utils/oauth/opencode.d.ts +0 -18
  171. package/dist/types/utils/oauth/parallel.d.ts +0 -17
  172. package/dist/types/utils/oauth/perplexity.d.ts +0 -9
  173. package/dist/types/utils/oauth/pkce.d.ts +0 -8
  174. package/dist/types/utils/oauth/qianfan.d.ts +0 -17
  175. package/dist/types/utils/oauth/qwen-portal.d.ts +0 -19
  176. package/dist/types/utils/oauth/synthetic.d.ts +0 -1
  177. package/dist/types/utils/oauth/tavily.d.ts +0 -17
  178. package/dist/types/utils/oauth/together.d.ts +0 -1
  179. package/dist/types/utils/oauth/types.d.ts +0 -45
  180. package/dist/types/utils/oauth/venice.d.ts +0 -18
  181. package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +0 -18
  182. package/dist/types/utils/oauth/vllm.d.ts +0 -16
  183. package/dist/types/utils/oauth/xai.d.ts +0 -30
  184. package/dist/types/utils/oauth/xiaomi.d.ts +0 -25
  185. package/dist/types/utils/oauth/zai.d.ts +0 -18
  186. package/dist/types/utils/oauth/zenmux.d.ts +0 -1
  187. package/dist/types/utils/overflow.d.ts +0 -64
  188. package/dist/types/utils/parse-bind.d.ts +0 -23
  189. package/dist/types/utils/provider-response.d.ts +0 -3
  190. package/dist/types/utils/retry-after.d.ts +0 -3
  191. package/dist/types/utils/retry-budget.d.ts +0 -1
  192. package/dist/types/utils/retry.d.ts +0 -26
  193. package/dist/types/utils/schema/adapt.d.ts +0 -24
  194. package/dist/types/utils/schema/compatibility.d.ts +0 -30
  195. package/dist/types/utils/schema/dereference.d.ts +0 -11
  196. package/dist/types/utils/schema/draft.d.ts +0 -10
  197. package/dist/types/utils/schema/equality.d.ts +0 -4
  198. package/dist/types/utils/schema/fields.d.ts +0 -49
  199. package/dist/types/utils/schema/index.d.ts +0 -14
  200. package/dist/types/utils/schema/json-schema-validator.d.ts +0 -12
  201. package/dist/types/utils/schema/meta-validator.d.ts +0 -2
  202. package/dist/types/utils/schema/normalize.d.ts +0 -93
  203. package/dist/types/utils/schema/root-combinator.d.ts +0 -12
  204. package/dist/types/utils/schema/spill.d.ts +0 -8
  205. package/dist/types/utils/schema/stamps.d.ts +0 -25
  206. package/dist/types/utils/schema/types.d.ts +0 -4
  207. package/dist/types/utils/schema/wire.d.ts +0 -54
  208. package/dist/types/utils/schema/zod-decontaminate.d.ts +0 -31
  209. package/dist/types/utils/sse-debug.d.ts +0 -10
  210. package/dist/types/utils/tool-call-healing.d.ts +0 -71
  211. package/dist/types/utils/tool-choice-capability.d.ts +0 -41
  212. package/dist/types/utils/tool-choice.d.ts +0 -50
  213. package/dist/types/utils/validation.d.ts +0 -17
  214. package/dist/types/utils.d.ts +0 -35
@@ -95,12 +95,20 @@ let warnedReasoningSummaryLevel = false;
95
95
 
96
96
  // ─── inbound parser helpers ─────────────────────────────────────────────────
97
97
 
98
- function extractReasoningTextFromItem(item: OpenAIResponsesReasoningItem): string {
99
- // Prefer `summary[]` — mirrors real OpenAI and the openai-responses provider
100
- // which writes the surfaced reasoning summary into `summary[].text`.
101
- const fromSummary = (item.summary ?? []).map(c => c.text).join("");
102
- if (fromSummary) return fromSummary;
103
- return (item.content ?? []).map(c => c.text).join("");
98
+ function reasoningContentFromItem(
99
+ item: OpenAIResponsesReasoningItem,
100
+ ): Pick<ThinkingContent, "thinking" | "provenance" | "summaryText" | "rawText"> {
101
+ // `summary[]` is provider-displayable; `content[]` is raw reasoning. Keep
102
+ // these channels distinct so a Responses gateway round-trip cannot relabel
103
+ // raw CoT as a summary merely because summary text is absent.
104
+ const summaryText = (item.summary ?? []).map(part => part.text).join("");
105
+ const rawText = (item.content ?? []).map(part => part.text).join("");
106
+ if (summaryText && rawText) {
107
+ return { thinking: summaryText, provenance: "mixed", summaryText, rawText };
108
+ }
109
+ if (summaryText) return { thinking: summaryText, provenance: "summary", summaryText };
110
+ if (rawText) return { thinking: rawText, provenance: "raw", rawText };
111
+ return { thinking: "" };
104
112
  }
105
113
 
106
114
  type InputBlockUnion =
@@ -335,10 +343,10 @@ export function parseRequest(body: unknown, headers?: Headers): ParsedRequest {
335
343
  }
336
344
  if (effectiveType === "reasoning") {
337
345
  const reasoning = item as OpenAIResponsesReasoningItem;
338
- const text = extractReasoningTextFromItem(reasoning);
346
+ const content = reasoningContentFromItem(reasoning);
339
347
  const thinking: ThinkingContent = {
340
348
  type: "thinking",
341
- thinking: text,
349
+ ...content,
342
350
  thinkingSignature: JSON.stringify(reasoning),
343
351
  ...(reasoning.id ? { itemId: reasoning.id } : {}),
344
352
  };
@@ -538,6 +546,41 @@ function responseStatusForStopReason(message: AssistantMessage): ResponseStatus
538
546
  return "completed";
539
547
  }
540
548
 
549
+ /**
550
+ * Privacy boundary for the public Responses envelope: a reasoning item's
551
+ * `summary_text` must carry ONLY provider-displayable summary text, never raw
552
+ * chain-of-thought. A summary is published ONLY for blocks explicitly marked
553
+ * `provenance: "summary" | "mixed"` (the #2304 provenance path), sourced from
554
+ * `summaryText`. Raw-provenance AND unmarked blocks are omitted: unmarked
555
+ * `thinking` can be raw CoT from providers that stream unmarked reasoning (e.g.
556
+ * openai-completions / ollama) which the auth gateway re-encodes into the
557
+ * Responses wire format, so falling open to `part.thinking` would leak raw CoT.
558
+ */
559
+ function envelopeSummaryText(part: ThinkingContent): string | undefined {
560
+ if (part.provenance === "summary" || part.provenance === "mixed") return part.summaryText;
561
+ return undefined;
562
+ }
563
+
564
+ function envelopeSummaryParts(part: ThinkingContent): Array<{ type: "summary_text"; text: string }> {
565
+ const text = envelopeSummaryText(part);
566
+ return text ? [{ type: "summary_text", text }] : [];
567
+ }
568
+
569
+ function normalizeSummaryParts(value: unknown): Array<{ type: "summary_text"; text: string }> {
570
+ // A serialized signature's `summary` is, by the Responses protocol, provider-
571
+ // displayable summary text (raw reasoning lives in content[]/encrypted_content,
572
+ // which is stripped). Coerce to the canonical shape, keeping only well-formed
573
+ // summary_text entries. This is NOT the unsafe `part.thinking` fallback.
574
+ if (!Array.isArray(value)) return [];
575
+ const out: Array<{ type: "summary_text"; text: string }> = [];
576
+ for (const entry of value) {
577
+ if (isObj(entry) && entry.type === "summary_text" && typeof entry.text === "string") {
578
+ out.push({ type: "summary_text", text: entry.text });
579
+ }
580
+ }
581
+ return out;
582
+ }
583
+
541
584
  function buildReasoningItem(part: ThinkingContent): ReasoningOutputItem {
542
585
  const baseId = part.itemId ?? makeReasoningId();
543
586
  if (part.thinkingSignature) {
@@ -548,10 +591,15 @@ function buildReasoningItem(part: ThinkingContent): ReasoningOutputItem {
548
591
  // Preserve any extra fields (encrypted_content, …) the original carried,
549
592
  // but normalize the summary into the canonical `{type, text}[]` shape.
550
593
  const merged: Record<string, unknown> = { ...sigParsed, type: "reasoning", id };
551
- merged.summary = [{ type: "summary_text", text: part.thinking }];
552
- // `content[]` is the encrypted/raw side-channel; leave whatever was
553
- // already there. If absent, omit — real OpenAI only emits `content[]`
554
- // when `include=['reasoning.encrypted_content']` is set.
594
+ merged.summary =
595
+ part.provenance === "summary" || part.provenance === "mixed"
596
+ ? envelopeSummaryParts(part)
597
+ : normalizeSummaryParts(sigParsed.summary);
598
+ // Strip any `content[]` (raw `reasoning_text`) the serialized signature
599
+ // carried: raw chain-of-thought must never surface in the public final
600
+ // envelope (#2304 CoT boundary). Opaque top-level `encrypted_content`
601
+ // (when present) is a separate field and is preserved by the spread above.
602
+ delete merged.content;
555
603
  return merged as ReasoningOutputItem;
556
604
  }
557
605
  } catch {
@@ -561,7 +609,7 @@ function buildReasoningItem(part: ThinkingContent): ReasoningOutputItem {
561
609
  return {
562
610
  type: "reasoning",
563
611
  id: baseId,
564
- summary: [{ type: "summary_text", text: part.thinking }],
612
+ summary: envelopeSummaryParts(part),
565
613
  };
566
614
  }
567
615
 
@@ -701,7 +749,8 @@ interface OpenReasoning {
701
749
  kind: "reasoning";
702
750
  itemId: string;
703
751
  outputIndex: number;
704
- reasoningText: string;
752
+ summaryText: string;
753
+ summaryPartText: string;
705
754
  }
706
755
  interface OpenFunctionCall {
707
756
  kind: "function_call";
@@ -781,16 +830,13 @@ export function encodeStream(
781
830
  summary: [] as Array<{ type: "summary_text"; text: string }>,
782
831
  };
783
832
  emit("response.output_item.added", { output_index: outputIndex, item });
784
- // Open the summary part. Real OpenAI streams summary text in the
785
- // canonical `reasoning_summary_*` lifecycle; pi-ai's own decoder
786
- // reads `summary[].text` from the eventual `output_item.done`.
787
- emit("response.reasoning_summary_part.added", {
788
- item_id: itemId,
789
- output_index: outputIndex,
790
- summary_index: 0,
791
- part: { type: "summary_text", text: "" },
792
- });
793
- const next: OpenReasoning = { kind: "reasoning", itemId, outputIndex, reasoningText: "" };
833
+ const next: OpenReasoning = {
834
+ kind: "reasoning",
835
+ itemId,
836
+ outputIndex,
837
+ summaryText: "",
838
+ summaryPartText: "",
839
+ };
794
840
  state.open = next;
795
841
  return next;
796
842
  };
@@ -856,18 +902,21 @@ export function encodeStream(
856
902
  content: state.open.content,
857
903
  });
858
904
  } else if (state.open.kind === "reasoning") {
859
- const summary = [{ type: "summary_text" as const, text: state.open.reasoningText ?? "" }];
860
- const item = {
905
+ const summary = state.open.summaryText
906
+ ? [{ type: "summary_text" as const, text: state.open.summaryText }]
907
+ : [];
908
+ // Final reasoning envelope carries the displayable summary ONLY. Raw
909
+ // chain-of-thought is streamed live via response.reasoning_text.delta
910
+ // (the internal raw channel) and is deliberately NOT persisted into the
911
+ // terminal item's content[] — the public final envelope must never carry
912
+ // raw CoT (#2304 CoT boundary).
913
+ const item: ReasoningOutputItem = {
861
914
  type: "reasoning",
862
915
  id: state.open.itemId,
863
916
  summary,
864
917
  };
865
918
  emit("response.output_item.done", { output_index: state.open.outputIndex, item });
866
- finishedItems.push({
867
- type: "reasoning",
868
- id: state.open.itemId,
869
- summary,
870
- });
919
+ finishedItems.push(item);
871
920
  } else {
872
921
  const text = state.open.argsText ?? "";
873
922
  if (state.open.customWireName) {
@@ -1006,9 +1055,34 @@ export function encodeStream(
1006
1055
  break;
1007
1056
  }
1008
1057
  case "thinking_delta": {
1058
+ // Raw reasoning is private. The public Responses gateway emits only
1059
+ // provider-displayable reasoning_summary_* events.
1060
+ break;
1061
+ }
1062
+ case "thinking_end": {
1063
+ if (state.open?.kind !== "reasoning") break;
1064
+ // Raw reasoning is intentionally omitted from every public gateway
1065
+ // frame. Only reasoning_summary_* events populate the terminal item.
1066
+ closeOpen();
1067
+ break;
1068
+ }
1069
+ case "reasoning_summary_start": {
1009
1070
  if (state.open?.kind !== "reasoning") break;
1010
1071
  const cur: OpenReasoning = state.open;
1011
- cur.reasoningText += ev.delta;
1072
+ cur.summaryPartText = "";
1073
+ emit("response.reasoning_summary_part.added", {
1074
+ item_id: cur.itemId,
1075
+ output_index: cur.outputIndex,
1076
+ summary_index: 0,
1077
+ part: { type: "summary_text", text: "" },
1078
+ });
1079
+ break;
1080
+ }
1081
+ case "reasoning_summary_delta": {
1082
+ if (state.open?.kind !== "reasoning") break;
1083
+ const cur: OpenReasoning = state.open;
1084
+ cur.summaryPartText += ev.delta;
1085
+ cur.summaryText += ev.delta;
1012
1086
  emit("response.reasoning_summary_text.delta", {
1013
1087
  item_id: cur.itemId,
1014
1088
  output_index: cur.outputIndex,
@@ -1017,11 +1091,13 @@ export function encodeStream(
1017
1091
  });
1018
1092
  break;
1019
1093
  }
1020
- case "thinking_end": {
1094
+ case "reasoning_summary_end": {
1021
1095
  if (state.open?.kind !== "reasoning") break;
1022
1096
  const cur: OpenReasoning = state.open;
1023
- const text = ev.content ?? cur.reasoningText;
1024
- cur.reasoningText = text;
1097
+ const text = ev.content ?? cur.summaryPartText;
1098
+ // A separator-only accumulated summary (e.g. a part.done "\n\n" before any
1099
+ // real text) is treated as empty so the real end content wins.
1100
+ if (!cur.summaryText.trim()) cur.summaryText = text;
1025
1101
  emit("response.reasoning_summary_text.done", {
1026
1102
  item_id: cur.itemId,
1027
1103
  output_index: cur.outputIndex,
@@ -1034,7 +1110,6 @@ export function encodeStream(
1034
1110
  summary_index: 0,
1035
1111
  part: { type: "summary_text", text },
1036
1112
  });
1037
- closeOpen();
1038
1113
  break;
1039
1114
  }
1040
1115
  case "toolcall_start": {
@@ -376,6 +376,9 @@ export async function processResponsesStream<TApi extends Api>(
376
376
  item: StreamItem;
377
377
  block: StreamBlock;
378
378
  blockContentIndex: number;
379
+ summaryBuffer: string;
380
+ rawBuffer: string;
381
+ summaryStarted: boolean;
379
382
  }
380
383
  // Per-item argument buffer keyed on stable item identity. Multiple tool-call
381
384
  // items can stream interleaved argument deltas in one response, so a single
@@ -412,7 +415,14 @@ export async function processResponsesStream<TApi extends Api>(
412
415
  };
413
416
  const registerEntry = (item: StreamItem, block: StreamBlock, outputIndex: number | undefined): ItemEntry => {
414
417
  output.content.push(block);
415
- const entry: ItemEntry = { item, block, blockContentIndex: output.content.length - 1 };
418
+ const entry: ItemEntry = {
419
+ item,
420
+ block,
421
+ blockContentIndex: output.content.length - 1,
422
+ summaryBuffer: "",
423
+ rawBuffer: "",
424
+ summaryStarted: false,
425
+ };
416
426
  // Primary key prefers the stable item id; if the wire omits it, fall back to
417
427
  // the positional index. A synthetic key keeps the entry addressable as lastKey
418
428
  // for continuation-style non-tool events even when neither is present.
@@ -480,9 +490,13 @@ export async function processResponsesStream<TApi extends Api>(
480
490
  }
481
491
  } else if (event.type === "response.reasoning_summary_part.added") {
482
492
  const entry = resolveEntry(event.item_id, event.output_index, "always");
483
- if (entry?.item.type === "reasoning") {
493
+ if (entry?.item.type === "reasoning" && entry.block.type === "thinking") {
484
494
  entry.item.summary = entry.item.summary || [];
485
495
  entry.item.summary.push(event.part);
496
+ if (!entry.summaryStarted) {
497
+ entry.summaryStarted = true;
498
+ stream.push({ type: "reasoning_summary_start", contentIndex: entry.blockContentIndex, partial: output });
499
+ }
486
500
  }
487
501
  } else if (event.type === "response.reasoning_summary_text.delta") {
488
502
  const entry = resolveEntry(event.item_id, event.output_index, "always");
@@ -491,9 +505,10 @@ export async function processResponsesStream<TApi extends Api>(
491
505
  const lastPart = entry.item.summary[entry.item.summary.length - 1];
492
506
  if (lastPart) {
493
507
  entry.block.thinking += event.delta;
508
+ entry.summaryBuffer += event.delta;
494
509
  lastPart.text += event.delta;
495
510
  stream.push({
496
- type: "thinking_delta",
511
+ type: "reasoning_summary_delta",
497
512
  contentIndex: entry.blockContentIndex,
498
513
  delta: event.delta,
499
514
  partial: output,
@@ -507,9 +522,10 @@ export async function processResponsesStream<TApi extends Api>(
507
522
  const lastPart = entry.item.summary[entry.item.summary.length - 1];
508
523
  if (lastPart) {
509
524
  entry.block.thinking += "\n\n";
525
+ entry.summaryBuffer += "\n\n";
510
526
  lastPart.text += "\n\n";
511
527
  stream.push({
512
- type: "thinking_delta",
528
+ type: "reasoning_summary_delta",
513
529
  contentIndex: entry.blockContentIndex,
514
530
  delta: "\n\n",
515
531
  partial: output,
@@ -522,6 +538,7 @@ export async function processResponsesStream<TApi extends Api>(
522
538
  const entry = resolveEntry(event.item_id, event.output_index, "always");
523
539
  if (entry?.item.type === "reasoning" && entry.block.type === "thinking") {
524
540
  entry.block.thinking += event.delta;
541
+ entry.rawBuffer += event.delta;
525
542
  stream.push({
526
543
  type: "thinking_delta",
527
544
  contentIndex: entry.blockContentIndex,
@@ -608,12 +625,15 @@ export async function processResponsesStream<TApi extends Api>(
608
625
  options?.onOutputItemDone?.(item);
609
626
  const entry = resolveEntry(item.id, event.output_index, "never");
610
627
  if (item.type === "reasoning") {
611
- const thinking =
612
- item.summary?.length > 0
613
- ? item.summary.map(part => part.text).join("\n\n")
614
- : item.content?.[0]?.type === "reasoning_text"
615
- ? (item.content[0].text ?? "")
616
- : "";
628
+ // Prefer the streamed summary buffer only when it carries real text. When it
629
+ // holds only synthetic separators (e.g. a part.done arrived before/without any
630
+ // summary_text delta), fall back to the canonical `item.summary` from
631
+ // output_item.done so the materialized summaryText is not blank/separator-only.
632
+ const bufferSummary = entry?.summaryBuffer ?? "";
633
+ const itemSummary = item.summary?.map(part => part.text).join("\n\n") ?? "";
634
+ const summaryText = bufferSummary.trim() ? bufferSummary : itemSummary;
635
+ const rawText =
636
+ entry?.rawBuffer || (item.content?.[0]?.type === "reasoning_text" ? (item.content[0].text ?? "") : "");
617
637
  const reasoningBlock =
618
638
  entry?.block.type === "thinking"
619
639
  ? entry.block
@@ -621,14 +641,53 @@ export async function processResponsesStream<TApi extends Api>(
621
641
  | ThinkingContent
622
642
  | undefined);
623
643
  if (reasoningBlock) {
624
- reasoningBlock.thinking = thinking;
644
+ const mutable = reasoningBlock as {
645
+ provenance?: "summary" | "raw" | "mixed";
646
+ summaryText?: string;
647
+ rawText?: string;
648
+ };
649
+ if (mutable.provenance === undefined) {
650
+ if (mutable.summaryText === undefined && summaryText) mutable.summaryText = summaryText;
651
+ if (mutable.rawText === undefined && rawText) mutable.rawText = rawText;
652
+ mutable.provenance =
653
+ summaryText && rawText ? "mixed" : summaryText ? "summary" : rawText ? "raw" : undefined;
654
+ }
655
+ // Finalized display string must exclude raw CoT when a summary exists.
656
+ // Derive it from the STORED write-once provenance fields (falling back to
657
+ // this event's locals only for a first classification) so a later or
658
+ // duplicate finalization carrying only raw can never overwrite a summary/
659
+ // mixed block's safe display with raw CoT. Raw-only stays raw.
660
+ {
661
+ const effSummary = mutable.summaryText ?? summaryText;
662
+ const effRaw = mutable.rawText ?? rawText;
663
+ reasoningBlock.thinking = mutable.provenance === "raw" ? effRaw : effSummary || effRaw;
664
+ }
625
665
  reasoningBlock.thinkingSignature = JSON.stringify(item);
626
666
  const reasoningBlockIndex =
627
667
  entry?.block === reasoningBlock ? entry.blockContentIndex : output.content.indexOf(reasoningBlock);
668
+ if (summaryText) {
669
+ // If the summary text came only from the canonical item.summary (no
670
+ // streamed summary deltas/part.added), no reasoning_summary_start was
671
+ // emitted. Emit one now so consumers that open a summary on start
672
+ // (e.g. the Responses SSE encoder) don't receive an orphaned end.
673
+ if (!entry?.summaryStarted) {
674
+ stream.push({
675
+ type: "reasoning_summary_start",
676
+ contentIndex: reasoningBlockIndex,
677
+ partial: output,
678
+ });
679
+ }
680
+ stream.push({
681
+ type: "reasoning_summary_end",
682
+ contentIndex: reasoningBlockIndex,
683
+ content: summaryText,
684
+ partial: output,
685
+ });
686
+ }
628
687
  stream.push({
629
688
  type: "thinking_end",
630
689
  contentIndex: reasoningBlockIndex,
631
- content: thinking,
690
+ content: reasoningBlock.thinking,
632
691
  partial: output,
633
692
  });
634
693
  }
@@ -33,12 +33,15 @@ import {
33
33
  createOpenAIResponsesHistoryPayload,
34
34
  getOpenAIResponsesHistoryItems,
35
35
  getOpenAIResponsesHistoryPayload,
36
+ isInvalidPromptError,
37
+ neutralizeResponsesInputControlTokens,
36
38
  normalizeSystemPrompts,
37
39
  resolveCacheRetention,
38
40
  sanitizeOpenAIResponsesHistoryItemsForReplay,
39
41
  } from "../utils";
40
42
  import { createAbortSourceTracker } from "../utils/abort";
41
43
  import { AssistantMessageEventStream } from "../utils/event-stream";
44
+ import { transportFailureFacts } from "../utils/fallback-transport";
42
45
  import { finalizeErrorMessage, type RawHttpRequestDump, rewriteCopilotError } from "../utils/http-inspector";
43
46
  import {
44
47
  createWatchdog,
@@ -298,9 +301,12 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
298
301
  await notifyProviderResponse(options, response, model, request_id);
299
302
  return data;
300
303
  },
301
- { provider: model.provider, signal: requestSignal },
304
+ { provider: model.provider, signal: requestSignal, fallbackManaged: options?.fallbackManaged },
302
305
  ).catch(async error => {
303
- if (!isForcedToolChoiceUnsupportedError(error, isForcedOpenAIResponsesToolChoice(params.tool_choice))) {
306
+ if (
307
+ options?.fallbackManaged ||
308
+ !isForcedToolChoiceUnsupportedError(error, isForcedOpenAIResponsesToolChoice(params.tool_choice))
309
+ ) {
304
310
  throw error;
305
311
  }
306
312
  const reason = await finalizeErrorMessage(error, rawRequestDump);
@@ -379,8 +385,25 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
379
385
  const firstEventTimeoutError = abortTracker.getLocalAbortReason();
380
386
  output.stopReason = abortTracker.wasCallerAbort() ? "aborted" : "error";
381
387
  output.errorStatus = extractHttpStatusFromError(error);
388
+ output.transportFailure = transportFailureFacts(error);
382
389
  output.errorMessage = firstEventTimeoutError?.message ?? (await finalizeErrorMessage(error, rawRequestDump));
383
390
  output.errorMessage = rewriteCopilotError(output.errorMessage, error, model.provider);
391
+ // Explicitly mark the poisoned-history rejection so the shared
392
+ // `invalid_prompt` contract is present even when the SDK error surfaces
393
+ // only a message (no structured code). This keeps the responses
394
+ // transport's classification uniform with the codex transport's
395
+ // non-retryable event set and lets the session-level circuit breaker
396
+ // key on one durable marker instead of per-transport string matching.
397
+ if (
398
+ output.stopReason === "error" &&
399
+ !output.transportFailure?.providerCode &&
400
+ (isInvalidPromptError(error) || isInvalidPromptError(output.errorMessage))
401
+ ) {
402
+ output.transportFailure = {
403
+ ...(output.transportFailure ?? { kind: "transport" }),
404
+ providerCode: "invalid_prompt",
405
+ };
406
+ }
384
407
  output.duration = Date.now() - startTime;
385
408
  if (firstTokenTime) output.ttft = firstTokenTime - startTime;
386
409
  stream.push({ type: "error", reason: output.stopReason, error: output });
@@ -493,7 +516,7 @@ function buildParams(
493
516
  strictResponsesPairing,
494
517
  providerSessionState,
495
518
  );
496
- const messages: ResponseInput = [...conversationMessages];
519
+ const messages: ResponseInput = neutralizeResponsesInputControlTokens(conversationMessages);
497
520
 
498
521
  const systemPrompts = normalizeSystemPrompts(context.systemPrompt);
499
522
  if (isComposerHarnessModel(model.id)) {
@@ -11,8 +11,8 @@
11
11
  *
12
12
  * Activated when a {@link Model} has `transport: "pi-native"` set; the
13
13
  * dispatch hook lives in `streamSimple()` (see `../stream.ts`). Used by
14
- * containerized skc deployments (e.g. roboskc slots) that route every LLM call
15
- * through a credential-holding sidecar so the slot itself stays credential-free.
14
+ * containerized SKC deployments that route every LLM call through a
15
+ * credential-holding sidecar so the container stays credential-free.
16
16
  */
17
17
  import { readSseJson } from "@sayknow-cli/utils";
18
18
  import type {
@@ -44,6 +44,7 @@ const NON_WIRE_KEYS = new Set<keyof SimpleStreamOptions>([
44
44
  "cursorExecHandlers",
45
45
  "cursorOnToolResult",
46
46
  "providerSessionState",
47
+ "fallbackAttempt",
47
48
  ]);
48
49
 
49
50
  function buildWireOptions(options: SimpleStreamOptions | undefined): Record<string, unknown> {
@@ -70,15 +71,27 @@ async function decodeGatewayError(response: Response): Promise<Error> {
70
71
  if (typeof err === "object" && err !== null) {
71
72
  const message = (err as { message?: unknown }).message;
72
73
  const type = (err as { type?: unknown }).type;
74
+ const code = (err as { code?: unknown }).code;
73
75
  const out = new Error(typeof message === "string" ? message : `auth-gateway ${status}`);
74
- (out as { status?: number; type?: string }).status = status;
75
- if (typeof type === "string") (out as { type?: string }).type = type;
76
+ const transportError = out as Error & {
77
+ status?: number;
78
+ type?: string;
79
+ providerCode?: string;
80
+ headers?: Headers;
81
+ };
82
+ transportError.status = status;
83
+ transportError.headers = response.headers;
84
+ if (typeof type === "string") transportError.type = type;
85
+ if (typeof code === "string") transportError.providerCode = code;
86
+ else if (typeof type === "string") transportError.providerCode = type;
76
87
  return out;
77
88
  }
78
89
  }
79
90
  const text = typeof body === "string" ? body : JSON.stringify(body);
80
91
  const err = new Error(`auth-gateway ${status}: ${text || response.statusText}`);
81
- (err as { status?: number }).status = status;
92
+ const transportError = err as Error & { status?: number; headers?: Headers };
93
+ transportError.status = status;
94
+ transportError.headers = response.headers;
82
95
  return err;
83
96
  }
84
97
 
@@ -180,19 +193,18 @@ export function streamPiNative<TApi extends Api>(
180
193
  }
181
194
 
182
195
  if (!sawTerminal) {
183
- // SSE closed before a terminal event reached us — synthesize one
184
- // so awaiters of `.result()` resolve instead of hanging forever.
185
- // Matches the gateway's own defensive fallback in
186
- // `pi-native-server.encodeStream`.
187
196
  const aborted = signal?.aborted === true;
188
- const partial = makeSyntheticAssistant(model as Model<Api>);
189
197
  if (aborted) {
198
+ const partial = makeSyntheticAssistant(model as Model<Api>);
190
199
  partial.stopReason = "aborted";
191
200
  partial.errorMessage = "stream closed without terminal event";
192
201
  stream.push({ type: "error", reason: "aborted", error: partial });
193
202
  } else {
194
- partial.stopReason = "stop";
195
- stream.push({ type: "done", reason: "stop", message: partial });
203
+ const error = Object.assign(new Error("pi-native SSE stream closed without terminal event"), {
204
+ status: 502,
205
+ headers: response.headers,
206
+ });
207
+ stream.fail(error);
196
208
  }
197
209
  }
198
210
  stream.end();