@sayknow-cli/ai 0.3.16 → 0.4.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 (214) hide show
  1. package/package.json +23 -24
  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
@@ -967,13 +967,18 @@ export function validateToolCall(tools: Tool[], toolCall: ToolCall): ToolCall["a
967
967
  */
968
968
  export function validateToolArguments(tool: Tool, toolCall: ToolCall): ToolCall["arguments"] {
969
969
  const originalArgs = toolCall.arguments;
970
+ const rawValidation = tool.rawArgumentValidation?.(originalArgs);
971
+ if (rawValidation?.outcome === "reject") {
972
+ throw new Error(`Validation failed for tool "${toolCall.name}": raw arguments rejected before coercion`);
973
+ }
974
+ const rawArgs = rawValidation?.outcome === "accept" ? rawValidation.arguments : originalArgs;
970
975
  const ctx = getValidationContext(tool);
971
976
  const { json } = ctx;
972
977
 
973
978
  // Always normalize first — strip null and string "null" from optional
974
979
  // fields and substitute defaults. Handles LLM outputting string "null"
975
980
  // to mean "no value" even when validation would otherwise pass.
976
- let normalizedArgs: unknown = originalArgs;
981
+ let normalizedArgs: unknown = rawArgs;
977
982
  let changed = false;
978
983
  const initialNormalization = normalizeOptionalNullsForSchema(json, normalizedArgs);
979
984
  if (initialNormalization.changed) {
package/src/utils.ts CHANGED
@@ -119,22 +119,131 @@ export function sanitizeOpenAIResponsesHistoryItemsForReplay(items: Array<Record
119
119
  return sanitized ? [sanitized] : [];
120
120
  });
121
121
  }
122
+ const RESERVED_CONTROL_TOKEN_RE =
123
+ /<\|(?=(?:[A-Za-z0-9_]+|(?:system|developer|user|assistant|tool)[ \t]+to=[^\s<>|]+)\|>)/g;
124
+ /**
125
+ * Neutralize leaked OpenAI Harmony / control tokens (`<|channel|>`, `<|message|>`,
126
+ * `<|call|>`, `<|constrain|>`, `<|recipient|>`, `<|content|>`, ...) in replayed
127
+ * history text. A subagent whose tool-call channel degenerates can dump raw
128
+ * control-token scaffolding into its reply text; once that poisoned text lands in
129
+ * history the Codex / Responses endpoint rejects every subsequent request with
130
+ * `Request blocked (code=invalid_prompt)`, permanently wedging the session because
131
+ * the offending item is re-sent on each turn. Insert a zero-width space after `<`
132
+ * so the delimiter can no longer be tokenized as a reserved control token while the
133
+ * text stays human-readable.
134
+ *
135
+ * The pattern matches the two control-token shapes only, so ordinary text and pipe
136
+ * syntax is left untouched:
137
+ * - simple form `<|ident|>` — a leading run of identifier chars then `|>`; and
138
+ * - header form `<|role to=recipient|>` — a known Harmony role
139
+ * (`system`/`developer`/`user`/`assistant`/`tool`) followed by a single
140
+ * recipient assignment `to=<recipient>` whose value is an unbounded run of
141
+ * non-delimiter, non-whitespace chars (so long MCP/custom tool recipients like
142
+ * `to=functions.<long.name>` are covered).
143
+ * The header branch is deliberately scoped to the known role + `to=` recipient
144
+ * grammar rather than an arbitrary `key=value`, so request-boundary sanitization
145
+ * never rewrites non-control delimiter text such as `<|foo bar=baz|>`. A single-line
146
+ * body (no `\n`) and the required leading identifier char also leave compact
147
+ * pipe/operator syntax alone — e.g. F# `value <| f |> g` (space after `<|`),
148
+ * `sum<|a+b|>c` (punctuation body), and `<|foo bar|>` (no assignment) never match.
149
+ * The simple branch is a strict superset of the original identifier-only pattern:
150
+ * every marker the old regex caught still matches.
151
+ */
152
+ export function neutralizeReservedControlTokens(text: string): string {
153
+ if (!text.includes("<|")) return text;
154
+ return text.replace(RESERVED_CONTROL_TOKEN_RE, "<\u200b|");
155
+ }
156
+
157
+ /**
158
+ * Shape-tolerant classifier for the poisoned-history rejection that wedges
159
+ * gpt-5.6 sessions: `Request blocked (code=invalid_prompt)`. Accepts a raw
160
+ * provider error, an assistant message, or any object carrying a
161
+ * `providerCode` / `transportFailure` / `errorMessage` field, and returns true
162
+ * when the failure is the deterministic `invalid_prompt` content fault rather
163
+ * than a transient upstream error. This is the single shared contract the
164
+ * provider transports and the session-level circuit breaker key on so the
165
+ * classification is explicit (not inferred from a catch-all bucket) and
166
+ * uniformly testable across transports.
167
+ */
168
+ export function isInvalidPromptError(input: unknown): boolean {
169
+ if (!input) return false;
170
+ if (typeof input === "string") return INVALID_PROMPT_MESSAGE_RE.test(input);
171
+ if (typeof input !== "object") return false;
172
+ const value = input as {
173
+ providerCode?: unknown;
174
+ code?: unknown;
175
+ errorMessage?: unknown;
176
+ message?: unknown;
177
+ transportFailure?: { providerCode?: unknown; code?: unknown };
178
+ error?: { code?: unknown };
179
+ };
180
+ const code =
181
+ asLowerString(value.providerCode) ??
182
+ asLowerString(value.code) ??
183
+ asLowerString(value.transportFailure?.providerCode) ??
184
+ asLowerString(value.transportFailure?.code) ??
185
+ asLowerString(value.error?.code);
186
+ if (code === "invalid_prompt") return true;
187
+ const message =
188
+ typeof value.errorMessage === "string"
189
+ ? value.errorMessage
190
+ : typeof value.message === "string"
191
+ ? value.message
192
+ : undefined;
193
+ return message !== undefined && INVALID_PROMPT_MESSAGE_RE.test(message);
194
+ }
195
+
196
+ const INVALID_PROMPT_MESSAGE_RE = /code=invalid[_ -]prompt|request blocked[^\n]*invalid[_ -]prompt/i;
197
+
198
+ function asLowerString(value: unknown): string | undefined {
199
+ return typeof value === "string" ? value.toLowerCase() : undefined;
200
+ }
201
+
202
+ /**
203
+ * Neutralize leaked reserved control tokens across every string in an outgoing
204
+ * Responses `input` array. This is the request-boundary complement to the
205
+ * replay-history sanitizer: leaked Harmony markers (`<|channel|>analysis`, ...)
206
+ * can enter the payload from assistant reasoning summaries, live-converted
207
+ * message/tool-output text, or user-authored content — not just replayed
208
+ * history — and every gpt-5.6 request that carries one is rejected with
209
+ * `Request blocked (code=invalid_prompt)`. Walking every string (rather than an
210
+ * item-type allowlist) guarantees no leak source is missed as item shapes
211
+ * evolve; the zero-width-space insertion is idempotent (`<\u200b|` no longer
212
+ * matches `<|`) and keeps the text human-readable.
213
+ */
214
+ export function neutralizeResponsesInputControlTokens<T>(items: readonly T[]): T[] {
215
+ return items.map(item => deepNeutralizeReservedControlTokens(item) as T);
216
+ }
217
+
218
+ function deepNeutralizeReservedControlTokens(value: unknown): unknown {
219
+ if (typeof value === "string") return neutralizeReservedControlTokens(value);
220
+ if (Array.isArray(value)) return value.map(deepNeutralizeReservedControlTokens);
221
+ if (value && typeof value === "object") {
222
+ const out: Record<string, unknown> = {};
223
+ for (const [key, nested] of Object.entries(value)) {
224
+ out[key] = deepNeutralizeReservedControlTokens(nested);
225
+ }
226
+ return out;
227
+ }
228
+ return value;
229
+ }
230
+
122
231
  function stringifyResponsesStringParamForReplay(value: unknown): string {
123
- if (typeof value === "string") return value.toWellFormed();
232
+ if (typeof value === "string") return neutralizeReservedControlTokens(value.toWellFormed());
124
233
  try {
125
234
  const encoded = JSON.stringify(value);
126
- if (typeof encoded === "string") return encoded.toWellFormed();
235
+ if (typeof encoded === "string") return neutralizeReservedControlTokens(encoded.toWellFormed());
127
236
  } catch {
128
237
  // Fall through to String().
129
238
  }
130
- return String(value ?? "").toWellFormed();
239
+ return neutralizeReservedControlTokens(String(value ?? "").toWellFormed());
131
240
  }
132
241
 
133
242
  function normalizeResponsesMessageTextForReplay(value: unknown): string {
134
- if (typeof value === "string") return value.toWellFormed();
243
+ if (typeof value === "string") return neutralizeReservedControlTokens(value.toWellFormed());
135
244
  if (value && typeof value === "object") {
136
245
  const nestedText = (value as { text?: unknown }).text;
137
- if (typeof nestedText === "string") return nestedText.toWellFormed();
246
+ if (typeof nestedText === "string") return neutralizeReservedControlTokens(nestedText.toWellFormed());
138
247
  }
139
248
  return stringifyResponsesStringParamForReplay(value);
140
249
  }
@@ -163,7 +272,7 @@ function normalizeResponsesImageUrlForReplay(value: unknown): NormalizedResponse
163
272
  }
164
273
 
165
274
  function sanitizeResponsesMessageContentForReplay(content: unknown): unknown {
166
- if (typeof content === "string") return content.toWellFormed();
275
+ if (typeof content === "string") return neutralizeReservedControlTokens(content.toWellFormed());
167
276
  if (!Array.isArray(content)) return content;
168
277
  return content.map(part => {
169
278
  if (!part || typeof part !== "object") return part;
@@ -197,12 +306,11 @@ function sanitizeResponsesStringFieldsForReplay(item: Record<string, unknown>):
197
306
  if (item.type === "custom_tool_call" && "input" in item && typeof item.input !== "string") {
198
307
  item.input = stringifyResponsesStringParamForReplay(item.input);
199
308
  }
200
- if (
201
- (item.type === "function_call_output" || item.type === "custom_tool_call_output") &&
202
- "output" in item &&
203
- typeof item.output !== "string"
204
- ) {
205
- item.output = stringifyResponsesStringParamForReplay(item.output);
309
+ if ((item.type === "function_call_output" || item.type === "custom_tool_call_output") && "output" in item) {
310
+ item.output =
311
+ typeof item.output === "string"
312
+ ? neutralizeReservedControlTokens(item.output.toWellFormed())
313
+ : stringifyResponsesStringParamForReplay(item.output);
206
314
  }
207
315
  }
208
316
 
@@ -1,30 +0,0 @@
1
- /**
2
- * Custom API provider registry.
3
- *
4
- * Allows extensions to register streaming functions for custom API types
5
- * (e.g., "vertex-Anthropic model-api") that are not built into stream.ts.
6
- */
7
- import type { Api, AssistantMessageEventStream, Context, Model, SimpleStreamOptions, StreamOptions } from "./types";
8
- export type CustomStreamFn = (model: Model<Api>, context: Context, options?: StreamOptions) => AssistantMessageEventStream;
9
- export type CustomStreamSimpleFn = (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;
10
- export interface RegisteredCustomApi {
11
- stream: CustomStreamFn;
12
- streamSimple: CustomStreamSimpleFn;
13
- sourceId?: string;
14
- }
15
- /**
16
- * Register a custom API streaming function.
17
- */
18
- export declare function registerCustomApi(api: string, streamSimple: CustomStreamSimpleFn, sourceId?: string, stream?: CustomStreamFn): void;
19
- /**
20
- * Get a custom API provider by API identifier.
21
- */
22
- export declare function getCustomApi(api: string): RegisteredCustomApi | undefined;
23
- /**
24
- * Remove all custom APIs registered by a specific source (e.g., extension path).
25
- */
26
- export declare function unregisterCustomApis(sourceId: string): void;
27
- /**
28
- * Clear all custom API registrations.
29
- */
30
- export declare function clearCustomApis(): void;
@@ -1,67 +0,0 @@
1
- import type { AuthCredential } from "../auth-storage";
2
- import type { CredentialDisableResponse, CredentialIfAbsentUploadResponse, CredentialRefreshResponse, CredentialUploadResponse, HealthzResponse, SnapshotResponse, SnapshotStreamEvent, UsageResponse } from "./types";
3
- export interface AuthBrokerClientOptions {
4
- /** Base URL (e.g. `https://broker.tailnet:8765`). Trailing slashes are trimmed. */
5
- url: string;
6
- /** Bearer token used for everything except `healthz`. */
7
- token: string;
8
- /** Per-request timeout in milliseconds. Default 10s. */
9
- timeoutMs?: number;
10
- /** Retry connection errors this many times. Default 1. */
11
- maxRetries?: number;
12
- /** Override fetch (used in tests). Default global `fetch`. */
13
- fetchImpl?: typeof fetch;
14
- }
15
- export declare class AuthBrokerError extends Error {
16
- readonly status: number | undefined;
17
- readonly body: string | undefined;
18
- constructor(message: string, opts?: {
19
- status?: number;
20
- body?: string;
21
- cause?: unknown;
22
- });
23
- }
24
- /**
25
- * Thrown when a broker responds 404 to `GET /v1/snapshot/stream` — old
26
- * brokers that predate the SSE endpoint. Callers (`RemoteAuthCredentialStore`)
27
- * detect this sentinel to fall back to long-polling permanently.
28
- */
29
- export declare class AuthBrokerStreamUnsupportedError extends AuthBrokerError {
30
- constructor(message?: string);
31
- }
32
- export interface FetchSnapshotOptions {
33
- ifGenerationGt?: number;
34
- waitMs?: number;
35
- signal?: AbortSignal;
36
- }
37
- export type FetchSnapshotResult = {
38
- status: 200;
39
- snapshot: SnapshotResponse;
40
- generation: number;
41
- } | {
42
- status: 304;
43
- generation: number;
44
- };
45
- export declare class AuthBrokerClient {
46
- #private;
47
- constructor(opts: AuthBrokerClientOptions);
48
- healthz(signal?: AbortSignal): Promise<HealthzResponse>;
49
- fetchSnapshot(opts?: FetchSnapshotOptions): Promise<FetchSnapshotResult>;
50
- /**
51
- * Subscribe to the broker's SSE snapshot stream. The first frame is always
52
- * a full `snapshot`; subsequent frames are `entry` upserts / refreshes or
53
- * `removed` deletes. Caller controls lifecycle via `opts.signal`.
54
- *
55
- * Throws {@link AuthBrokerStreamUnsupportedError} when the broker responds
56
- * 404 — older brokers predate this endpoint and the caller should fall back
57
- * to long-polling for the remainder of its lifetime.
58
- */
59
- openSnapshotStream(opts?: {
60
- signal?: AbortSignal;
61
- }): AsyncGenerator<SnapshotStreamEvent>;
62
- fetchUsage(signal?: AbortSignal): Promise<UsageResponse>;
63
- refreshCredential(id: number, signal?: AbortSignal): Promise<CredentialRefreshResponse>;
64
- disableCredential(id: number, cause: string, signal?: AbortSignal): Promise<CredentialDisableResponse>;
65
- uploadCredential(provider: string, credential: AuthCredential, signal?: AbortSignal): Promise<CredentialUploadResponse>;
66
- uploadCredentialIfAbsent(provider: string, credential: AuthCredential, signal?: AbortSignal): Promise<CredentialIfAbsentUploadResponse>;
67
- }
@@ -1,5 +0,0 @@
1
- export * from "./client";
2
- export * from "./refresher";
3
- export * from "./remote-store";
4
- export * from "./server";
5
- export * from "./types";
@@ -1,25 +0,0 @@
1
- import type { AuthStorage } from "../auth-storage";
2
- export interface AuthBrokerRefresherOptions {
3
- storage: AuthStorage;
4
- /** Refresh credentials expiring within this window. Default 5 min. */
5
- refreshSkewMs?: number;
6
- /** Loop cadence. Default 60s. */
7
- refreshIntervalMs?: number;
8
- /** Override clock (tests). */
9
- now?: () => number;
10
- }
11
- export interface AuthBrokerRefresherSchedule {
12
- enabled: boolean;
13
- intervalMs: number;
14
- skewMs: number;
15
- nextSweepAt: number;
16
- }
17
- export declare class AuthBrokerRefresher {
18
- #private;
19
- constructor(opts: AuthBrokerRefresherOptions);
20
- start(): void;
21
- stop(): void;
22
- getSchedule(): AuthBrokerRefresherSchedule;
23
- /** Run one sweep. Exposed for tests. */
24
- tick(): Promise<void>;
25
- }
@@ -1,99 +0,0 @@
1
- import { type AuthCredential, type AuthCredentialIfAbsentResult, type AuthCredentialStore, type OAuthCredential, type StoredAuthCredential } from "../auth-storage";
2
- import type { Provider } from "../types";
3
- import type { UsageReport } from "../usage";
4
- import type { OAuthCredentials } from "../utils/oauth/types";
5
- import { type AuthBrokerClient } from "./client";
6
- import type { SnapshotResponse } from "./types";
7
- export interface RemoteAuthCredentialStoreOptions {
8
- client: AuthBrokerClient;
9
- /**
10
- * Initial snapshot. When omitted, callers must call
11
- * {@link RemoteAuthCredentialStore.refreshSnapshot} before the first read.
12
- */
13
- initialSnapshot?: SnapshotResponse;
14
- /**
15
- * Subscribe to the broker's SSE snapshot stream when available. Falls back
16
- * to long-poll permanently when the broker returns 404. Default `true`.
17
- */
18
- streamSnapshots?: boolean;
19
- }
20
- export declare class RemoteAuthCredentialStore implements AuthCredentialStore {
21
- #private;
22
- constructor(opts: RemoteAuthCredentialStoreOptions);
23
- get client(): AuthBrokerClient;
24
- get snapshot(): SnapshotResponse;
25
- /** Re-hydrate the in-memory snapshot from the broker. */
26
- refreshSnapshot(): Promise<SnapshotResponse>;
27
- listAuthCredentials(provider?: string): StoredAuthCredential[];
28
- /**
29
- * In-memory update from a successful refresh through the broker. AuthStorage
30
- * calls this after `#replaceCredentialAt`; the broker already persisted the
31
- * authoritative row, so we just mirror it.
32
- */
33
- updateAuthCredential(id: number, credential: AuthCredential): void;
34
- deleteAuthCredential(id: number, disabledCause: string): void;
35
- tryDisableAuthCredentialIfMatches(id: number, _expectedData: string, disabledCause: string): boolean;
36
- waitForFreshSnapshot(maxWaitMs: number, opts?: {
37
- signal?: AbortSignal;
38
- }): Promise<boolean>;
39
- prepareForRequest(credentialId: number, opts?: {
40
- signal?: AbortSignal;
41
- }): Promise<boolean>;
42
- markCredentialSuspect(credentialId: number, opts?: {
43
- signal?: AbortSignal;
44
- }): Promise<void>;
45
- replaceAuthCredentialsForProvider(_provider: string, _credentials: AuthCredential[]): StoredAuthCredential[];
46
- upsertAuthCredentialForProvider(_provider: string, _credential: AuthCredential): StoredAuthCredential[];
47
- upsertAuthCredentialForProviderIfAbsent(_provider: string, _credential: AuthCredential): AuthCredentialIfAbsentResult;
48
- deleteAuthCredentialsForProvider(_provider: string, _disabledCause: string): void;
49
- /**
50
- * Upsert a single credential through the broker. The broker server is the
51
- * canonical writer — see `POST /v1/credential`. The redacted snapshot
52
- * entries returned by the server replace the provider's rows in our local
53
- * snapshot, and the global snapshot is then refreshed in the background so
54
- * any concurrent peer (refresh, generation bump) stays in sync.
55
- */
56
- upsertAuthCredentialRemote(provider: string, credential: AuthCredential): Promise<StoredAuthCredential[]>;
57
- upsertAuthCredentialRemoteIfAbsent(provider: string, credential: AuthCredential): Promise<AuthCredentialIfAbsentResult>;
58
- /**
59
- * Replace-all semantics: disable every active credential for the provider,
60
- * then upload each of the new credentials. Used by API-key login so a new
61
- * key clobbers any previously stored key for the same provider.
62
- */
63
- replaceAuthCredentialsRemote(provider: string, credentials: AuthCredential[]): Promise<StoredAuthCredential[]>;
64
- /**
65
- * Logout: disable every active credential for the provider on the broker,
66
- * then drop them from the local snapshot. Refresh fetches the authoritative
67
- * post-state in the background.
68
- */
69
- deleteAuthCredentialsRemote(provider: string, disabledCause: string): Promise<void>;
70
- getCache(key: string): string | null;
71
- setCache(key: string, value: string, expiresAtSec: number): void;
72
- cleanExpiredCache(): void;
73
- deleteCachePrefix(prefix: string): void;
74
- /**
75
- * Store-level hook consumed by `AuthStorage` — routes refresh through the
76
- * broker so the actual refresh token never leaves the broker host. Returns
77
- * the broker-redacted credential with {@link REMOTE_REFRESH_SENTINEL} in
78
- * the `refresh` slot.
79
- */
80
- refreshOAuthCredential(_provider: Provider, credentialId: number, _credential: OAuthCredential, signal?: AbortSignal): Promise<OAuthCredentials>;
81
- /**
82
- * Store-level hook consumed by `AuthStorage.fetchUsageReports()` — proxies
83
- * to the broker's `/v1/usage` endpoint. The broker's egress IP isn't
84
- * rate-limited by Anthropic's per-IP `/usage` cap the way a heavy
85
- * residential laptop is, so all credentials surface every cycle.
86
- */
87
- fetchUsageReports(signal?: AbortSignal): Promise<UsageReport[] | null>;
88
- /**
89
- * Per-credential usage hook consumed by `AuthStorage.#getUsageReport`. Pulls
90
- * the aggregate broker `/v1/usage` once and serves all callers from the
91
- * same response (coalesced + cached), then matches the credential to a
92
- * report by provider + identity (accountId / email / projectId).
93
- *
94
- * The broker already aggregates with its own 30s TTL on the server side; our
95
- * 15s client TTL is below that so we usually re-use the broker's cache too.
96
- */
97
- getUsageReport(provider: Provider, credential: OAuthCredential, signal?: AbortSignal): Promise<UsageReport | null>;
98
- close(): void;
99
- }
@@ -1,32 +0,0 @@
1
- import type { AuthStorage } from "../auth-storage";
2
- export interface AuthBrokerServerOptions {
3
- /** Underlying credential storage (wraps the local SQLite store on the broker). */
4
- storage: AuthStorage;
5
- /** Listen address; accepts `host:port` or just `port`. */
6
- bind?: string;
7
- /** Accept any of these bearer tokens. Empty disables auth (loopback only). */
8
- bearerTokens: string[];
9
- /** Broker version string surfaced on `/v1/healthz`. */
10
- version?: string;
11
- /** Refresh credentials expiring within this window. Default 5 min. */
12
- refreshSkewMs?: number;
13
- /** Background refresh cadence. Default 60s. */
14
- refreshIntervalMs?: number;
15
- /** Disable the background refresher (e.g. for tests). */
16
- disableRefresher?: boolean;
17
- /**
18
- * Override SSE keepalive cadence in milliseconds for `/v1/snapshot/stream`.
19
- * Internal-only — tests use a short interval so they can assert heartbeats
20
- * without long sleeps. Default {@link DEFAULT_STREAM_KEEPALIVE_MS}.
21
- */
22
- streamKeepaliveMs?: number;
23
- }
24
- export interface AuthBrokerServerHandle {
25
- /** Bound URL (`http://host:port`). */
26
- url: string;
27
- port: number;
28
- hostname: string;
29
- close(): Promise<void>;
30
- }
31
- /** Boot the broker. Caller owns lifecycle; `handle.close()` to stop. */
32
- export declare function startAuthBroker(opts: AuthBrokerServerOptions): AuthBrokerServerHandle;
@@ -1,110 +0,0 @@
1
- /**
2
- * Wire types shared between the auth-broker server and clients.
3
- *
4
- * The broker holds OAuth refresh tokens and exposes a redacted snapshot;
5
- * clients use `access` tokens directly and call back to the broker when a
6
- * credential expires or a 401 surfaces on a supposedly-fresh credential.
7
- */
8
- import type { AuthCredential, AuthCredentialIfAbsentReason, AuthCredentialSnapshot, AuthCredentialSnapshotEntry } from "../auth-storage";
9
- import type { UsageReport } from "../usage";
10
- /** GET /v1/healthz response body. */
11
- export interface HealthzResponse {
12
- ok: boolean;
13
- version?: string;
14
- }
15
- export interface RefresherSchedule {
16
- enabled: boolean;
17
- intervalMs: number;
18
- skewMs: number;
19
- nextSweepInMs: number;
20
- }
21
- export type SnapshotEntry = AuthCredentialSnapshotEntry & {
22
- rotatesInMs: number | null;
23
- };
24
- /** GET /v1/snapshot response body. */
25
- export interface SnapshotResponse extends Omit<AuthCredentialSnapshot, "credentials"> {
26
- serverNowMs: number;
27
- refresher: RefresherSchedule;
28
- credentials: SnapshotEntry[];
29
- }
30
- /** GET /v1/usage response body — matches the local `AuthStorage.fetchUsageReports` shape. */
31
- export interface UsageResponse {
32
- generatedAt: number;
33
- reports: UsageReport[];
34
- }
35
- /** POST /v1/credential/:id/refresh response body. */
36
- export interface CredentialRefreshResponse {
37
- entry: AuthCredentialSnapshotEntry;
38
- }
39
- /** POST /v1/credential/:id/disable request body. */
40
- export interface CredentialDisableRequest {
41
- cause: string;
42
- }
43
- /** POST /v1/credential/:id/disable response body. */
44
- export interface CredentialDisableResponse {
45
- ok: boolean;
46
- }
47
- /**
48
- * POST /v1/credential request body. The OAuth `refresh` must be the *real*
49
- * refresh token (not the sentinel) — the broker is the canonical writer.
50
- */
51
- export interface CredentialUploadRequest {
52
- provider: string;
53
- credential: AuthCredential;
54
- }
55
- /** POST /v1/credential response body — redacted snapshot of the provider's rows after upsert. */
56
- export interface CredentialUploadResponse {
57
- entries: AuthCredentialSnapshotEntry[];
58
- }
59
- export interface CredentialIfAbsentUploadResponse {
60
- inserted: boolean;
61
- reason: AuthCredentialIfAbsentReason;
62
- entries: AuthCredentialSnapshotEntry[];
63
- }
64
- /**
65
- * SSE event kinds emitted on `GET /v1/snapshot/stream`. The same value is set
66
- * as the SSE `event:` name (load-bearing for clients) **and** embedded as a
67
- * `kind` field inside the JSON body so a Zod discriminated union can validate
68
- * the payload without consulting the line metadata.
69
- */
70
- export type SnapshotStreamEventKind = "snapshot" | "entry" | "removed";
71
- /** Initial frame emitted on connect — the full {@link SnapshotResponse}. */
72
- export interface SnapshotStreamSnapshotEvent extends SnapshotResponse {
73
- kind: "snapshot";
74
- }
75
- /** Single credential added/changed (upsert or refresh). */
76
- export interface SnapshotStreamEntryEvent {
77
- kind: "entry";
78
- generation: number;
79
- serverNowMs: number;
80
- refresher: RefresherSchedule;
81
- entry: SnapshotEntry;
82
- }
83
- /** Single credential disabled/deleted. */
84
- export interface SnapshotStreamRemovedEvent {
85
- kind: "removed";
86
- generation: number;
87
- serverNowMs: number;
88
- refresher: RefresherSchedule;
89
- id: number;
90
- }
91
- /** Discriminated union of every event the snapshot stream emits. */
92
- export type SnapshotStreamEvent = SnapshotStreamSnapshotEvent | SnapshotStreamEntryEvent | SnapshotStreamRemovedEvent;
93
- /**
94
- * Default bearer-protected route prefix. The broker exposes `/v1/healthz`
95
- * unauthenticated for liveness probes; everything else requires a bearer.
96
- */
97
- export declare const AUTH_BROKER_API_PREFIX = "/v1";
98
- /** Default port when none is configured. Loopback-only, no external exposure. */
99
- export declare const DEFAULT_AUTH_BROKER_BIND = "127.0.0.1:8765";
100
- /** Default broker→provider refresh skew. Refresh credentials this close to expiry. */
101
- export declare const DEFAULT_REFRESH_SKEW_MS: number;
102
- /** Default broker refresh-loop cadence. */
103
- export declare const DEFAULT_REFRESH_INTERVAL_MS = 60000;
104
- /** Keepalive cadence for `GET /v1/snapshot/stream` SSE comments. */
105
- export declare const DEFAULT_STREAM_KEEPALIVE_MS = 20000;
106
- /**
107
- * Bun.serve `idleTimeout` (seconds) used by the broker. Default Bun idle
108
- * timeout (10s) would close long-lived SSE connections between keepalives.
109
- */
110
- export declare const DEFAULT_SERVER_IDLE_TIMEOUT_S = 255;