@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.
- package/README.md +33 -157
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +8 -0
- package/dist/context.js.map +1 -1
- package/dist/errors.d.ts +36 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +72 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/key-pool.d.ts +60 -0
- package/dist/key-pool.d.ts.map +1 -0
- package/dist/key-pool.js +235 -0
- package/dist/key-pool.js.map +1 -0
- package/dist/providers/anthropic.d.ts +9 -0
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +12 -13
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/gemini.d.ts +40 -0
- package/dist/providers/gemini.d.ts.map +1 -0
- package/dist/providers/gemini.js +303 -0
- package/dist/providers/gemini.js.map +1 -0
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +9 -0
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/responses.d.ts +38 -0
- package/dist/providers/responses.d.ts.map +1 -0
- package/dist/providers/responses.js +341 -0
- package/dist/providers/responses.js.map +1 -0
- package/dist/rate-limit.d.ts +29 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +194 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/transport.d.ts +18 -5
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +61 -35
- package/dist/transport.js.map +1 -1
- package/package.json +2 -2
- package/src/context.ts +7 -0
- package/src/errors.ts +79 -1
- package/src/index.ts +4 -0
- package/src/key-pool.ts +272 -0
- package/src/providers/anthropic.ts +21 -18
- package/src/providers/gemini.ts +386 -0
- package/src/providers/openai.ts +12 -0
- package/src/providers/responses.ts +455 -0
- package/src/rate-limit.ts +217 -0
- 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 {
|
|
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 =
|
|
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
|
-
*
|
|
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
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
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*
|
|
112
|
-
const
|
|
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
|
-
|
|
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
|
+
}
|