@providerkit/core 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +35 -158
  2. package/dist/context.d.ts.map +1 -1
  3. package/dist/context.js +8 -0
  4. package/dist/context.js.map +1 -1
  5. package/dist/errors.d.ts +36 -0
  6. package/dist/errors.d.ts.map +1 -1
  7. package/dist/errors.js +75 -2
  8. package/dist/errors.js.map +1 -1
  9. package/dist/index.d.ts +4 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +4 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/key-pool.d.ts +60 -0
  14. package/dist/key-pool.d.ts.map +1 -0
  15. package/dist/key-pool.js +235 -0
  16. package/dist/key-pool.js.map +1 -0
  17. package/dist/providers/anthropic.d.ts +10 -6
  18. package/dist/providers/anthropic.d.ts.map +1 -1
  19. package/dist/providers/anthropic.js +30 -14
  20. package/dist/providers/anthropic.js.map +1 -1
  21. package/dist/providers/gemini.d.ts +40 -0
  22. package/dist/providers/gemini.d.ts.map +1 -0
  23. package/dist/providers/gemini.js +303 -0
  24. package/dist/providers/gemini.js.map +1 -0
  25. package/dist/providers/openai.d.ts +38 -1
  26. package/dist/providers/openai.d.ts.map +1 -1
  27. package/dist/providers/openai.js +122 -16
  28. package/dist/providers/openai.js.map +1 -1
  29. package/dist/providers/responses.d.ts +38 -0
  30. package/dist/providers/responses.d.ts.map +1 -0
  31. package/dist/providers/responses.js +341 -0
  32. package/dist/providers/responses.js.map +1 -0
  33. package/dist/rate-limit.d.ts +29 -0
  34. package/dist/rate-limit.d.ts.map +1 -0
  35. package/dist/rate-limit.js +194 -0
  36. package/dist/rate-limit.js.map +1 -0
  37. package/dist/transport.d.ts +18 -5
  38. package/dist/transport.d.ts.map +1 -1
  39. package/dist/transport.js +61 -35
  40. package/dist/transport.js.map +1 -1
  41. package/dist/types.d.ts +13 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/types.js +7 -2
  44. package/dist/types.js.map +1 -1
  45. package/dist/zod.d.ts +8 -1
  46. package/dist/zod.d.ts.map +1 -1
  47. package/dist/zod.js +9 -2
  48. package/dist/zod.js.map +1 -1
  49. package/package.json +2 -2
  50. package/src/context.ts +7 -0
  51. package/src/errors.ts +82 -2
  52. package/src/index.ts +4 -0
  53. package/src/key-pool.ts +272 -0
  54. package/src/providers/anthropic.ts +41 -20
  55. package/src/providers/gemini.ts +386 -0
  56. package/src/providers/openai.ts +153 -16
  57. package/src/providers/responses.ts +455 -0
  58. package/src/rate-limit.ts +217 -0
  59. package/src/transport.ts +61 -35
  60. package/src/types.ts +19 -2
  61. package/src/zod.ts +12 -2
@@ -0,0 +1,38 @@
1
+ import type { ChatMessage, Effort, Provider } from "../types.ts";
2
+ export interface ResponsesConfig {
3
+ apiKey: string;
4
+ model: string;
5
+ /** Any endpoint speaking the Responses format. Defaults to OpenAI itself. */
6
+ baseUrl?: string;
7
+ /** Names the provider in errors and logs. */
8
+ id?: string;
9
+ effort?: Effort;
10
+ maxTokens?: number;
11
+ fetchImpl?: typeof fetch;
12
+ /** Extra request headers — where a subscription backend's account id goes
13
+ * (`ChatGPT-Account-Id`), which those backends reject the request without. */
14
+ headers?: Record<string, string>;
15
+ /**
16
+ * Where this backend serves the endpoint, when it is not `/v1/responses`.
17
+ * The ChatGPT subscription surface serves it at `/backend-api/codex/responses`
18
+ * with no version segment, so that backend needs
19
+ * `{ baseUrl: "https://chatgpt.com/backend-api/codex", path: "/responses" }`.
20
+ * Without the override the POST 404s, and a 404 classifies as "model" — the
21
+ * user is told the model id does not exist when the path was the problem.
22
+ */
23
+ path?: string;
24
+ }
25
+ /**
26
+ * Flatten a history into `instructions` plus the input item list.
27
+ *
28
+ * This shape has no system ROLE — the system prompt is a top-level
29
+ * `instructions` string, and everything else is items. One assistant turn can
30
+ * become several items (its text, then one `function_call` per tool it asked
31
+ * for), which is why a message maps to a list rather than to one item.
32
+ */
33
+ export declare function toResponsesInput(messages: readonly ChatMessage[]): {
34
+ instructions?: string;
35
+ input: unknown[];
36
+ };
37
+ export declare function createResponsesProvider(config: ResponsesConfig): Provider;
38
+ //# sourceMappingURL=responses.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"responses.d.ts","sourceRoot":"","sources":["../../src/providers/responses.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EACV,WAAW,EAEX,MAAM,EAGN,QAAQ,EAIT,MAAM,aAAa,CAAC;AAGrB,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB;mFAC+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAiDD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,GAAG;IAClE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,OAAO,EAAE,CAAC;CAClB,CAuDA;AA2DD,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,eAAe,GAAG,QAAQ,CAuOzE"}
@@ -0,0 +1,341 @@
1
+ // Responses-shape adapter — SSE from POST /v1/responses.
2
+ //
3
+ // OpenAI's second wire format, and the only one some backends expose: the
4
+ // ChatGPT subscription surface (chatgpt.com/backend-api/codex) has no
5
+ // chat/completions endpoint at all. It is not chat/completions with a new path
6
+ // — the input is an ITEM LIST rather than a message list, the stream is
7
+ // event-typed rather than choice-delta'd, and the terminal event carries the
8
+ // usage record instead of a trailing usage-only frame.
9
+ //
10
+ // The event names arrive on the SSE `event:` line and are repeated inside each
11
+ // payload's own `type`. The transport yields only `data:` payloads, so this
12
+ // adapter reads `type` — which is what survives, and what gateways agree on.
13
+ import { streamError } from "../errors.js";
14
+ import { streamSse, apiUrl } from "../transport.js";
15
+ import { toDataUri } from "../types.js";
16
+ const DEFAULT_BASE_URL = "https://api.openai.com";
17
+ const DEFAULT_PATH = "/v1/responses";
18
+ /**
19
+ * `response.incomplete` means the turn was cut short, and the seam has one word
20
+ * for that: "length". `content_filter` is the only other reason this shape
21
+ * documents; anything new stays on "length" rather than reporting a clean stop,
22
+ * because a caller that believes a truncated answer finished will act on it.
23
+ */
24
+ function mapIncompleteReason(reason) {
25
+ return reason === "content_filter" ? "content_filter" : "length";
26
+ }
27
+ function partsToResponses(content) {
28
+ if (typeof content === "string")
29
+ return [{ type: "input_text", text: content }];
30
+ return content.map((part) => part.type === "text"
31
+ ? { type: "input_text", text: part.text }
32
+ : { type: "input_image", image_url: toDataUri(part) });
33
+ }
34
+ /** A tool result is a bare string unless it carried images — then the content-
35
+ * array form, the only way to hand this shape a screenshot back. */
36
+ function toolOutput(content, images) {
37
+ if (images.length === 0)
38
+ return content;
39
+ const parts = [];
40
+ if (content)
41
+ parts.push({ type: "input_text", text: content });
42
+ for (const image of images)
43
+ parts.push({ type: "input_image", image_url: toDataUri(image) });
44
+ return parts;
45
+ }
46
+ /**
47
+ * Flatten a history into `instructions` plus the input item list.
48
+ *
49
+ * This shape has no system ROLE — the system prompt is a top-level
50
+ * `instructions` string, and everything else is items. One assistant turn can
51
+ * become several items (its text, then one `function_call` per tool it asked
52
+ * for), which is why a message maps to a list rather than to one item.
53
+ */
54
+ export function toResponsesInput(messages) {
55
+ const instructions = messages
56
+ .filter((message) => message.role === "system")
57
+ .map((message) => message.content)
58
+ .join("\n\n");
59
+ const input = [];
60
+ for (const message of messages) {
61
+ switch (message.role) {
62
+ case "system":
63
+ break; // lifted into `instructions` above
64
+ case "user":
65
+ input.push({ type: "message", role: "user", content: partsToResponses(message.content) });
66
+ break;
67
+ case "tool":
68
+ input.push({
69
+ // `call_id` — the id the model coined for the CALL, not the `fc_…` id
70
+ // of the output item that carried it. Sending the wrong one is a 400
71
+ // reading "No tool output found for function call", one turn later.
72
+ type: "function_call_output",
73
+ call_id: message.toolCallId,
74
+ output: toolOutput(message.content, message.images ?? []),
75
+ });
76
+ break;
77
+ case "assistant": {
78
+ // Reasoning is deliberately NOT replayed. This shape wants the ORIGINAL
79
+ // reasoning item back — its `rs_…` id, and under `store: false` its
80
+ // `encrypted_content` blob — and the seam carries neither, only the
81
+ // plain summary text a caller renders. A synthesized reasoning item is
82
+ // rejected; omitting it costs only the model re-deriving its own chain
83
+ // of thought, which is what every stateless caller already lives with.
84
+ if (message.content) {
85
+ input.push({
86
+ type: "message",
87
+ role: "assistant",
88
+ content: [{ type: "output_text", text: message.content }],
89
+ });
90
+ }
91
+ for (const call of message.toolCalls ?? []) {
92
+ input.push({
93
+ type: "function_call",
94
+ call_id: call.id,
95
+ name: call.name,
96
+ arguments: call.arguments,
97
+ });
98
+ }
99
+ break;
100
+ }
101
+ }
102
+ }
103
+ return { ...(instructions ? { instructions } : {}), input };
104
+ }
105
+ function usageChunk(usage) {
106
+ return {
107
+ type: "usage",
108
+ usage: {
109
+ inputTokens: usage.input_tokens ?? 0,
110
+ // Already a SUBSET of input_tokens on this shape, as on chat/completions
111
+ // — and the only signal that its automatic prefix caching is working.
112
+ cachedInputTokens: usage.input_tokens_details?.cached_tokens ?? 0,
113
+ // Reasoning tokens are billed INSIDE output_tokens, not beside them.
114
+ outputTokens: usage.output_tokens ?? 0,
115
+ },
116
+ };
117
+ }
118
+ export function createResponsesProvider(config) {
119
+ const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
120
+ const id = config.id ?? "openai-responses";
121
+ return {
122
+ id,
123
+ model: config.model,
124
+ async *createStream(messages, tools, opts = {}) {
125
+ const effort = opts.effort ?? config.effort;
126
+ const { instructions, input } = toResponsesInput(messages);
127
+ const request = {
128
+ model: opts.model ?? config.model,
129
+ input,
130
+ stream: true,
131
+ // The caller's history is the entire state of a run. Server-side
132
+ // storage adds a retention surface nobody asked for, and is refused
133
+ // outright on zero-data-retention accounts.
134
+ store: false,
135
+ };
136
+ if (instructions)
137
+ request.instructions = instructions;
138
+ const maxTokens = opts.maxTokens ?? config.maxTokens;
139
+ if (maxTokens !== undefined)
140
+ request.max_output_tokens = maxTokens;
141
+ if (opts.temperature !== undefined)
142
+ request.temperature = opts.temperature;
143
+ if (effort && effort !== "none") {
144
+ // `summary` is what switches the reasoning stream ON. Without it this
145
+ // shape emits no reasoning_summary_text events at all, and a caller
146
+ // rendering a thinking pane silently gets nothing while the tokens are
147
+ // billed either way.
148
+ request.reasoning = { effort, summary: "auto" };
149
+ }
150
+ if (tools.length > 0) {
151
+ // Flat here — no nested `function` envelope, unlike chat/completions.
152
+ request.tools = tools.map((tool) => ({
153
+ type: "function",
154
+ name: tool.name,
155
+ description: tool.description,
156
+ parameters: tool.inputSchema,
157
+ }));
158
+ }
159
+ if (opts.toolChoice && opts.toolChoice !== "auto") {
160
+ request.tool_choice =
161
+ typeof opts.toolChoice === "string"
162
+ ? opts.toolChoice
163
+ : { type: "function", name: opts.toolChoice.name };
164
+ }
165
+ if (opts.json) {
166
+ // The schema rides in `text.format`, not `response_format`.
167
+ request.text = {
168
+ format: {
169
+ type: "json_schema",
170
+ name: opts.json.name,
171
+ schema: opts.json.schema,
172
+ strict: true,
173
+ },
174
+ };
175
+ }
176
+ // Tool calls arrive as an item skeleton plus argument deltas; keyed by the
177
+ // output item id so parallel calls never cross wires. The seam's index is
178
+ // ours to assign — `output_index` counts reasoning and message items too.
179
+ const pending = new Map();
180
+ let nextIndex = 0;
181
+ // This shape never states a stop reason on a clean finish, so it is
182
+ // inferred from whether the turn produced a function call.
183
+ let sawToolCall = false;
184
+ for await (const data of streamSse({
185
+ url: apiUrl(baseUrl, config.path ?? DEFAULT_PATH),
186
+ headers: { authorization: `Bearer ${config.apiKey}`, ...config.headers },
187
+ body: request,
188
+ provider: id,
189
+ ...(opts.signal ? { signal: opts.signal } : {}),
190
+ ...(config.fetchImpl ? { fetchImpl: config.fetchImpl } : {}),
191
+ })) {
192
+ let event;
193
+ try {
194
+ event = JSON.parse(data);
195
+ }
196
+ catch {
197
+ continue; // a keep-alive or a frame we do not model
198
+ }
199
+ switch (event.type) {
200
+ case "response.output_text.delta":
201
+ if (event.delta)
202
+ yield { type: "delta", content: event.delta };
203
+ break;
204
+ // Two names for the same stream: `reasoning_summary_text` is the
205
+ // redacted summary the API returns, `reasoning_text` the raw trace
206
+ // the ChatGPT backend streams. A caller wants whichever it gets.
207
+ case "response.reasoning_summary_text.delta":
208
+ case "response.reasoning_text.delta":
209
+ if (event.delta)
210
+ yield { type: "delta", reasoning: event.delta };
211
+ break;
212
+ case "response.output_item.added": {
213
+ const item = event.item;
214
+ if (item?.type !== "function_call" || !item.id)
215
+ break;
216
+ sawToolCall = true;
217
+ const index = nextIndex++;
218
+ // Normally empty here, but a backend that already has the whole
219
+ // call sends it in the skeleton.
220
+ const seeded = item.arguments ?? "";
221
+ pending.set(item.id, {
222
+ index,
223
+ id: item.call_id ?? "",
224
+ name: item.name ?? "",
225
+ streamed: seeded,
226
+ });
227
+ // Only the fields the skeleton actually states. An empty `id` here
228
+ // is not "unknown", it is a wrong answer: a consumer takes the last
229
+ // stated value, so `""` written into the slot survives the real
230
+ // `call_id` arriving on `.done`.
231
+ yield {
232
+ type: "delta",
233
+ toolCalls: [
234
+ {
235
+ index,
236
+ ...(item.call_id ? { id: item.call_id } : {}),
237
+ ...(item.name ? { name: item.name } : {}),
238
+ ...(seeded ? { arguments: seeded } : {}),
239
+ },
240
+ ],
241
+ };
242
+ break;
243
+ }
244
+ case "response.function_call_arguments.delta": {
245
+ const call = event.item_id ? pending.get(event.item_id) : undefined;
246
+ if (!call || !event.delta)
247
+ break;
248
+ call.streamed += event.delta;
249
+ yield { type: "delta", toolCalls: [{ index: call.index, arguments: event.delta }] };
250
+ break;
251
+ }
252
+ case "response.output_item.done": {
253
+ const item = event.item;
254
+ if (item?.type !== "function_call")
255
+ break;
256
+ const known = item.id ? pending.get(item.id) : undefined;
257
+ if (item.id)
258
+ pending.delete(item.id);
259
+ const snapshot = item.arguments ?? "";
260
+ if (!known) {
261
+ // A backend that emits neither the skeleton nor the deltas — the
262
+ // whole call arrives here or not at all. Without a `call_id`
263
+ // there is nothing to answer it with: the caller's
264
+ // `function_call_output` would quote `""` and take a 400 reading
265
+ // "No tool output found for function call" one turn later, so the
266
+ // call is dropped rather than handed over unrunnable. Dropping is
267
+ // only possible here, where nothing has been streamed for it yet.
268
+ if (!item.call_id)
269
+ break;
270
+ sawToolCall = true;
271
+ yield {
272
+ type: "delta",
273
+ toolCalls: [
274
+ {
275
+ index: nextIndex++,
276
+ id: item.call_id,
277
+ ...(item.name ? { name: item.name } : {}),
278
+ arguments: snapshot,
279
+ },
280
+ ],
281
+ };
282
+ break;
283
+ }
284
+ // The snapshot is authoritative, but the fragments already went
285
+ // out: re-emitting it whole concatenates the JSON with itself and
286
+ // every argument parse fails. Send only what the deltas missed —
287
+ // which is all of it when they never came.
288
+ const tail = snapshot.length > known.streamed.length && snapshot.startsWith(known.streamed)
289
+ ? snapshot.slice(known.streamed.length)
290
+ : "";
291
+ // `.done` restates the identity, and on a backend that leaves it
292
+ // out of the skeleton this is the only frame that carries it. It
293
+ // rides last so it WINS: the alternative is a caller assembling a
294
+ // nameless call it has no tool to dispatch, quoting an empty
295
+ // `call_id` back on the turn after.
296
+ const restated = {
297
+ ...(item.call_id && item.call_id !== known.id ? { id: item.call_id } : {}),
298
+ ...(item.name && item.name !== known.name ? { name: item.name } : {}),
299
+ ...(tail ? { arguments: tail } : {}),
300
+ };
301
+ if (Object.keys(restated).length > 0) {
302
+ yield { type: "delta", toolCalls: [{ index: known.index, ...restated }] };
303
+ }
304
+ break;
305
+ }
306
+ case "response.completed": {
307
+ const usage = event.response?.usage;
308
+ if (usage)
309
+ yield usageChunk(usage);
310
+ yield { type: "finish", finishReason: sawToolCall ? "tool_calls" : "stop" };
311
+ return;
312
+ }
313
+ case "response.incomplete": {
314
+ // A turn that ran out of output tokens still billed for its input,
315
+ // and this event carries usage in the same shape as `completed`.
316
+ const usage = event.response?.usage;
317
+ if (usage)
318
+ yield usageChunk(usage);
319
+ yield {
320
+ type: "finish",
321
+ finishReason: mapIncompleteReason(event.response?.incomplete_details?.reason),
322
+ };
323
+ return;
324
+ }
325
+ // The two ways this shape reports a failure after its headers went
326
+ // out: a terminal `response.failed`, or a bare error frame from a
327
+ // gateway in front of it.
328
+ case "response.failed":
329
+ throw streamError(id, event.response?.error);
330
+ case "error":
331
+ case "response.error":
332
+ throw streamError(id, event.error ?? { message: event.message, code: event.code });
333
+ }
334
+ }
335
+ // The stream closed without a terminal event. Nothing is lost but the
336
+ // finish reason and the usage record — every delta, tool-call fragment
337
+ // included, was already yielded as it arrived.
338
+ },
339
+ };
340
+ }
341
+ //# sourceMappingURL=responses.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"responses.js","sourceRoot":"","sources":["../../src/providers/responses.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /** Which subscription window bound. Only Anthropic's unified headers name one
2
+ * outright; the codex body shape has it inferred from the wait. */
3
+ export type RateLimitWindow = "5h" | "weekly" | "monthly";
4
+ export interface RateLimitReset {
5
+ /** Absolute time the binding window resets, when any header disclosed it. */
6
+ resetAtMs?: number;
7
+ /** Server-requested wait (`retry-after`) — what a retry policy honours. */
8
+ retryAfterMs?: number;
9
+ /** Which subscription window bound, when it could be named at all. */
10
+ window?: RateLimitWindow;
11
+ }
12
+ /**
13
+ * Parse the rate-limit headers off a (usually 429) response.
14
+ *
15
+ * Anthropic's `anthropic-ratelimit-unified-*` pair rides on every OAuth
16
+ * response, so both windows are always reported and only one of them is the
17
+ * reason for this 429. API-key accounts get the `anthropic-ratelimit-*-reset`
18
+ * RFC 3339 timestamps instead. Everything is optional — an unrecognized shape
19
+ * yields an empty result, and the caller falls back to its generic message.
20
+ */
21
+ export declare function parseRateLimitReset(headers: Headers, now?: number): RateLimitReset;
22
+ /**
23
+ * ChatGPT's codex backend puts the reset IN THE 429 BODY, not the headers:
24
+ * `{"error":{"type":"usage_limit_reached","resets_at":1788801754,"resets_in_seconds":2501465}}`.
25
+ * The window name is inferred from the wait itself, and only past the floor
26
+ * above — a sub-minute retry is a throttle, not a subscription window.
27
+ */
28
+ export declare function parseUsageLimitBody(bodyText: string, now?: number): RateLimitReset;
29
+ //# sourceMappingURL=rate-limit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AA2BA;oEACoE;AACpE,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE1D,MAAM,WAAW,cAAc;IAC7B,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AA8DD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,SAAa,GAAG,cAAc,CA+CtF;AAsBD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG,cAAc,CAgCtF"}
@@ -0,0 +1,194 @@
1
+ // Rate-limit headers, read for the one thing a 429 never says in its body: WHEN.
2
+ //
3
+ // A 429 discloses that a limit was hit, never which limit. A per-minute
4
+ // throttle and a Claude OAuth subscription window (5-hour, or weekly) arrive
5
+ // as the same status with the same wording, and the second one resets in DAYS.
6
+ // Only the headers tell them apart. Without them a caller can say nothing
7
+ // better than "try again in a moment", which is a lie for the multi-day case.
8
+ //
9
+ // Parsing only. Rendering `resetAtMs` as "in 4 hours (6:47 PM)" belongs to the
10
+ // caller: relative-time wording is locale work, and a library that ships it
11
+ // either drags in an i18n dependency or hardcodes English.
12
+ //
13
+ // The overlap with `retryAfterFromHeaders` in transport.ts is deliberate, and
14
+ // the two are not interchangeable. That one answers the retry policy's
15
+ // question — one number, how long to sleep — so it treats `Retry-After` as
16
+ // authoritative whenever it is present, and falls back to any vendor reset
17
+ // header it can find. This one answers the user-facing question, where the
18
+ // unified window's reset OUTRANKS `Retry-After`: Anthropic sends
19
+ // `retry-after: 60` beside a weekly window that lifts in three days, and 60
20
+ // seconds is the correct sleep and the wrong horizon. Delegating here would
21
+ // also fill `retryAfterMs` — documented as what the SERVER asked for — with a
22
+ // vendor reset header on every response that carries no `Retry-After` at all.
23
+ // What must NOT differ is which headers the two can date: the fallback list
24
+ // below is transport's list, because a header only one of them reads is a 429
25
+ // whose `ProviderError.retryAfterMs` knows the wait is three days while the
26
+ // user-facing answer is empty.
27
+ const RETRY_AFTER = "retry-after";
28
+ const UNIFIED_5H = "anthropic-ratelimit-unified-5h-";
29
+ const UNIFIED_7D = "anthropic-ratelimit-unified-7d-";
30
+ /** Reset headers that name a time rather than a window, in the order
31
+ * transport.ts reads them and kept in step with that list. The singular
32
+ * `anthropic-ratelimit-unified-reset` rides on OAuth responses that carry no
33
+ * 5h/7d pair, and the `x-ratelimit-*` spellings are what every non-Anthropic
34
+ * provider and gateway sends. */
35
+ const RESET_HEADERS = [
36
+ "anthropic-ratelimit-unified-reset",
37
+ "anthropic-ratelimit-requests-reset",
38
+ "anthropic-ratelimit-tokens-reset",
39
+ "x-ratelimit-reset-requests",
40
+ "x-ratelimit-reset-tokens",
41
+ "x-ratelimit-reset",
42
+ ];
43
+ /** Seconds since 1970 passed a billion in 2001, so a bare integer above this is
44
+ * a timestamp and anything below it is a countdown. Vendors disagree on which
45
+ * they send under the same header name, so magnitude decides. */
46
+ const EPOCH_SECONDS_FLOOR = 1_000_000_000;
47
+ /** A non-negative finite number, or nothing. The null guard carries weight:
48
+ * `Number("")` is 0, so an absent header would otherwise read as a real zero. */
49
+ function numeric(value) {
50
+ if (!value)
51
+ return undefined;
52
+ const n = Number(value);
53
+ return Number.isFinite(n) && n >= 0 ? n : undefined;
54
+ }
55
+ function unixSecondsToMs(value) {
56
+ const n = numeric(value);
57
+ return n === undefined ? undefined : n * 1000;
58
+ }
59
+ /** Absolute reset time from a header that spells it either way: epoch seconds
60
+ * (the unified reset, most gateways) or an RFC 3339 / HTTP-date string (the
61
+ * API-key reset headers). The digit test runs first because `Date.parse("120")`
62
+ * is not a rejection — it is the year 120, two millennia in the past. */
63
+ function resetHeaderMs(value, now) {
64
+ if (!value)
65
+ return undefined;
66
+ if (/^\d+$/.test(value)) {
67
+ const seconds = Number(value);
68
+ return seconds > EPOCH_SECONDS_FLOOR ? seconds * 1000 : now + seconds * 1000;
69
+ }
70
+ const at = Date.parse(value);
71
+ return Number.isNaN(at) ? undefined : at;
72
+ }
73
+ /** `retry-after` is delay-seconds, but RFC 9110 also allows an absolute date,
74
+ * and both forms occur across the providers this package speaks to. */
75
+ function parseRetryAfter(value, now) {
76
+ if (!value)
77
+ return undefined;
78
+ const n = Number(value);
79
+ if (Number.isFinite(n) && n >= 0)
80
+ return n * 1000;
81
+ const at = Date.parse(value);
82
+ return Number.isNaN(at) ? undefined : Math.max(0, at - now);
83
+ }
84
+ /**
85
+ * Parse the rate-limit headers off a (usually 429) response.
86
+ *
87
+ * Anthropic's `anthropic-ratelimit-unified-*` pair rides on every OAuth
88
+ * response, so both windows are always reported and only one of them is the
89
+ * reason for this 429. API-key accounts get the `anthropic-ratelimit-*-reset`
90
+ * RFC 3339 timestamps instead. Everything is optional — an unrecognized shape
91
+ * yields an empty result, and the caller falls back to its generic message.
92
+ */
93
+ export function parseRateLimitReset(headers, now = Date.now()) {
94
+ const result = {};
95
+ const after = parseRetryAfter(headers.get(RETRY_AFTER), now);
96
+ if (after !== undefined) {
97
+ result.retryAfterMs = after;
98
+ result.resetAtMs = now + after;
99
+ }
100
+ // The window with the higher utilization is the one a "would exceed your
101
+ // rate limit" 429 is about. A reset already in the past belongs to a window
102
+ // that rolled over between the response and this parse — naming it would put
103
+ // a past time in front of the user — so it drops out here.
104
+ const windows = [
105
+ {
106
+ window: "5h",
107
+ utilization: numeric(headers.get(`${UNIFIED_5H}utilization`)),
108
+ reset: unixSecondsToMs(headers.get(`${UNIFIED_5H}reset`)),
109
+ },
110
+ {
111
+ window: "weekly",
112
+ utilization: numeric(headers.get(`${UNIFIED_7D}utilization`)),
113
+ reset: unixSecondsToMs(headers.get(`${UNIFIED_7D}reset`)),
114
+ },
115
+ ].filter((w) => w.utilization !== undefined && w.reset !== undefined && w.reset > now);
116
+ const binding = windows.sort((a, b) => (b.utilization ?? 0) - (a.utilization ?? 0))[0];
117
+ if (binding) {
118
+ result.window = binding.window;
119
+ // The window's own reset outranks retry-after: it names the real horizon,
120
+ // where retry-after names the next polite attempt.
121
+ if (binding.reset !== undefined)
122
+ result.resetAtMs = binding.reset;
123
+ }
124
+ // No window named the horizon, so take the first reset header that dates the
125
+ // future. These stay below `retry-after`, which is the server speaking about
126
+ // this request; they only fill a silence.
127
+ if (result.resetAtMs === undefined) {
128
+ for (const name of RESET_HEADERS) {
129
+ const at = resetHeaderMs(headers.get(name), now);
130
+ if (at !== undefined && at > now) {
131
+ result.resetAtMs = at;
132
+ break;
133
+ }
134
+ }
135
+ }
136
+ return result;
137
+ }
138
+ /** Under ten minutes the wait is a per-minute throttle whatever the body calls
139
+ * it, and calling a 40-second retry a "5-hour window" would be its own lie. */
140
+ const WINDOW_FLOOR_MS = 10 * 60_000;
141
+ /** Bucket edges, padded: a window's reset lands wherever inside it the first
142
+ * request fell, so a 5-hour window routinely reports four hours and change. */
143
+ const FIVE_HOUR_MAX_MS = 5.5 * 3_600_000;
144
+ const WEEKLY_MAX_MS = 7.5 * 86_400_000;
145
+ /** A finite number off a JSON field, the string form a relay may stringify it
146
+ * into included. Bare `Number()` is the trap `numeric()` guards against one
147
+ * layer up: `Number(null)`, `Number("")`, `Number([])` and `Number(false)` are
148
+ * all 0, and 0 is finite and non-negative, so a relay that nulls out
149
+ * `resets_in_seconds` beside a real `resets_at` would win the branch below and
150
+ * report a three-day lockout as "retry now". */
151
+ function jsonSeconds(value) {
152
+ const usable = typeof value === "number" || (typeof value === "string" && value.trim() !== "");
153
+ const n = usable ? Number(value) : NaN;
154
+ return Number.isFinite(n) ? n : undefined;
155
+ }
156
+ /**
157
+ * ChatGPT's codex backend puts the reset IN THE 429 BODY, not the headers:
158
+ * `{"error":{"type":"usage_limit_reached","resets_at":1788801754,"resets_in_seconds":2501465}}`.
159
+ * The window name is inferred from the wait itself, and only past the floor
160
+ * above — a sub-minute retry is a throttle, not a subscription window.
161
+ */
162
+ export function parseUsageLimitBody(bodyText, now = Date.now()) {
163
+ let body;
164
+ try {
165
+ body = JSON.parse(bodyText);
166
+ }
167
+ catch {
168
+ return {};
169
+ }
170
+ if (typeof body !== "object" || body === null)
171
+ return {};
172
+ // The fields sit under `error` on the documented shape and at the top level
173
+ // on some gateway relays of it. Both are read rather than guessed between.
174
+ const source = body.error;
175
+ const error = typeof source === "object" && source !== null
176
+ ? source
177
+ : body;
178
+ const inSeconds = jsonSeconds(error.resets_in_seconds);
179
+ const atSeconds = jsonSeconds(error.resets_at);
180
+ const waitMs = inSeconds !== undefined && inSeconds >= 0
181
+ ? inSeconds * 1000
182
+ : atSeconds !== undefined && atSeconds > 0
183
+ ? Math.max(0, atSeconds * 1000 - now)
184
+ : undefined;
185
+ if (waitMs === undefined)
186
+ return {};
187
+ const result = { retryAfterMs: waitMs, resetAtMs: now + waitMs };
188
+ if (waitMs > WINDOW_FLOOR_MS) {
189
+ result.window =
190
+ waitMs <= FIVE_HOUR_MAX_MS ? "5h" : waitMs <= WEEKLY_MAX_MS ? "weekly" : "monthly";
191
+ }
192
+ return result;
193
+ }
194
+ //# sourceMappingURL=rate-limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"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"}