@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.
- package/README.md +35 -158
- 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 +75 -2
- 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 +10 -6
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +30 -14
- 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 +38 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +122 -16
- 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/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +7 -2
- package/dist/types.js.map +1 -1
- package/dist/zod.d.ts +8 -1
- package/dist/zod.d.ts.map +1 -1
- package/dist/zod.js +9 -2
- package/dist/zod.js.map +1 -1
- package/package.json +2 -2
- package/src/context.ts +7 -0
- package/src/errors.ts +82 -2
- package/src/index.ts +4 -0
- package/src/key-pool.ts +272 -0
- package/src/providers/anthropic.ts +41 -20
- package/src/providers/gemini.ts +386 -0
- package/src/providers/openai.ts +153 -16
- package/src/providers/responses.ts +455 -0
- package/src/rate-limit.ts +217 -0
- package/src/transport.ts +61 -35
- package/src/types.ts +19 -2
- package/src/zod.ts +12 -2
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
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -71,6 +71,16 @@ export type ChatMessage =
|
|
|
71
71
|
* unsupported. `stripReasoning` below is that rule, once.
|
|
72
72
|
*/
|
|
73
73
|
reasoning?: string;
|
|
74
|
+
/**
|
|
75
|
+
* OpenRouter's normalized reasoning payload, arriving on the stream and
|
|
76
|
+
* riding back UNMODIFIED on the next turn's assistant message.
|
|
77
|
+
*
|
|
78
|
+
* Opaque on purpose — the same contract as Gemini's `thoughtSignature`,
|
|
79
|
+
* and for the same reason: it is the provider's own record of how it got
|
|
80
|
+
* here, and reading, reshaping or dropping it costs the model its
|
|
81
|
+
* continuity across a tool round. Absent on every other dialect.
|
|
82
|
+
*/
|
|
83
|
+
reasoningDetails?: unknown[];
|
|
74
84
|
toolCalls?: ToolCall[];
|
|
75
85
|
}
|
|
76
86
|
| {
|
|
@@ -131,6 +141,9 @@ export interface ProviderChunk {
|
|
|
131
141
|
type: "delta" | "usage" | "finish";
|
|
132
142
|
content?: string;
|
|
133
143
|
reasoning?: string;
|
|
144
|
+
/** OpenRouter's normalized reasoning payload — hand it back on the next
|
|
145
|
+
* turn's assistant message verbatim. See ChatMessage.reasoningDetails. */
|
|
146
|
+
reasoningDetails?: unknown[];
|
|
134
147
|
toolCalls?: ToolCallDelta[];
|
|
135
148
|
usage?: TokenUsage;
|
|
136
149
|
finishReason?: FinishReason;
|
|
@@ -219,8 +232,12 @@ export async function drainStream(
|
|
|
219
232
|
*/
|
|
220
233
|
export function stripReasoning(messages: readonly ChatMessage[]): ChatMessage[] {
|
|
221
234
|
return messages.map((message) => {
|
|
222
|
-
if (message.role !== "assistant"
|
|
223
|
-
|
|
235
|
+
if (message.role !== "assistant") return message;
|
|
236
|
+
if (message.reasoning === undefined && message.reasoningDetails === undefined) return message;
|
|
237
|
+
// Both halves go. `reasoningDetails` is the same chain of thought in the
|
|
238
|
+
// provider's own words, so leaving it behind carries into a thinking-off
|
|
239
|
+
// turn exactly what stripping `reasoning` was meant to keep out.
|
|
240
|
+
const { reasoning: _text, reasoningDetails: _payload, ...rest } = message;
|
|
224
241
|
return rest;
|
|
225
242
|
});
|
|
226
243
|
}
|
package/src/zod.ts
CHANGED
|
@@ -8,9 +8,19 @@ import { defineTool, type Tool, type ToolContext } from "./tools.ts";
|
|
|
8
8
|
import { clampToSchema } from "./schema.ts";
|
|
9
9
|
import type { JsonObjectSchema } from "./types.ts";
|
|
10
10
|
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* A zod schema as the JSON Schema every provider's tool contract wants.
|
|
13
|
+
*
|
|
14
|
+
* `$schema` is dropped. zod emits the dialect URI at the root, and a provider
|
|
15
|
+
* validating a tool's `parameters` against its own supported subset — OpenAI
|
|
16
|
+
* under `strict: true`, Gemini's `parametersJsonSchema` — rejects the whole
|
|
17
|
+
* tool over that one key, with a message that names neither zod nor the field.
|
|
18
|
+
*/
|
|
12
19
|
export function toJsonObjectSchema(schema: z.ZodType, label = "schema"): JsonObjectSchema {
|
|
13
|
-
const json = z.toJSONSchema(schema, { io: "input" }) as Record<
|
|
20
|
+
const { $schema: _dialect, ...json } = z.toJSONSchema(schema, { io: "input" }) as Record<
|
|
21
|
+
string,
|
|
22
|
+
unknown
|
|
23
|
+
>;
|
|
14
24
|
if (json.type !== "object") {
|
|
15
25
|
// Every provider requires an object at the top level of a tool's
|
|
16
26
|
// parameters; a bare string or array is rejected at the wire, far from
|