@providerkit/core 0.24.0 → 0.25.1
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 +14 -0
- package/dist/auth/device.d.ts +3 -2
- package/dist/auth/device.d.ts.map +1 -1
- package/dist/auth/device.js +5 -2
- package/dist/auth/device.js.map +1 -1
- package/dist/auth/flows.d.ts +5 -1
- package/dist/auth/flows.d.ts.map +1 -1
- package/dist/auth/flows.js +237 -0
- package/dist/auth/flows.js.map +1 -1
- package/dist/auth/oauth.d.ts +4 -2
- package/dist/auth/oauth.d.ts.map +1 -1
- package/dist/auth/oauth.js +4 -3
- package/dist/auth/oauth.js.map +1 -1
- package/dist/auth/with-token.js +5 -3
- package/dist/auth/with-token.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -0
- package/dist/errors.js.map +1 -1
- package/dist/presets.d.ts +53 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +100 -0
- package/dist/presets.js.map +1 -1
- package/dist/providers/factory.d.ts +16 -1
- package/dist/providers/factory.d.ts.map +1 -1
- package/dist/providers/factory.js +176 -18
- package/dist/providers/factory.js.map +1 -1
- package/dist/providers/gemini.d.ts +23 -5
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +247 -119
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +4 -3
- package/dist/providers/openai.js.map +1 -1
- package/dist/schema.d.ts +27 -9
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +197 -62
- package/dist/schema.js.map +1 -1
- package/package.json +1 -1
- package/src/auth/device.ts +8 -4
- package/src/auth/flows.ts +288 -1
- package/src/auth/oauth.ts +8 -3
- package/src/auth/with-token.ts +5 -3
- package/src/errors.ts +3 -0
- package/src/presets.ts +100 -0
- package/src/providers/factory.ts +233 -20
- package/src/providers/gemini.ts +280 -124
- package/src/providers/openai.ts +5 -3
- package/src/schema.ts +200 -72
package/src/providers/factory.ts
CHANGED
|
@@ -23,7 +23,15 @@ import {
|
|
|
23
23
|
type ProviderPreset,
|
|
24
24
|
type ProviderPresetId,
|
|
25
25
|
} from "../presets.ts";
|
|
26
|
-
import type {
|
|
26
|
+
import type {
|
|
27
|
+
ChatMessage,
|
|
28
|
+
Effort,
|
|
29
|
+
Provider,
|
|
30
|
+
ProviderChunk,
|
|
31
|
+
ServiceTier,
|
|
32
|
+
StreamOptions,
|
|
33
|
+
ToolDefinition,
|
|
34
|
+
} from "../types.ts";
|
|
27
35
|
import { withConfiguredFallbacks, type ProviderFallbackConfig } from "../fallback.ts";
|
|
28
36
|
|
|
29
37
|
export interface PresetProviderConfig extends ProviderFallbackConfig {
|
|
@@ -48,7 +56,7 @@ export interface PresetProviderConfig extends ProviderFallbackConfig {
|
|
|
48
56
|
headers?: Record<string, string>;
|
|
49
57
|
/**
|
|
50
58
|
* The session id for a call without `StreamOptions.sessionId`, on a preset
|
|
51
|
-
* that sends one (`opencode-go`, `chatgpt`). Absent, the provider mints its
|
|
59
|
+
* that sends one (`opencode-go`, `opencode-zen`, `chatgpt`). Absent, the provider mints its
|
|
52
60
|
* own when it is built. Pass one when you build the provider per attempt
|
|
53
61
|
* (`withKeyPool`, `withToken`): each build would mint a new id, and a gateway
|
|
54
62
|
* that pins a session to one upstream host then treats every call as a new
|
|
@@ -57,6 +65,8 @@ export interface PresetProviderConfig extends ProviderFallbackConfig {
|
|
|
57
65
|
* it and ignores it.
|
|
58
66
|
*/
|
|
59
67
|
instanceSessionId?: string;
|
|
68
|
+
/** Google Antigravity (Cloud Code Assist) project id for the request envelope. */
|
|
69
|
+
projectId?: string;
|
|
60
70
|
/**
|
|
61
71
|
* App attribution for OpenRouter's rankings. The site URL rides as
|
|
62
72
|
* `HTTP-Referer`, the app name as `X-Title`. Public, not secret. Sent on
|
|
@@ -132,6 +142,26 @@ export function presetAdapter(id: ProviderPresetId, config: PresetAdapterConfig)
|
|
|
132
142
|
"Remove serviceTier.",
|
|
133
143
|
);
|
|
134
144
|
}
|
|
145
|
+
let provider = buildWireAdapter(id, preset, wire, model, config, config.fetchImpl);
|
|
146
|
+
if (id === "opencode-zen") {
|
|
147
|
+
provider = wrapOpenCodeZen(provider, preset, wire, config, model);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Here rather than in `createPresetProvider`: the wire is resolved here, and
|
|
151
|
+
// the bare adapter is exported, so it must refuse on its own a per-call model
|
|
152
|
+
// it would post to the wrong endpoint. That 404 reads as an unknown model,
|
|
153
|
+
// and a pool benches the model for an hour.
|
|
154
|
+
return preset.routes ? refuseOtherWires(provider, preset, wire) : provider;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildWireAdapter(
|
|
158
|
+
id: ProviderPresetId,
|
|
159
|
+
preset: ProviderPreset,
|
|
160
|
+
wire: Wire,
|
|
161
|
+
model: string,
|
|
162
|
+
config: PresetAdapterConfig,
|
|
163
|
+
fetchImpl: typeof fetch | undefined,
|
|
164
|
+
): Provider {
|
|
135
165
|
const headers = { ...preset.headers, ...config.headers };
|
|
136
166
|
const baseUrl = config.baseUrl ?? wire.baseUrl;
|
|
137
167
|
const maxTokens = config.maxTokens ?? preset.maxTokens;
|
|
@@ -146,12 +176,11 @@ export function presetAdapter(id: ProviderPresetId, config: PresetAdapterConfig)
|
|
|
146
176
|
watchdog: undefined,
|
|
147
177
|
};
|
|
148
178
|
|
|
149
|
-
let provider: Provider;
|
|
150
179
|
switch (wire.shape) {
|
|
151
180
|
case "anthropic":
|
|
152
181
|
// `key` rides Anthropic's native x-api-key; coding plans and OAuth
|
|
153
182
|
// tokens read Bearer (measured across the family's coding endpoints).
|
|
154
|
-
|
|
183
|
+
return createAnthropicProvider({
|
|
155
184
|
...baseConfig,
|
|
156
185
|
apiKey: config.apiKey,
|
|
157
186
|
model,
|
|
@@ -165,11 +194,10 @@ export function presetAdapter(id: ProviderPresetId, config: PresetAdapterConfig)
|
|
|
165
194
|
// The endpoint's thinking dialect rides the table (zai: silence means
|
|
166
195
|
// the model default, which is ON — "none" must be said out loud).
|
|
167
196
|
...(preset.explicitNone ? { explicitNone: true } : {}),
|
|
168
|
-
fetchImpl
|
|
197
|
+
fetchImpl,
|
|
169
198
|
});
|
|
170
|
-
break;
|
|
171
199
|
case "openai":
|
|
172
|
-
|
|
200
|
+
return createOpenAIProvider({
|
|
173
201
|
...baseConfig,
|
|
174
202
|
id,
|
|
175
203
|
apiKey: config.apiKey,
|
|
@@ -183,11 +211,10 @@ export function presetAdapter(id: ProviderPresetId, config: PresetAdapterConfig)
|
|
|
183
211
|
...(preset.sessionHeader ? { sessionHeader: preset.sessionHeader } : {}),
|
|
184
212
|
...(preset.initiatorHeader ? { initiatorHeader: true } : {}),
|
|
185
213
|
...(preset.noSampling ? { noSampling: true } : {}),
|
|
186
|
-
fetchImpl
|
|
214
|
+
fetchImpl,
|
|
187
215
|
});
|
|
188
|
-
break;
|
|
189
216
|
case "responses":
|
|
190
|
-
|
|
217
|
+
return createResponsesProvider({
|
|
191
218
|
...baseConfig,
|
|
192
219
|
apiKey: config.apiKey,
|
|
193
220
|
model,
|
|
@@ -200,28 +227,214 @@ export function presetAdapter(id: ProviderPresetId, config: PresetAdapterConfig)
|
|
|
200
227
|
...(preset.sessionHeader ? { sessionHeader: preset.sessionHeader } : {}),
|
|
201
228
|
...(preset.imageLimits ? { imageLimits: preset.imageLimits } : {}),
|
|
202
229
|
...(preset.replayReasoning ? { replayReasoning: true } : {}),
|
|
203
|
-
fetchImpl
|
|
230
|
+
fetchImpl,
|
|
204
231
|
});
|
|
205
|
-
break;
|
|
206
232
|
case "gemini":
|
|
207
|
-
|
|
233
|
+
return createGeminiProvider({
|
|
208
234
|
...baseConfig,
|
|
209
235
|
apiKey: config.apiKey,
|
|
210
236
|
model,
|
|
211
237
|
effort: config.effort,
|
|
238
|
+
maxTokens,
|
|
212
239
|
baseUrl,
|
|
240
|
+
...(wire.path ? { path: wire.path } : {}),
|
|
241
|
+
bearer: wire.auth !== "key",
|
|
242
|
+
...(config.projectId ? { projectId: config.projectId } : {}),
|
|
243
|
+
...(preset.sessionHeader ? { sessionHeader: preset.sessionHeader } : {}),
|
|
213
244
|
id,
|
|
214
245
|
headers,
|
|
215
|
-
fetchImpl
|
|
246
|
+
fetchImpl,
|
|
216
247
|
});
|
|
217
|
-
break;
|
|
218
248
|
}
|
|
249
|
+
}
|
|
219
250
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
251
|
+
/**
|
|
252
|
+
* The User-Agent OpenCode Zen's free tier accepts. Exported for hosts that cannot set
|
|
253
|
+
* the header on `fetch` (Chromium ignores it) and stamp it another way.
|
|
254
|
+
*/
|
|
255
|
+
export const OPENCODE_CLI_USER_AGENT = "opencode/latest/2.0.32/cli";
|
|
256
|
+
const BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
257
|
+
|
|
258
|
+
/** Whether an OpenCode Zen model id belongs to the free shelf (`big-pickle` or `*-free`). */
|
|
259
|
+
export function isZenFreeModel(model: string): boolean {
|
|
260
|
+
return model === "big-pickle" || model.endsWith("-free");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Deterministically turns any conversation session id into OpenCode's
|
|
265
|
+
* `ses_<12 hex><14 base62>` identifier shape (`packages/schema/src/session-id.ts`),
|
|
266
|
+
* matching cc-proxy's `forge_opencode_session_id`.
|
|
267
|
+
*/
|
|
268
|
+
export async function forgeOpenCodeSessionId(sessionId?: string): Promise<string> {
|
|
269
|
+
if (sessionId && /^ses_[0-9a-f]{12}[0-9A-Za-z]{14}$/.test(sessionId)) {
|
|
270
|
+
return sessionId;
|
|
271
|
+
}
|
|
272
|
+
const seed = sessionId && sessionId.length > 0 ? sessionId : crypto.randomUUID();
|
|
273
|
+
const digest = new Uint8Array(
|
|
274
|
+
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(seed)),
|
|
275
|
+
);
|
|
276
|
+
let hexPart = "";
|
|
277
|
+
for (let i = 0; i < 6; i++) {
|
|
278
|
+
hexPart += digest[i]!.toString(16).padStart(2, "0");
|
|
279
|
+
}
|
|
280
|
+
let tail = "";
|
|
281
|
+
for (let i = 6; i < 20; i++) {
|
|
282
|
+
tail += BASE62[digest[i]! % 62];
|
|
283
|
+
}
|
|
284
|
+
return `ses_${hexPart}${tail}`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* OpenCode Zen's free tier answers only a streamed call whose tools include
|
|
289
|
+
* OpenCode's own `read` (`cc-proxy/src/providers/opencode/mod.rs`). A call with
|
|
290
|
+
* `Read` sends it as `read` and gets `Read` back; a call with neither gets a
|
|
291
|
+
* decoy `read` the model is told not to call (and `toolChoice: "none"` when the
|
|
292
|
+
* caller passed no tools).
|
|
293
|
+
*/
|
|
294
|
+
function offerOpenCodeRead(
|
|
295
|
+
messages: ChatMessage[],
|
|
296
|
+
tools: ToolDefinition[],
|
|
297
|
+
opts: StreamOptions,
|
|
298
|
+
): {
|
|
299
|
+
messages: ChatMessage[];
|
|
300
|
+
tools: ToolDefinition[];
|
|
301
|
+
opts: StreamOptions;
|
|
302
|
+
renamedRead: boolean;
|
|
303
|
+
} {
|
|
304
|
+
if (tools.some((t) => t.name === "read")) {
|
|
305
|
+
return { messages, tools, opts, renamedRead: false };
|
|
306
|
+
}
|
|
307
|
+
const hasClaudeRead = tools.some((t) => t.name === "Read");
|
|
308
|
+
if (!hasClaudeRead) {
|
|
309
|
+
const decoy: ToolDefinition = {
|
|
310
|
+
name: "read",
|
|
311
|
+
description: "Not available in this conversation. Never call it.",
|
|
312
|
+
inputSchema: { type: "object", properties: {} },
|
|
313
|
+
};
|
|
314
|
+
return {
|
|
315
|
+
messages,
|
|
316
|
+
tools: [...tools, decoy],
|
|
317
|
+
opts: tools.length === 0 ? { ...opts, toolChoice: "none" } : opts,
|
|
318
|
+
renamedRead: false,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const nextTools = tools.map((t) => (t.name === "Read" ? { ...t, name: "read" } : t));
|
|
322
|
+
const nextMessages: ChatMessage[] = messages.map((msg) => {
|
|
323
|
+
if (msg.role === "assistant" && msg.toolCalls?.some((c) => c.name === "Read")) {
|
|
324
|
+
return {
|
|
325
|
+
...msg,
|
|
326
|
+
toolCalls: msg.toolCalls.map((c) => (c.name === "Read" ? { ...c, name: "read" } : c)),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
if (msg.role === "tool" && msg.name === "Read") {
|
|
330
|
+
return { ...msg, name: "read" };
|
|
331
|
+
}
|
|
332
|
+
return msg;
|
|
333
|
+
});
|
|
334
|
+
const nextOpts =
|
|
335
|
+
opts.toolChoice && typeof opts.toolChoice === "object" && opts.toolChoice.name === "Read"
|
|
336
|
+
? { ...opts, toolChoice: { type: "tool" as const, name: "read" } }
|
|
337
|
+
: opts;
|
|
338
|
+
return { messages: nextMessages, tools: nextTools, opts: nextOpts, renamedRead: true };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function wrapOpenCodeZen(
|
|
342
|
+
baseProvider: Provider,
|
|
343
|
+
preset: ProviderPreset,
|
|
344
|
+
wire: Wire,
|
|
345
|
+
config: PresetAdapterConfig,
|
|
346
|
+
defaultModel: string,
|
|
347
|
+
): Provider {
|
|
348
|
+
const instanceSessionId = config.instanceSessionId ?? crypto.randomUUID();
|
|
349
|
+
const baseFetch = config.fetchImpl ?? fetch;
|
|
350
|
+
const callerUa = config.headers?.["user-agent"] ?? config.headers?.["User-Agent"];
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
...baseProvider,
|
|
354
|
+
async *createStream(messages, tools, opts = {}) {
|
|
355
|
+
const activeModel = opts.model ?? defaultModel;
|
|
356
|
+
const free = isZenFreeModel(activeModel);
|
|
357
|
+
const ses = await forgeOpenCodeSessionId(opts.sessionId ?? instanceSessionId);
|
|
358
|
+
const prepared = free
|
|
359
|
+
? offerOpenCodeRead(messages, tools, opts)
|
|
360
|
+
: { messages, tools, opts, renamedRead: false };
|
|
361
|
+
|
|
362
|
+
const runAttempt = async function* (anonymous: boolean): AsyncIterable<ProviderChunk> {
|
|
363
|
+
const fetchImpl: typeof fetch = (input, init) => {
|
|
364
|
+
const merged = new Headers(init?.headers);
|
|
365
|
+
if (!callerUa) merged.set("user-agent", OPENCODE_CLI_USER_AGENT);
|
|
366
|
+
merged.set("x-opencode-session", ses);
|
|
367
|
+
merged.set("x-opencode-session-id", ses);
|
|
368
|
+
merged.set("x-session-affinity", ses);
|
|
369
|
+
merged.set("x-session-id", ses);
|
|
370
|
+
merged.set("x-opencode-project", "global");
|
|
371
|
+
merged.set("x-opencode-client", "cli");
|
|
372
|
+
if (anonymous) {
|
|
373
|
+
merged.delete("authorization");
|
|
374
|
+
merged.delete("x-api-key");
|
|
375
|
+
merged.delete("x-goog-api-key");
|
|
376
|
+
}
|
|
377
|
+
const headerRecord: Record<string, string> = {};
|
|
378
|
+
merged.forEach((v, k) => {
|
|
379
|
+
headerRecord[k] = v;
|
|
380
|
+
});
|
|
381
|
+
return baseFetch(input, { ...init, headers: headerRecord });
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const bound = buildWireAdapter(
|
|
385
|
+
"opencode-zen",
|
|
386
|
+
preset,
|
|
387
|
+
wire,
|
|
388
|
+
defaultModel,
|
|
389
|
+
config,
|
|
390
|
+
fetchImpl,
|
|
391
|
+
);
|
|
392
|
+
for await (const chunk of bound.createStream(prepared.messages, prepared.tools, {
|
|
393
|
+
...prepared.opts,
|
|
394
|
+
sessionId: ses,
|
|
395
|
+
})) {
|
|
396
|
+
if (
|
|
397
|
+
prepared.renamedRead &&
|
|
398
|
+
chunk.type === "delta" &&
|
|
399
|
+
chunk.toolCalls?.some((c) => c.name === "read")
|
|
400
|
+
) {
|
|
401
|
+
yield {
|
|
402
|
+
...chunk,
|
|
403
|
+
toolCalls: chunk.toolCalls.map((c) =>
|
|
404
|
+
c.name === "read" ? { ...c, name: "Read" } : c,
|
|
405
|
+
),
|
|
406
|
+
};
|
|
407
|
+
} else {
|
|
408
|
+
yield chunk;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const startAnonymous = free && !config.apiKey;
|
|
414
|
+
let emitted = false;
|
|
415
|
+
try {
|
|
416
|
+
for await (const chunk of runAttempt(startAnonymous)) {
|
|
417
|
+
emitted = true;
|
|
418
|
+
yield chunk;
|
|
419
|
+
}
|
|
420
|
+
return;
|
|
421
|
+
} catch (err) {
|
|
422
|
+
// Free models accept a valid key (using the account's own free quota),
|
|
423
|
+
// and refuse a stale key with 401. Retry anonymously when that happens.
|
|
424
|
+
if (
|
|
425
|
+
free &&
|
|
426
|
+
!startAnonymous &&
|
|
427
|
+
!emitted &&
|
|
428
|
+
err instanceof ProviderError &&
|
|
429
|
+
err.status === 401
|
|
430
|
+
) {
|
|
431
|
+
yield* runAttempt(true);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
throw err;
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
};
|
|
225
438
|
}
|
|
226
439
|
|
|
227
440
|
/** The table is closed (`satisfies`), but a caller may hold an id as a plain
|