@vib-rato/ai 0.17.2 → 0.18.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.18.0] - 2026-09-08
6
+
7
+ - Transport failure facts now retain the Vibrato Usage Gateway's `x-vug-daily-limit`, `x-vug-daily-used`, `x-vug-daily-remaining`, `x-vug-daily-reset`, `x-vug-queue-depth`, `x-vug-inflight`, and `x-vug-queued-ms` headers (raw string values, allowlisted like the existing `retry-after`/`retry-after-ms` entries), and classify a gateway `daily_token_limit` provider code as a `quota` fallback trigger instead of `rate_limit`. Previously the gateway's daily-limit 429 was treated as an ordinary rate limit with a 12-hour retry window, and the `x-vug-daily-*` headers were dropped before reaching consumers. A code-less 429 still classifies as `rate_limit`, and 503 `queue_timeout`/`queue_full` still classify as `server`.
8
+ - OpenAI Codex HTTP failures with an empty response body and empty status text now include the HTTP status in the surfaced message instead of the opaque `Request failed`, while structured provider errors retain their existing messages and classification.
9
+ - OAuth callback responses now serialize provider-controlled result fields as safe JSON script data, preventing callback values from terminating the embedded state element while preserving exact JSON values and callback behavior.
10
+ - Bundled `anthropic/claude-fable-5-1` (Claude Fable 5.1): 1M context, 128K output, `anthropic-adaptive` thinking clamped to `xhigh` on the Messages API, matching the Fable 5 effort ladder exactly. The model is served only by Anthropic's first-party API and requires a Claude Code compatibility version of at least `2.1.251`; older fingerprints are answered with an HTTP 400 `claude_code_version_too_old`, which the `2.1.257` attribution bump clears. Verified live: `2.1.219` returns the 400, `2.1.257` returns 200, `low`/`high`/`xhigh` all stream, and adaptive thinking returns visible summarized content rather than signature-only billed thinking.
11
+ - OpenAI Codex WebSockets are now opt-in on Windows. Bun 1.4.0 can segfault in its Windows TLS WebSocket handshake, so the automatic model preference uses SSE on Windows while the explicit WebSocket setting remains available for operators who have a newer safe runtime.
12
+ ## [0.17.3] - 2026-09-03
13
+
5
14
  ## [0.17.2] - 2026-09-03
6
15
 
7
16
  ## [0.17.1] - 2026-09-03
@@ -107,7 +107,7 @@ export interface CpaToolAliasRestoreFailure {
107
107
  */
108
108
  export declare function parseCpaToolAliasRestoreFailure(error: unknown): CpaToolAliasRestoreFailure | undefined;
109
109
  export declare function isCpaToolAliasRestoreFailure(error: unknown): boolean;
110
- export declare const claudeCodeVersion = "2.1.219";
110
+ export declare const claudeCodeVersion = "2.1.257";
111
111
  export declare const claudeCodeEntrypoint = "sdk-cli";
112
112
  export declare const claudeToolPrefix: string;
113
113
  export declare const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
@@ -25,6 +25,12 @@ export declare function normalizeCodexToolChoice(choice: ToolChoice | undefined,
25
25
  export declare function formatCodexUserAgent(platform: string, release: string, arch: string): string;
26
26
  export declare const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses">;
27
27
  export declare function prewarmOpenAICodexResponses(model: Model<"openai-codex-responses">, options?: Pick<OpenAICodexResponsesOptions, "apiKey" | "headers" | "sessionId" | "signal" | "preferWebsockets" | "providerSessionState">): Promise<void>;
28
+ /**
29
+ * Bun 1.4.0's Windows WebSocket client can segfault during TLS handshakes.
30
+ * Keep the model's websocket preference available on other platforms, while
31
+ * requiring an explicit opt-in on Windows until the bundled runtime is fixed.
32
+ */
33
+ export declare function isCodexWebSocketSafeByDefault(platform?: NodeJS.Platform): boolean;
28
34
  export interface OpenAICodexTransportDetails {
29
35
  websocketPreferred: boolean;
30
36
  lastTransport?: CodexTransport;
@@ -997,6 +997,18 @@ export interface LongContextPricing {
997
997
  threshold: number;
998
998
  cost: ModelCost;
999
999
  }
1000
+ /**
1001
+ * Capability facts the serving endpoint advertised for a discovered model
1002
+ * (the `vibrato` object on its OpenAI-style models-list entry). Only fields
1003
+ * that describe what the model can do are carried: a hint can never redirect a
1004
+ * request or change its credentials.
1005
+ */
1006
+ export interface DiscoveredModelHint {
1007
+ name?: string;
1008
+ reasoning?: boolean;
1009
+ thinking?: ThinkingConfig;
1010
+ compat?: Pick<OpenAICompat, "supportsReasoningEffort" | "reasoningContentField" | "thinkingFormat">;
1011
+ }
1000
1012
  export interface Model<TApi extends Api = any> {
1001
1013
  id: string;
1002
1014
  name: string;
@@ -1022,6 +1034,12 @@ export interface Model<TApi extends Api = any> {
1022
1034
  maxTokens: number;
1023
1035
  /** Explicit models.yml/model-override authority; absent keeps the safe transport default. */
1024
1036
  maxTokensSource?: ModelMaxTokensSource;
1037
+ /**
1038
+ * What the endpoint advertised about this model at discovery time. Kept on
1039
+ * the model so a cached discovery restores it; the registry applies it after
1040
+ * provider overrides and before the user's model overrides.
1041
+ */
1042
+ discoveryHint?: DiscoveredModelHint;
1025
1043
  headers?: Record<string, string>;
1026
1044
  /**
1027
1045
  * Streaming transport override. When `"pi-native"`, `streamSimple` routes
@@ -35,8 +35,9 @@ export type TransportHeaders = Headers | Record<string, string | undefined>;
35
35
  * Structured facts from an upstream HTTP or transport failure. Retry decisions
36
36
  * must use these facts rather than provider- or application-owned error text.
37
37
  *
38
- * `headers` is always a plain record limited to the retained retry-signal
39
- * entries: facts travel on persisted `AssistantMessage`s and through
38
+ * `headers` is always a plain record limited to the retained retry and
39
+ * gateway quota/queue signal entries: facts travel on persisted
40
+ * `AssistantMessage`s and through
40
41
  * `structuredClone` snapshots (managed fallback attempt staging), so they must
41
42
  * never carry a live `Headers` instance — cloning one throws `DataCloneError`
42
43
  * ("The object can not be cloned.") and masks the real provider failure.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@vib-rato/ai",
4
- "version": "0.17.2",
4
+ "version": "0.18.0",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://github.com/Keonho-Chu/Vibrato",
7
7
  "author": "Yeachan-Heo and Vibrato Contributors",
@@ -40,8 +40,8 @@
40
40
  "dependencies": {
41
41
  "@anthropic-ai/sdk": "^0.94.0",
42
42
  "@bufbuild/protobuf": "^2.12.0",
43
- "@vib-rato/natives": "0.17.2",
44
- "@vib-rato/utils": "0.17.2",
43
+ "@vib-rato/natives": "0.18.0",
44
+ "@vib-rato/utils": "0.18.0",
45
45
  "openai": "^6.36.0",
46
46
  "partial-json": "^0.1.7",
47
47
  "zod": "4.4.3"
@@ -230,7 +230,6 @@ export function modelSupportsReasoningControl<TApi extends Api>(
230
230
  resolvedBaseUrl?: string,
231
231
  ): boolean {
232
232
  if (!model.reasoning) return false;
233
-
234
233
  if (model.api === "openai-completions") {
235
234
  const completionsModel = model as ApiModel<"openai-completions">;
236
235
  const explicitSupport = completionsModel.compat?.supportsReasoningEffort;
@@ -346,6 +345,18 @@ export function clampThinkingLevelForModel<TApi extends Api>(
346
345
  if (!modelSupportsReasoningControl(model) || requested === undefined) {
347
346
  return undefined;
348
347
  }
348
+ if (model.api === "anthropic-messages" && model.provider === "anthropic") {
349
+ const baseUrl = model.baseUrl;
350
+ try {
351
+ const url = new URL(baseUrl || "https://api.anthropic.com");
352
+ if (url.protocol !== "https:" || url.hostname !== "api.anthropic.com") return undefined;
353
+ } catch {
354
+ return undefined;
355
+ }
356
+ }
357
+ if (!model.thinking) {
358
+ return undefined;
359
+ }
349
360
 
350
361
  const levels = getSupportedEfforts(model);
351
362
  if (levels.includes(requested)) {
package/src/models.json CHANGED
@@ -3853,6 +3853,31 @@
3853
3853
  "maxLevel": "xhigh"
3854
3854
  }
3855
3855
  },
3856
+ "claude-fable-5-1": {
3857
+ "id": "claude-fable-5-1",
3858
+ "name": "Anthropic Fable 5.1",
3859
+ "api": "anthropic-messages",
3860
+ "provider": "anthropic",
3861
+ "baseUrl": "https://api.anthropic.com",
3862
+ "reasoning": true,
3863
+ "input": [
3864
+ "text",
3865
+ "image"
3866
+ ],
3867
+ "cost": {
3868
+ "input": 10,
3869
+ "output": 50,
3870
+ "cacheRead": 0.25,
3871
+ "cacheWrite": 12.5
3872
+ },
3873
+ "contextWindow": 1000000,
3874
+ "maxTokens": 128000,
3875
+ "thinking": {
3876
+ "mode": "anthropic-adaptive",
3877
+ "minLevel": "minimal",
3878
+ "maxLevel": "xhigh"
3879
+ }
3880
+ },
3856
3881
  "claude-haiku-4-5": {
3857
3882
  "id": "claude-haiku-4-5",
3858
3883
  "name": "Anthropic Haiku 4.5 (latest)",
@@ -107,7 +107,7 @@ export interface CpaToolAliasRestoreFailure {
107
107
  */
108
108
  export declare function parseCpaToolAliasRestoreFailure(error: unknown): CpaToolAliasRestoreFailure | undefined;
109
109
  export declare function isCpaToolAliasRestoreFailure(error: unknown): boolean;
110
- export declare const claudeCodeVersion = "2.1.219";
110
+ export declare const claudeCodeVersion = "2.1.257";
111
111
  export declare const claudeCodeEntrypoint = "sdk-cli";
112
112
  export declare const claudeToolPrefix: string;
113
113
  export declare const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
@@ -828,7 +828,7 @@ function getCacheControl(
828
828
  }
829
829
 
830
830
  // Stealth mode: Mimic Anthropic Code headers and tool prefixing.
831
- export const claudeCodeVersion = "2.1.219";
831
+ export const claudeCodeVersion = "2.1.257";
832
832
  export const claudeCodeEntrypoint = "sdk-cli";
833
833
  export const claudeToolPrefix: string = "proxy_";
834
834
  export const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
@@ -25,7 +25,7 @@ const REQUEST_BLOCKED_MESSAGE_RE = /^\s*request blocked\b/i;
25
25
 
26
26
  export async function parseCodexError(response: Response): Promise<CodexErrorInfo> {
27
27
  const raw = await response.text();
28
- let message = raw || response.statusText || "Request failed";
28
+ let message = raw || response.statusText || `Codex request failed (HTTP ${response.status})`;
29
29
  let friendlyMessage: string | undefined;
30
30
  let rateLimits: CodexRateLimits | undefined;
31
31
  let code: string | undefined;
@@ -25,6 +25,12 @@ export declare function normalizeCodexToolChoice(choice: ToolChoice | undefined,
25
25
  export declare function formatCodexUserAgent(platform: string, release: string, arch: string): string;
26
26
  export declare const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses">;
27
27
  export declare function prewarmOpenAICodexResponses(model: Model<"openai-codex-responses">, options?: Pick<OpenAICodexResponsesOptions, "apiKey" | "headers" | "sessionId" | "signal" | "preferWebsockets" | "providerSessionState">): Promise<void>;
28
+ /**
29
+ * Bun 1.4.0's Windows WebSocket client can segfault during TLS handshakes.
30
+ * Keep the model's websocket preference available on other platforms, while
31
+ * requiring an explicit opt-in on Windows until the bundled runtime is fixed.
32
+ */
33
+ export declare function isCodexWebSocketSafeByDefault(platform?: NodeJS.Platform): boolean;
28
34
  export interface OpenAICodexTransportDetails {
29
35
  websocketPreferred: boolean;
30
36
  lastTransport?: CodexTransport;
@@ -2307,6 +2307,15 @@ function recordCodexWebSocketFailure(state: CodexWebSocketSessionState, activate
2307
2307
  }
2308
2308
  }
2309
2309
 
2310
+ /**
2311
+ * Bun 1.4.0's Windows WebSocket client can segfault during TLS handshakes.
2312
+ * Keep the model's websocket preference available on other platforms, while
2313
+ * requiring an explicit opt-in on Windows until the bundled runtime is fixed.
2314
+ */
2315
+ export function isCodexWebSocketSafeByDefault(platform: NodeJS.Platform = process.platform): boolean {
2316
+ return platform !== "win32";
2317
+ }
2318
+
2310
2319
  function shouldUseCodexWebSocket(
2311
2320
  model: Model<"openai-codex-responses">,
2312
2321
  state: CodexWebSocketSessionState | undefined,
@@ -2314,7 +2323,11 @@ function shouldUseCodexWebSocket(
2314
2323
  ): boolean {
2315
2324
  if (!state || state.disableWebsocket) return false;
2316
2325
  if (preferWebsockets === false) return false;
2317
- return isCodexWebSocketEnvEnabled() || preferWebsockets === true || model.preferWebsockets === true;
2326
+ return (
2327
+ isCodexWebSocketEnvEnabled() ||
2328
+ preferWebsockets === true ||
2329
+ (isCodexWebSocketSafeByDefault() && model.preferWebsockets === true)
2330
+ );
2318
2331
  }
2319
2332
 
2320
2333
  export interface OpenAICodexTransportDetails {
@@ -2372,7 +2385,9 @@ export function getOpenAICodexTransportDetails(
2372
2385
  const websocketPreferred =
2373
2386
  options?.preferWebsockets === false
2374
2387
  ? false
2375
- : isCodexWebSocketEnvEnabled() || options?.preferWebsockets === true || model.preferWebsockets === true;
2388
+ : isCodexWebSocketEnvEnabled() ||
2389
+ options?.preferWebsockets === true ||
2390
+ (isCodexWebSocketSafeByDefault() && model.preferWebsockets === true);
2376
2391
  const state = getCodexWebSocketStateForPublicSession(model, options);
2377
2392
 
2378
2393
  return {
@@ -49,6 +49,29 @@ const NON_WIRE_KEYS = new Set<keyof SimpleStreamOptions>([
49
49
  "fallbackAttempt",
50
50
  ]);
51
51
 
52
+ /**
53
+ * Project the caller's {@link Context} onto the wire schema. Runtime tool
54
+ * objects routinely carry harness-only state (runners, session managers,
55
+ * fs-stat BigInts) that must never be serialized: BigInt fields make
56
+ * `JSON.stringify` throw outright, and the rest is dead weight the gateway
57
+ * re-derives from its own tool registry. Only the protocol-meaningful,
58
+ * JSON-safe `Tool` fields cross the wire.
59
+ */
60
+ function buildWireContext(context: Context): Context {
61
+ if (!context.tools || context.tools.length === 0) return context;
62
+ return {
63
+ ...context,
64
+ tools: context.tools.map(tool => ({
65
+ name: tool.name,
66
+ description: tool.description,
67
+ parameters: tool.parameters,
68
+ ...(tool.strict !== undefined ? { strict: tool.strict } : {}),
69
+ ...(tool.customFormat !== undefined ? { customFormat: tool.customFormat } : {}),
70
+ ...(tool.customWireName !== undefined ? { customWireName: tool.customWireName } : {}),
71
+ })),
72
+ };
73
+ }
74
+
52
75
  function buildWireOptions(options: SimpleStreamOptions | undefined): Record<string, unknown> {
53
76
  if (!options) return {};
54
77
  const wire: Record<string, unknown> = {};
@@ -166,7 +189,7 @@ export function streamPiNative<TApi extends Api>(
166
189
  const headers = buildHeaders(model as Model<Api>, options?.apiKey);
167
190
  const body = JSON.stringify({
168
191
  modelId: model.id,
169
- context,
192
+ context: buildWireContext(context),
170
193
  options: buildWireOptions(options),
171
194
  stream: true,
172
195
  });
package/src/types.d.ts CHANGED
@@ -997,6 +997,18 @@ export interface LongContextPricing {
997
997
  threshold: number;
998
998
  cost: ModelCost;
999
999
  }
1000
+ /**
1001
+ * Capability facts the serving endpoint advertised for a discovered model
1002
+ * (the `vibrato` object on its OpenAI-style models-list entry). Only fields
1003
+ * that describe what the model can do are carried: a hint can never redirect a
1004
+ * request or change its credentials.
1005
+ */
1006
+ export interface DiscoveredModelHint {
1007
+ name?: string;
1008
+ reasoning?: boolean;
1009
+ thinking?: ThinkingConfig;
1010
+ compat?: Pick<OpenAICompat, "supportsReasoningEffort" | "reasoningContentField" | "thinkingFormat">;
1011
+ }
1000
1012
  export interface Model<TApi extends Api = any> {
1001
1013
  id: string;
1002
1014
  name: string;
@@ -1022,6 +1034,12 @@ export interface Model<TApi extends Api = any> {
1022
1034
  maxTokens: number;
1023
1035
  /** Explicit models.yml/model-override authority; absent keeps the safe transport default. */
1024
1036
  maxTokensSource?: ModelMaxTokensSource;
1037
+ /**
1038
+ * What the endpoint advertised about this model at discovery time. Kept on
1039
+ * the model so a cached discovery restores it; the registry applies it after
1040
+ * provider overrides and before the user's model overrides.
1041
+ */
1042
+ discoveryHint?: DiscoveredModelHint;
1025
1043
  headers?: Record<string, string>;
1026
1044
  /**
1027
1045
  * Streaming transport override. When `"pi-native"`, `streamSimple` routes
package/src/types.ts CHANGED
@@ -1213,6 +1213,19 @@ export interface LongContextPricing {
1213
1213
  cost: ModelCost;
1214
1214
  }
1215
1215
 
1216
+ /**
1217
+ * Capability facts the serving endpoint advertised for a discovered model
1218
+ * (the `vibrato` object on its OpenAI-style models-list entry). Only fields
1219
+ * that describe what the model can do are carried: a hint can never redirect a
1220
+ * request or change its credentials.
1221
+ */
1222
+ export interface DiscoveredModelHint {
1223
+ name?: string;
1224
+ reasoning?: boolean;
1225
+ thinking?: ThinkingConfig;
1226
+ compat?: Pick<OpenAICompat, "supportsReasoningEffort" | "reasoningContentField" | "thinkingFormat">;
1227
+ }
1228
+
1216
1229
  export interface Model<TApi extends Api = any> {
1217
1230
  id: string;
1218
1231
  name: string;
@@ -1238,6 +1251,12 @@ export interface Model<TApi extends Api = any> {
1238
1251
  maxTokens: number;
1239
1252
  /** Explicit models.yml/model-override authority; absent keeps the safe transport default. */
1240
1253
  maxTokensSource?: ModelMaxTokensSource;
1254
+ /**
1255
+ * What the endpoint advertised about this model at discovery time. Kept on
1256
+ * the model so a cached discovery restores it; the registry applies it after
1257
+ * provider overrides and before the user's model overrides.
1258
+ */
1259
+ discoveryHint?: DiscoveredModelHint;
1241
1260
  headers?: Record<string, string>;
1242
1261
  /**
1243
1262
  * Streaming transport override. When `"pi-native"`, `streamSimple` routes
@@ -35,8 +35,9 @@ export type TransportHeaders = Headers | Record<string, string | undefined>;
35
35
  * Structured facts from an upstream HTTP or transport failure. Retry decisions
36
36
  * must use these facts rather than provider- or application-owned error text.
37
37
  *
38
- * `headers` is always a plain record limited to the retained retry-signal
39
- * entries: facts travel on persisted `AssistantMessage`s and through
38
+ * `headers` is always a plain record limited to the retained retry and
39
+ * gateway quota/queue signal entries: facts travel on persisted
40
+ * `AssistantMessage`s and through
40
41
  * `structuredClone` snapshots (managed fallback attempt staging), so they must
41
42
  * never carry a live `Headers` instance — cloning one throws `DataCloneError`
42
43
  * ("The object can not be cloned.") and masks the real provider failure.
@@ -40,8 +40,9 @@ export type TransportHeaders = Headers | Record<string, string | undefined>;
40
40
  * Structured facts from an upstream HTTP or transport failure. Retry decisions
41
41
  * must use these facts rather than provider- or application-owned error text.
42
42
  *
43
- * `headers` is always a plain record limited to the retained retry-signal
44
- * entries: facts travel on persisted `AssistantMessage`s and through
43
+ * `headers` is always a plain record limited to the retained retry and
44
+ * gateway quota/queue signal entries: facts travel on persisted
45
+ * `AssistantMessage`s and through
45
46
  * `structuredClone` snapshots (managed fallback attempt staging), so they must
46
47
  * never carry a live `Headers` instance — cloning one throws `DataCloneError`
47
48
  * ("The object can not be cloned.") and masks the real provider failure.
@@ -147,14 +148,29 @@ function stringValue(value: unknown): string | undefined {
147
148
  return typeof value === "string" ? value : undefined;
148
149
  }
149
150
 
150
- /** Retry-signal headers retained on transport facts; everything else is dropped. */
151
- const RETAINED_TRANSPORT_HEADERS = ["retry-after", "retry-after-ms"] as const;
151
+ /**
152
+ * Retry-signal headers retained on transport facts; everything else is
153
+ * dropped. The `x-vug-*` entries are the Vibrato Usage Gateway's quota and
154
+ * queue-depth signals; their values are preserved as raw strings and
155
+ * interpreted by consumers, not by this module.
156
+ */
157
+ const RETAINED_TRANSPORT_HEADERS = [
158
+ "retry-after",
159
+ "retry-after-ms",
160
+ "x-vug-daily-limit",
161
+ "x-vug-daily-used",
162
+ "x-vug-daily-remaining",
163
+ "x-vug-daily-reset",
164
+ "x-vug-queue-depth",
165
+ "x-vug-inflight",
166
+ "x-vug-queued-ms",
167
+ ] as const;
152
168
 
153
169
  const RETAINED_TRANSPORT_HEADER_SET: ReadonlySet<string> = new Set(RETAINED_TRANSPORT_HEADERS);
154
170
 
155
171
  /**
156
- * Reduce transport headers to the retained retry-signal entries in a plain
157
- * record, so facts stay structured-cloneable and JSON-serializable and never
172
+ * Reduce transport headers to the retained retry and gateway quota/queue
173
+ * signal entries in a plain record, so facts stay structured-cloneable and JSON-serializable and never
158
174
  * persist arbitrary response headers into session files.
159
175
  *
160
176
  * Exception-safe by contract: inspection uses only `Headers.get()` results
@@ -276,9 +292,22 @@ export function transportFailureFacts(
276
292
  };
277
293
  }
278
294
 
295
+ /**
296
+ * Exception-safe by contract: `new Headers(record)` throws on an invalid
297
+ * header value (e.g. a CRLF in a gateway-supplied timestamp like
298
+ * `x-vug-daily-reset`), and the retained record is otherwise free-form
299
+ * provider input. A throw here falls through to `undefined` — the same
300
+ * "no retry hint" result as if no headers were retained at all — instead of
301
+ * propagating a TypeError out of classification.
302
+ */
279
303
  function headersOf(headers: TransportHeaders | undefined): Headers | undefined {
280
304
  if (headers instanceof Headers) return headers;
281
- return headers ? new Headers(headers as Record<string, string>) : undefined;
305
+ if (!headers) return undefined;
306
+ try {
307
+ return new Headers(headers as Record<string, string>);
308
+ } catch {
309
+ return undefined;
310
+ }
282
311
  }
283
312
 
284
313
  function parseRetryAfterSeconds(value: string | null, now = Date.now()): number | undefined {
@@ -305,7 +334,12 @@ function isQuotaCode(code: string | undefined): boolean {
305
334
  code === "quota_exhausted" ||
306
335
  code === "usage_limit_reached" ||
307
336
  code === "usage_not_included" ||
308
- code === "out_of_credits"
337
+ code === "out_of_credits" ||
338
+ // Vibrato Usage Gateway daily-limit code: a 429 with this code is a
339
+ // quota exhaustion, not an ordinary rate limit, even though the
340
+ // gateway's retry-after window can be many hours long. A code-less
341
+ // 429 from an older gateway still falls through to `rate_limit`.
342
+ code === "daily_token_limit"
309
343
  );
310
344
  }
311
345
 
@@ -19,6 +19,13 @@ const DEFAULT_TIMEOUT = 300_000;
19
19
  const DEFAULT_HOSTNAME = "localhost";
20
20
  const CALLBACK_PATH = "/callback";
21
21
 
22
+ function serializeScriptData(value: unknown): string {
23
+ return JSON.stringify(value)
24
+ .replaceAll("<", "\\u003c")
25
+ .replaceAll("\u2028", "\\u2028")
26
+ .replaceAll("\u2029", "\\u2029");
27
+ }
28
+
22
29
  export type CallbackResult = { code: string; state: string };
23
30
 
24
31
  export interface OAuthCallbackFlowOptions {
@@ -259,7 +266,7 @@ export abstract class OAuthCallbackFlow {
259
266
  });
260
267
 
261
268
  return new Response(
262
- (templateHtml as unknown as string).replaceAll("__OAUTH_STATE__", JSON.stringify(resultState)),
269
+ (templateHtml as unknown as string).replaceAll("__OAUTH_STATE__", () => serializeScriptData(resultState)),
263
270
  {
264
271
  status: resultState.ok ? 200 : 500,
265
272
  headers: { "Content-Type": "text/html" },