@providerkit/core 0.24.0 → 0.25.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 (48) hide show
  1. package/README.md +14 -0
  2. package/dist/auth/device.d.ts +3 -2
  3. package/dist/auth/device.d.ts.map +1 -1
  4. package/dist/auth/device.js +5 -2
  5. package/dist/auth/device.js.map +1 -1
  6. package/dist/auth/flows.d.ts +5 -1
  7. package/dist/auth/flows.d.ts.map +1 -1
  8. package/dist/auth/flows.js +237 -0
  9. package/dist/auth/flows.js.map +1 -1
  10. package/dist/auth/oauth.d.ts +4 -2
  11. package/dist/auth/oauth.d.ts.map +1 -1
  12. package/dist/auth/oauth.js +4 -3
  13. package/dist/auth/oauth.js.map +1 -1
  14. package/dist/auth/with-token.js +5 -3
  15. package/dist/auth/with-token.js.map +1 -1
  16. package/dist/errors.d.ts.map +1 -1
  17. package/dist/errors.js +3 -0
  18. package/dist/errors.js.map +1 -1
  19. package/dist/presets.d.ts +53 -0
  20. package/dist/presets.d.ts.map +1 -1
  21. package/dist/presets.js +100 -0
  22. package/dist/presets.js.map +1 -1
  23. package/dist/providers/factory.d.ts +11 -1
  24. package/dist/providers/factory.d.ts.map +1 -1
  25. package/dist/providers/factory.js +172 -18
  26. package/dist/providers/factory.js.map +1 -1
  27. package/dist/providers/gemini.d.ts +17 -5
  28. package/dist/providers/gemini.d.ts.map +1 -1
  29. package/dist/providers/gemini.js +241 -119
  30. package/dist/providers/gemini.js.map +1 -1
  31. package/dist/providers/openai.d.ts.map +1 -1
  32. package/dist/providers/openai.js +4 -3
  33. package/dist/providers/openai.js.map +1 -1
  34. package/dist/schema.d.ts +27 -9
  35. package/dist/schema.d.ts.map +1 -1
  36. package/dist/schema.js +197 -62
  37. package/dist/schema.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/auth/device.ts +8 -4
  40. package/src/auth/flows.ts +288 -1
  41. package/src/auth/oauth.ts +8 -3
  42. package/src/auth/with-token.ts +5 -3
  43. package/src/errors.ts +3 -0
  44. package/src/presets.ts +100 -0
  45. package/src/providers/factory.ts +229 -20
  46. package/src/providers/gemini.ts +273 -124
  47. package/src/providers/openai.ts +5 -3
  48. package/src/schema.ts +200 -72
@@ -23,7 +23,15 @@ import {
23
23
  type ProviderPreset,
24
24
  type ProviderPresetId,
25
25
  } from "../presets.ts";
26
- import type { Effort, Provider, ServiceTier } from "../types.ts";
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
- provider = createAnthropicProvider({
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: config.fetchImpl,
197
+ fetchImpl,
169
198
  });
170
- break;
171
199
  case "openai":
172
- provider = createOpenAIProvider({
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: config.fetchImpl,
214
+ fetchImpl,
187
215
  });
188
- break;
189
216
  case "responses":
190
- provider = createResponsesProvider({
217
+ return createResponsesProvider({
191
218
  ...baseConfig,
192
219
  apiKey: config.apiKey,
193
220
  model,
@@ -200,28 +227,210 @@ 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: config.fetchImpl,
230
+ fetchImpl,
204
231
  });
205
- break;
206
232
  case "gemini":
207
- provider = createGeminiProvider({
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: config.fetchImpl,
246
+ fetchImpl,
216
247
  });
217
- break;
218
248
  }
249
+ }
219
250
 
220
- // Here rather than in `createPresetProvider`: the wire is resolved here, and
221
- // the bare adapter is exported, so it must refuse on its own a per-call model
222
- // it would post to the wrong endpoint. That 404 reads as an unknown model,
223
- // and a pool benches the model for an hour.
224
- return preset.routes ? refuseOtherWires(provider, preset, wire) : provider;
251
+ const OPENCODE_CLI_USER_AGENT = "opencode/latest/2.0.32/cli";
252
+ const BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
253
+
254
+ /** Whether an OpenCode Zen model id belongs to the free shelf (`big-pickle` or `*-free`). */
255
+ export function isZenFreeModel(model: string): boolean {
256
+ return model === "big-pickle" || model.endsWith("-free");
257
+ }
258
+
259
+ /**
260
+ * Deterministically turns any conversation session id into OpenCode's
261
+ * `ses_<12 hex><14 base62>` identifier shape (`packages/schema/src/session-id.ts`),
262
+ * matching cc-proxy's `forge_opencode_session_id`.
263
+ */
264
+ export async function forgeOpenCodeSessionId(sessionId?: string): Promise<string> {
265
+ if (sessionId && /^ses_[0-9a-f]{12}[0-9A-Za-z]{14}$/.test(sessionId)) {
266
+ return sessionId;
267
+ }
268
+ const seed = sessionId && sessionId.length > 0 ? sessionId : crypto.randomUUID();
269
+ const digest = new Uint8Array(
270
+ await crypto.subtle.digest("SHA-256", new TextEncoder().encode(seed)),
271
+ );
272
+ let hexPart = "";
273
+ for (let i = 0; i < 6; i++) {
274
+ hexPart += digest[i]!.toString(16).padStart(2, "0");
275
+ }
276
+ let tail = "";
277
+ for (let i = 6; i < 20; i++) {
278
+ tail += BASE62[digest[i]! % 62];
279
+ }
280
+ return `ses_${hexPart}${tail}`;
281
+ }
282
+
283
+ /**
284
+ * OpenCode Zen's free tier answers only a streamed call whose tools include
285
+ * OpenCode's own `read` (`cc-proxy/src/providers/opencode/mod.rs`). A call with
286
+ * `Read` sends it as `read` and gets `Read` back; a call with neither gets a
287
+ * decoy `read` the model is told not to call (and `toolChoice: "none"` when the
288
+ * caller passed no tools).
289
+ */
290
+ function offerOpenCodeRead(
291
+ messages: ChatMessage[],
292
+ tools: ToolDefinition[],
293
+ opts: StreamOptions,
294
+ ): {
295
+ messages: ChatMessage[];
296
+ tools: ToolDefinition[];
297
+ opts: StreamOptions;
298
+ renamedRead: boolean;
299
+ } {
300
+ if (tools.some((t) => t.name === "read")) {
301
+ return { messages, tools, opts, renamedRead: false };
302
+ }
303
+ const hasClaudeRead = tools.some((t) => t.name === "Read");
304
+ if (!hasClaudeRead) {
305
+ const decoy: ToolDefinition = {
306
+ name: "read",
307
+ description: "Not available in this conversation. Never call it.",
308
+ inputSchema: { type: "object", properties: {} },
309
+ };
310
+ return {
311
+ messages,
312
+ tools: [...tools, decoy],
313
+ opts: tools.length === 0 ? { ...opts, toolChoice: "none" } : opts,
314
+ renamedRead: false,
315
+ };
316
+ }
317
+ const nextTools = tools.map((t) => (t.name === "Read" ? { ...t, name: "read" } : t));
318
+ const nextMessages: ChatMessage[] = messages.map((msg) => {
319
+ if (msg.role === "assistant" && msg.toolCalls?.some((c) => c.name === "Read")) {
320
+ return {
321
+ ...msg,
322
+ toolCalls: msg.toolCalls.map((c) => (c.name === "Read" ? { ...c, name: "read" } : c)),
323
+ };
324
+ }
325
+ if (msg.role === "tool" && msg.name === "Read") {
326
+ return { ...msg, name: "read" };
327
+ }
328
+ return msg;
329
+ });
330
+ const nextOpts =
331
+ opts.toolChoice && typeof opts.toolChoice === "object" && opts.toolChoice.name === "Read"
332
+ ? { ...opts, toolChoice: { type: "tool" as const, name: "read" } }
333
+ : opts;
334
+ return { messages: nextMessages, tools: nextTools, opts: nextOpts, renamedRead: true };
335
+ }
336
+
337
+ function wrapOpenCodeZen(
338
+ baseProvider: Provider,
339
+ preset: ProviderPreset,
340
+ wire: Wire,
341
+ config: PresetAdapterConfig,
342
+ defaultModel: string,
343
+ ): Provider {
344
+ const instanceSessionId = config.instanceSessionId ?? crypto.randomUUID();
345
+ const baseFetch = config.fetchImpl ?? fetch;
346
+ const callerUa = config.headers?.["user-agent"] ?? config.headers?.["User-Agent"];
347
+
348
+ return {
349
+ ...baseProvider,
350
+ async *createStream(messages, tools, opts = {}) {
351
+ const activeModel = opts.model ?? defaultModel;
352
+ const free = isZenFreeModel(activeModel);
353
+ const ses = await forgeOpenCodeSessionId(opts.sessionId ?? instanceSessionId);
354
+ const prepared = free
355
+ ? offerOpenCodeRead(messages, tools, opts)
356
+ : { messages, tools, opts, renamedRead: false };
357
+
358
+ const runAttempt = async function* (anonymous: boolean): AsyncIterable<ProviderChunk> {
359
+ const fetchImpl: typeof fetch = (input, init) => {
360
+ const merged = new Headers(init?.headers);
361
+ if (!callerUa) merged.set("user-agent", OPENCODE_CLI_USER_AGENT);
362
+ merged.set("x-opencode-session", ses);
363
+ merged.set("x-opencode-session-id", ses);
364
+ merged.set("x-session-affinity", ses);
365
+ merged.set("x-session-id", ses);
366
+ merged.set("x-opencode-project", "global");
367
+ merged.set("x-opencode-client", "cli");
368
+ if (anonymous) {
369
+ merged.delete("authorization");
370
+ merged.delete("x-api-key");
371
+ merged.delete("x-goog-api-key");
372
+ }
373
+ const headerRecord: Record<string, string> = {};
374
+ merged.forEach((v, k) => {
375
+ headerRecord[k] = v;
376
+ });
377
+ return baseFetch(input, { ...init, headers: headerRecord });
378
+ };
379
+
380
+ const bound = buildWireAdapter(
381
+ "opencode-zen",
382
+ preset,
383
+ wire,
384
+ defaultModel,
385
+ config,
386
+ fetchImpl,
387
+ );
388
+ for await (const chunk of bound.createStream(prepared.messages, prepared.tools, {
389
+ ...prepared.opts,
390
+ sessionId: ses,
391
+ })) {
392
+ if (
393
+ prepared.renamedRead &&
394
+ chunk.type === "delta" &&
395
+ chunk.toolCalls?.some((c) => c.name === "read")
396
+ ) {
397
+ yield {
398
+ ...chunk,
399
+ toolCalls: chunk.toolCalls.map((c) =>
400
+ c.name === "read" ? { ...c, name: "Read" } : c,
401
+ ),
402
+ };
403
+ } else {
404
+ yield chunk;
405
+ }
406
+ }
407
+ };
408
+
409
+ const startAnonymous = free && !config.apiKey;
410
+ let emitted = false;
411
+ try {
412
+ for await (const chunk of runAttempt(startAnonymous)) {
413
+ emitted = true;
414
+ yield chunk;
415
+ }
416
+ return;
417
+ } catch (err) {
418
+ // Free models accept a valid key (using the account's own free quota),
419
+ // and refuse a stale key with 401. Retry anonymously when that happens.
420
+ if (
421
+ free &&
422
+ !startAnonymous &&
423
+ !emitted &&
424
+ err instanceof ProviderError &&
425
+ err.status === 401
426
+ ) {
427
+ yield* runAttempt(true);
428
+ return;
429
+ }
430
+ throw err;
431
+ }
432
+ },
433
+ };
225
434
  }
226
435
 
227
436
  /** The table is closed (`satisfies`), but a caller may hold an id as a plain