@providerkit/core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +33 -157
  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 +72 -1
  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 +9 -0
  18. package/dist/providers/anthropic.d.ts.map +1 -1
  19. package/dist/providers/anthropic.js +12 -13
  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.map +1 -1
  26. package/dist/providers/openai.js +9 -0
  27. package/dist/providers/openai.js.map +1 -1
  28. package/dist/providers/responses.d.ts +38 -0
  29. package/dist/providers/responses.d.ts.map +1 -0
  30. package/dist/providers/responses.js +341 -0
  31. package/dist/providers/responses.js.map +1 -0
  32. package/dist/rate-limit.d.ts +29 -0
  33. package/dist/rate-limit.d.ts.map +1 -0
  34. package/dist/rate-limit.js +194 -0
  35. package/dist/rate-limit.js.map +1 -0
  36. package/dist/transport.d.ts +18 -5
  37. package/dist/transport.d.ts.map +1 -1
  38. package/dist/transport.js +61 -35
  39. package/dist/transport.js.map +1 -1
  40. package/package.json +2 -2
  41. package/src/context.ts +7 -0
  42. package/src/errors.ts +79 -1
  43. package/src/index.ts +4 -0
  44. package/src/key-pool.ts +272 -0
  45. package/src/providers/anthropic.ts +21 -18
  46. package/src/providers/gemini.ts +386 -0
  47. package/src/providers/openai.ts +12 -0
  48. package/src/providers/responses.ts +455 -0
  49. package/src/rate-limit.ts +217 -0
  50. package/src/transport.ts +61 -35
package/src/transport.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  //
5
5
  // Adapters keep only their per-event mapping; everything about being an HTTP
6
6
  // client lives here once.
7
- import { classify, isTransportFailure, ProviderError } from "./errors.ts";
7
+ import { classifyHttp, isTransportFailure, ProviderError } from "./errors.ts";
8
8
 
9
9
  export interface RequestInit_ {
10
10
  url: string;
@@ -62,7 +62,7 @@ export function retryAfterFromHeaders(headers: Headers, now = Date.now()): numbe
62
62
  /** Turn a non-2xx response into the classified error every caller branches on. */
63
63
  async function errorFor(provider: string, res: Response): Promise<ProviderError> {
64
64
  const text = await res.text().catch(() => "");
65
- const kind = classify({ status: res.status, error: text }, res.status, text);
65
+ const kind = classifyHttp(res.status, text);
66
66
  const message = text
67
67
  ? `${provider} ${res.status}: ${text.slice(0, 500)}`
68
68
  : `${provider} ${res.status} ${res.statusText}`;
@@ -100,48 +100,57 @@ export async function postJson<T = unknown>(opts: RequestInit_): Promise<T> {
100
100
  return (await res.json()) as T;
101
101
  }
102
102
 
103
+ /** Pull the `data:` payload out of one SSE frame, joining continuation lines
104
+ * the way the spec says to. Returns null for a comment or a frame carrying
105
+ * only an `event:` name. */
106
+ function payloadOf(frame: string): string | null {
107
+ const parts: string[] = [];
108
+ for (const line of frame.split("\n")) {
109
+ if (!line.startsWith("data:")) continue;
110
+ parts.push(line.slice(5).replace(/^ /, ""));
111
+ }
112
+ // Gemini abandons the framing to report a mid-stream failure: the
113
+ // google.rpc.Status is appended as a bare JSON object with no `data:` on
114
+ // it. Dropped here it never reaches an adapter, and a 429 or a 503 that
115
+ // lands after the headers reads as an empty, successful turn. Requiring an
116
+ // object keeps comments and `event:`-only frames returning null.
117
+ if (parts.length === 0) {
118
+ const bare = frame.trim();
119
+ return bare.startsWith("{") ? bare : null;
120
+ }
121
+ const payload = parts.join("\n").trim();
122
+ return payload.length > 0 ? payload : null;
123
+ }
124
+
103
125
  /**
104
- * POST an SSE request and yield each `data:` payload, trimmed.
126
+ * Yield each `data:` payload of an SSE body, trimmed plus the bare JSON
127
+ * object a vendor appends outside the framing, which is only ever an error
128
+ * (see `payloadOf`).
129
+ *
130
+ * Frames are split on the blank line the spec requires, so a payload containing
131
+ * a bare newline survives; `[DONE]` is swallowed here rather than in every
132
+ * adapter. CRLF is normalized — some gateways send it, and a `\r` left on the
133
+ * end of a JSON payload is a parse error nobody enjoys debugging.
105
134
  *
106
- * Frames are split on the blank line the spec requires, so a payload
107
- * containing a bare newline survives; `[DONE]` is swallowed here rather than in
108
- * every adapter. CRLF is normalized some gateways send it, and a `\r` left on
109
- * the end of a JSON payload is a parse error nobody enjoys debugging.
135
+ * Exported apart from `streamSse` because the envelope above it is the half an
136
+ * adopting app most often cannot take: an app with its own translated error
137
+ * copy, its own log levels, or its own auth refresh has to keep building the
138
+ * request and reading the failure itself. Framing is the half nobody should
139
+ * write twice — hand it a `res.body` and keep your own envelope.
110
140
  */
111
- export async function* streamSse(opts: RequestInit_): AsyncGenerator<string> {
112
- const res = await send(opts);
113
- if (!res.ok) throw await errorFor(opts.provider, res);
114
- // A 2xx with no body at all is an upstream anomaly, not a request we got
115
- // wrong — worth the same retry a 5xx gets.
116
- if (!res.body) {
117
- throw new ProviderError(opts.provider, "overload", `${opts.provider}: empty response body`, {
118
- status: res.status,
119
- });
120
- }
121
-
122
- const reader = res.body.getReader();
141
+ export async function* parseSseStream(body: ReadableStream<Uint8Array>): AsyncGenerator<string> {
142
+ const reader = body.getReader();
123
143
  const decoder = new TextDecoder();
124
144
  let buffer = "";
125
-
126
- /** Pull the `data:` payload out of one SSE frame, joining continuation
127
- * lines the way the spec says to. Returns null for a comment or a frame
128
- * carrying only an `event:` name. */
129
- function payloadOf(frame: string): string | null {
130
- const parts: string[] = [];
131
- for (const line of frame.split("\n")) {
132
- if (!line.startsWith("data:")) continue;
133
- parts.push(line.slice(5).replace(/^ /, ""));
134
- }
135
- if (parts.length === 0) return null;
136
- const payload = parts.join("\n").trim();
137
- return payload.length > 0 ? payload : null;
138
- }
139
-
140
145
  try {
141
146
  for (;;) {
142
147
  const { done, value } = await reader.read();
143
148
  if (done) break;
144
- buffer += decoder.decode(value, { stream: true }).replace(/\r\n/g, "\n");
149
+ // Normalized on the BUFFER, not on the decoded read: a network read can
150
+ // end between the CR and the LF, and normalizing each read separately
151
+ // leaves that CR stranded on the end of a payload — a JSON parse error
152
+ // that only ever reproduces under a particular packet split.
153
+ buffer = (buffer + decoder.decode(value, { stream: true })).replace(/\r\n/g, "\n");
145
154
 
146
155
  let boundary = buffer.indexOf("\n\n");
147
156
  while (boundary !== -1) {
@@ -160,3 +169,20 @@ export async function* streamSse(opts: RequestInit_): AsyncGenerator<string> {
160
169
  reader.releaseLock();
161
170
  }
162
171
  }
172
+
173
+ /**
174
+ * POST an SSE request and stream its payloads: the envelope (auth, the
175
+ * classified error, the retry hints) plus `parseSseStream`.
176
+ */
177
+ export async function* streamSse(opts: RequestInit_): AsyncGenerator<string> {
178
+ const res = await send(opts);
179
+ if (!res.ok) throw await errorFor(opts.provider, res);
180
+ // A 2xx with no body at all is an upstream anomaly, not a request we got
181
+ // wrong — worth the same retry a 5xx gets.
182
+ if (!res.body) {
183
+ throw new ProviderError(opts.provider, "overload", `${opts.provider}: empty response body`, {
184
+ status: res.status,
185
+ });
186
+ }
187
+ yield* parseSseStream(res.body);
188
+ }