@providerkit/core 0.23.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.
- package/README.md +43 -7
- 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.d.ts +4 -1
- package/dist/auth/with-token.d.ts.map +1 -1
- package/dist/auth/with-token.js +9 -4
- 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/gate.d.ts +29 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +96 -0
- package/dist/gate.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/key-pool.d.ts +11 -0
- package/dist/key-pool.d.ts.map +1 -1
- package/dist/key-pool.js +14 -0
- package/dist/key-pool.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/anthropic.d.ts +7 -0
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +3 -1
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/factory.d.ts +36 -0
- package/dist/providers/factory.d.ts.map +1 -1
- package/dist/providers/factory.js +208 -22
- package/dist/providers/factory.js.map +1 -1
- package/dist/providers/gemini.d.ts +17 -5
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +241 -119
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/openai.d.ts +7 -0
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +7 -4
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/responses.d.ts +11 -0
- package/dist/providers/responses.d.ts.map +1 -1
- package/dist/providers/responses.js +3 -1
- package/dist/providers/responses.js.map +1 -1
- package/dist/registry.d.ts +105 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +291 -0
- package/dist/registry.js.map +1 -0
- 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/dist/tools.d.ts +8 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +2 -2
- package/dist/tools.js.map +1 -1
- package/dist/turn.d.ts +55 -0
- package/dist/turn.d.ts.map +1 -0
- package/dist/turn.js +125 -0
- package/dist/turn.js.map +1 -0
- package/dist/types.d.ts +8 -22
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -45
- package/dist/types.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 +9 -4
- package/src/errors.ts +3 -0
- package/src/gate.ts +152 -0
- package/src/index.ts +3 -0
- package/src/key-pool.ts +15 -0
- package/src/presets.ts +100 -0
- package/src/providers/anthropic.ts +10 -1
- package/src/providers/factory.ts +288 -34
- package/src/providers/gemini.ts +273 -124
- package/src/providers/openai.ts +15 -4
- package/src/providers/responses.ts +14 -1
- package/src/registry.ts +420 -0
- package/src/schema.ts +200 -72
- package/src/tools.ts +10 -2
- package/src/turn.ts +166 -0
- package/src/types.ts +8 -62
package/src/providers/gemini.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// and the SDK is a Node-shaped one.
|
|
8
8
|
import { repairHistory } from "../context.ts";
|
|
9
9
|
import { ProviderError, streamCut, streamError, type ErrorKind } from "../errors.ts";
|
|
10
|
-
import { schemaPrompt, toGeminiToolSchema } from "../schema.ts";
|
|
10
|
+
import { schemaPrompt, toAntigravityToolSchema, toGeminiToolSchema } from "../schema.ts";
|
|
11
11
|
import { parseToolArgs, settleToolCalls } from "../tool-args.ts";
|
|
12
12
|
import { streamSse, apiUrl } from "../transport.ts";
|
|
13
13
|
import { withConfiguredFallbacks, type ProviderFallbackConfig } from "../fallback.ts";
|
|
@@ -28,12 +28,20 @@ export interface GeminiConfig extends ProviderFallbackConfig {
|
|
|
28
28
|
apiKey: string;
|
|
29
29
|
model: string;
|
|
30
30
|
/** Any endpoint speaking the Generative Language REST dialect, including a proxy or
|
|
31
|
-
* gateway. Defaults to the Generative Language API.
|
|
32
|
-
*
|
|
33
|
-
* Not Vertex: it serves `/v1/projects/…/locations/…/publishers/google/models`
|
|
34
|
-
* and authenticates with a Bearer token, and both the path and the
|
|
35
|
-
* `x-goog-api-key` header are fixed below. Vertex would be its own adapter. */
|
|
31
|
+
* gateway. Defaults to the Generative Language API. */
|
|
36
32
|
baseUrl?: string;
|
|
33
|
+
/** Explicit path override when a backend has custom routing (Antigravity:
|
|
34
|
+
* `/v1internal:streamGenerateContent?alt=sse`). */
|
|
35
|
+
path?: string;
|
|
36
|
+
/** Send the credential as `Authorization: Bearer` instead of `x-goog-api-key`
|
|
37
|
+
* (Antigravity OAuth). */
|
|
38
|
+
bearer?: boolean;
|
|
39
|
+
/** Google Antigravity (Cloud Code Assist) project id for the request envelope. */
|
|
40
|
+
projectId?: string;
|
|
41
|
+
/** Header that carries `StreamOptions.sessionId`. */
|
|
42
|
+
sessionHeader?: string;
|
|
43
|
+
/** The id `sessionHeader` carries when a call has no `sessionId`. */
|
|
44
|
+
instanceSessionId?: string;
|
|
37
45
|
/** Names the provider in errors and logs. */
|
|
38
46
|
id?: string;
|
|
39
47
|
/** Bound default; a per-call `effort` overrides it. */
|
|
@@ -53,23 +61,77 @@ export interface GeminiConfig extends ProviderFallbackConfig {
|
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com";
|
|
64
|
+
const ANTIGRAVITY_DAILY_API = "https://daily-cloudcode-pa.googleapis.com";
|
|
65
|
+
const ANTIGRAVITY_PROD_API = "https://cloudcode-pa.googleapis.com";
|
|
66
|
+
const ANTIGRAVITY_IDE_USER_AGENT =
|
|
67
|
+
"antigravity/ide/2.5.5 (os_type=windows; arch=amd64; aidev_client; auth_method=oauth)";
|
|
68
|
+
const ANTIGRAVITY_CLI_USER_AGENT =
|
|
69
|
+
"antigravity/cli/1.3.4 (os_type=darwin; arch=arm64; aidev_client; auth_method=oauth)";
|
|
70
|
+
const REJECTED_CLAUDE_SDK_PARAGRAPH =
|
|
71
|
+
"You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
72
|
+
|
|
73
|
+
const ANTIGRAVITY_WIRE_MODELS: Record<string, string> = {
|
|
74
|
+
"gemini-4-argon": "gemini-4-argon-high",
|
|
75
|
+
"gemini-3.8-flash": "gemini-3.8-flash-medium",
|
|
76
|
+
"gemini-3.7-flash": "gemini-3.7-flash-tiered",
|
|
77
|
+
"gemini-3.1-pro": "gemini-pro-agent",
|
|
78
|
+
"claude-opus-5-5": "claude-opus-5-5-high",
|
|
79
|
+
"claude-sonnet-5-5": "claude-sonnet-5-5-high",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Resolve an Antigravity model id to its Cloud Code Assist wire model id. */
|
|
83
|
+
export function resolveAntigravityWireModel(model: string): string {
|
|
84
|
+
const stripped = model.replace(/^antigravity\//, "");
|
|
85
|
+
return ANTIGRAVITY_WIRE_MODELS[stripped] ?? stripped;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Deterministic Antigravity session id (`-<uint64>`), matching cc-proxy. */
|
|
89
|
+
export async function antigravitySessionId(sessionId?: string): Promise<string> {
|
|
90
|
+
const seed = sessionId && sessionId.length > 0 ? sessionId : crypto.randomUUID();
|
|
91
|
+
const digest = new Uint8Array(
|
|
92
|
+
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(seed)),
|
|
93
|
+
);
|
|
94
|
+
const view = new DataView(digest.buffer, digest.byteOffset, 8);
|
|
95
|
+
const masked = view.getBigUint64(0, false) & 0x7fffffffffffffffn;
|
|
96
|
+
return `-${masked.toString()}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function cleanAntigravitySystemText(text: string): string {
|
|
100
|
+
return text
|
|
101
|
+
.replace(/^x-anthropic-billing-header:[^\n]*\n*/, "")
|
|
102
|
+
.split("\n\n")
|
|
103
|
+
.filter((p) => p !== REJECTED_CLAUDE_SDK_PARAGRAPH)
|
|
104
|
+
.join("\n\n");
|
|
105
|
+
}
|
|
56
106
|
|
|
57
107
|
/**
|
|
58
|
-
* Gemini 3
|
|
59
|
-
*
|
|
60
|
-
*
|
|
108
|
+
* Gemini 3+ thinking dial.
|
|
109
|
+
*
|
|
110
|
+
* Measured across all 17 text models on 2026-10-21:
|
|
111
|
+
* - Only `gemini-3-flash*`, `gemini-3.5-flash*`, `gemini-3.6-flash`, and the
|
|
112
|
+
* `flash-lite` models accept `MINIMAL` (which spends 0 thought tokens).
|
|
113
|
+
* - `gemini-3.7-flash`, `gemini-3.8-flash`, `gemini-4-*`, `gemini-flash-latest`,
|
|
114
|
+
* and every Pro model answer `MINIMAL` with `400 Thinking level MINIMAL is not
|
|
115
|
+
* supported for this model`, and take `LOW` as their lowest level.
|
|
61
116
|
*
|
|
62
117
|
* Sent as the REST enum NAMES. The SDK's `ThinkingLevel` members serialize to
|
|
63
118
|
* exactly these strings, so nothing is lost by writing them out.
|
|
64
119
|
*/
|
|
65
|
-
const THINKING_LEVEL: Record<Effort, string> = {
|
|
66
|
-
none: "MINIMAL",
|
|
120
|
+
const THINKING_LEVEL: Record<Exclude<Effort, "none">, string> = {
|
|
67
121
|
low: "LOW",
|
|
68
122
|
medium: "MEDIUM",
|
|
69
123
|
high: "HIGH",
|
|
70
124
|
max: "HIGH",
|
|
71
125
|
};
|
|
72
126
|
|
|
127
|
+
const TAKES_MINIMAL_THINKING =
|
|
128
|
+
/^gemini-(?:3(?:\.[056])?-flash(?:-preview|-lite(?:-preview)?)?|3\.1-flash-lite(?:-preview)?|flash-lite-latest)$/i;
|
|
129
|
+
|
|
130
|
+
function geminiThinkingLevel(model: string, effort: Effort): string {
|
|
131
|
+
if (effort !== "none") return THINKING_LEVEL[effort];
|
|
132
|
+
return TAKES_MINIMAL_THINKING.test(model) ? "MINIMAL" : "LOW";
|
|
133
|
+
}
|
|
134
|
+
|
|
73
135
|
/**
|
|
74
136
|
* Gemini 2.x thinking budget dial (tokens). Gemini 2.5 takes `thinkingBudget`
|
|
75
137
|
* (0 disables thinking) and has no level. Every newer model takes
|
|
@@ -327,6 +389,9 @@ interface GeminiResponse {
|
|
|
327
389
|
export function createGeminiProvider(config: GeminiConfig): Provider {
|
|
328
390
|
const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
329
391
|
const id = config.id ?? "gemini";
|
|
392
|
+
const isAntigravity = id === "antigravity";
|
|
393
|
+
const instanceSessionId =
|
|
394
|
+
config.sessionHeader || isAntigravity ? (config.instanceSessionId ?? crypto.randomUUID()) : "";
|
|
330
395
|
|
|
331
396
|
const provider: Provider = {
|
|
332
397
|
id,
|
|
@@ -347,6 +412,7 @@ export function createGeminiProvider(config: GeminiConfig): Provider {
|
|
|
347
412
|
// the doubled segment 404s as "model not found" — a confusing way to
|
|
348
413
|
// learn about a prefix.
|
|
349
414
|
const model = (opts.model ?? config.model).replace(/^models\//, "");
|
|
415
|
+
const wireModel = isAntigravity ? resolveAntigravityWireModel(model) : model;
|
|
350
416
|
const isGemini3 = /gemini-3/i.test(model);
|
|
351
417
|
// Not `!isGemini3`: `gemini-flash-latest` and models newer than 3 are not
|
|
352
418
|
// Gemini 3 either, and they must get a level.
|
|
@@ -361,10 +427,10 @@ export function createGeminiProvider(config: GeminiConfig): Provider {
|
|
|
361
427
|
// No effort means the model's own dynamic thinking. Sending MINIMAL / 0 here
|
|
362
428
|
// would switch that off for a caller who never asked, which is the whole
|
|
363
429
|
// reason the seam treats an absent effort as "never sent".
|
|
364
|
-
if (effort) {
|
|
430
|
+
if (effort && !isAntigravity) {
|
|
365
431
|
generationConfig.thinkingConfig = isGemini2
|
|
366
432
|
? { thinkingBudget: GEMINI_2_THINKING_BUDGET[effort] }
|
|
367
|
-
: { thinkingLevel:
|
|
433
|
+
: { thinkingLevel: geminiThinkingLevel(model, effort) };
|
|
368
434
|
}
|
|
369
435
|
// A schema that shares its call with tools pins the decoder, and a pinned
|
|
370
436
|
// decoder cannot emit a functionCall — the model narrates the call it
|
|
@@ -399,8 +465,9 @@ export function createGeminiProvider(config: GeminiConfig): Provider {
|
|
|
399
465
|
// The prompt-carried schema rides as a second part, after the caller's
|
|
400
466
|
// own text: this shape's cache is a prefix too, so a per-call schema in
|
|
401
467
|
// front of the conversation would cold-start it on every change.
|
|
468
|
+
const cleanedSystem = system && isAntigravity ? cleanAntigravitySystemText(system) : system;
|
|
402
469
|
const systemParts = [
|
|
403
|
-
...(
|
|
470
|
+
...(cleanedSystem ? [{ text: cleanedSystem }] : []),
|
|
404
471
|
...(opts.json && promptCarried ? [{ text: schemaPrompt(opts.json.schema) }] : []),
|
|
405
472
|
];
|
|
406
473
|
if (systemParts.length > 0) request.systemInstruction = { parts: systemParts };
|
|
@@ -420,9 +487,12 @@ export function createGeminiProvider(config: GeminiConfig): Provider {
|
|
|
420
487
|
functionDeclarations: tools.map((tool) => ({
|
|
421
488
|
name: tool.name,
|
|
422
489
|
description: tool.description,
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
parametersJsonSchema
|
|
490
|
+
// Antigravity (Cloud Code Assist) still expects the OpenAPI-subset
|
|
491
|
+
// `parameters` field; the Gemini API and OpenCode Zen take standard
|
|
492
|
+
// JSON Schema in `parametersJsonSchema` (cc-proxy 0.2.8).
|
|
493
|
+
...(isAntigravity
|
|
494
|
+
? { parameters: toAntigravityToolSchema(tool.inputSchema) }
|
|
495
|
+
: { parametersJsonSchema: toGeminiToolSchema(tool.inputSchema) }),
|
|
426
496
|
})),
|
|
427
497
|
},
|
|
428
498
|
];
|
|
@@ -432,119 +502,198 @@ export function createGeminiProvider(config: GeminiConfig): Provider {
|
|
|
432
502
|
if (toolConfig) request.toolConfig = toolConfig;
|
|
433
503
|
}
|
|
434
504
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
let sawFunctionCalls = false;
|
|
439
|
-
let callIndex = 0;
|
|
440
|
-
let finished = false;
|
|
505
|
+
const rawHeaders = { ...config.headers };
|
|
506
|
+
const headerProjectId = rawHeaders["x-antigravity-project"];
|
|
507
|
+
delete rawHeaders["x-antigravity-project"];
|
|
441
508
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
body: request,
|
|
448
|
-
provider: id,
|
|
449
|
-
...(opts.signal ? { signal: opts.signal } : {}),
|
|
450
|
-
...(config.fetchImpl ? { fetchImpl: config.fetchImpl } : {}),
|
|
451
|
-
...(opts.onActivity ? { onActivity: opts.onActivity } : {}),
|
|
452
|
-
...(opts.onRateLimit ? { onRateLimit: opts.onRateLimit } : {}),
|
|
453
|
-
// This wire has no terminal event to stop at, so the finish reason is
|
|
454
|
-
// the end: a read that dies after it can cost a late usage record, never
|
|
455
|
-
// the answer.
|
|
456
|
-
finished: () => finished,
|
|
457
|
-
})) {
|
|
458
|
-
let chunk: GeminiResponse;
|
|
459
|
-
try {
|
|
460
|
-
chunk = JSON.parse(data) as GeminiResponse;
|
|
461
|
-
} catch {
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// `?alt=sse` commits to 200 the moment the headers go out, so a
|
|
466
|
-
// throttle or an overload landing after that arrives here as a
|
|
467
|
-
// google.rpc.Status in the body rather than as a status line.
|
|
468
|
-
if (chunk.error) throw streamError(id, chunk.error);
|
|
469
|
-
// A blocked PROMPT has no candidate to finish, so it reads as a cut
|
|
470
|
-
// stream — retried, into the same block. It is a content failure.
|
|
471
|
-
const blocked = chunk.promptFeedback?.blockReason;
|
|
472
|
-
if (blocked) {
|
|
473
|
-
throw new ProviderError(id, "content", `${id}: the prompt was blocked (${blocked})`, {
|
|
474
|
-
code: blocked,
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
if (chunk.usageMetadata) {
|
|
479
|
-
const usage = chunk.usageMetadata;
|
|
480
|
-
yield {
|
|
481
|
-
type: "usage",
|
|
482
|
-
usage: {
|
|
483
|
-
inputTokens: usage.promptTokenCount ?? 0,
|
|
484
|
-
// Thoughts bill as OUTPUT, and Gemini reports them OUTSIDE
|
|
485
|
-
// candidatesTokenCount — leaving them out undercounts a thinking
|
|
486
|
-
// turn by most of what it cost.
|
|
487
|
-
outputTokens: (usage.candidatesTokenCount ?? 0) + (usage.thoughtsTokenCount ?? 0),
|
|
488
|
-
cachedInputTokens: usage.cachedContentTokenCount ?? 0,
|
|
489
|
-
...(usage.thoughtsTokenCount !== undefined
|
|
490
|
-
? { reasoningTokens: usage.thoughtsTokenCount }
|
|
491
|
-
: {}),
|
|
492
|
-
},
|
|
493
|
-
};
|
|
509
|
+
let wireBody: Record<string, unknown> = request;
|
|
510
|
+
if (isAntigravity) {
|
|
511
|
+
request.sessionId = await antigravitySessionId(opts.sessionId ?? instanceSessionId);
|
|
512
|
+
if (wireModel.includes("claude") && !request.toolConfig) {
|
|
513
|
+
request.toolConfig = { functionCallingConfig: { mode: "VALIDATED" } };
|
|
494
514
|
}
|
|
515
|
+
wireBody = {
|
|
516
|
+
model: wireModel,
|
|
517
|
+
userAgent: "antigravity",
|
|
518
|
+
requestType: "agent",
|
|
519
|
+
project: config.projectId ?? headerProjectId ?? "",
|
|
520
|
+
requestId: `agent-${crypto.randomUUID()}`,
|
|
521
|
+
request,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
495
524
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
525
|
+
const versionedBase = /\/v\d+[^/]*$/i.test(baseUrl.replace(/\/+$/, ""));
|
|
526
|
+
const defaultPath = `${versionedBase ? "" : "/v1beta"}/models/${model}:streamGenerateContent?alt=sse`;
|
|
527
|
+
const path = config.path ?? defaultPath;
|
|
528
|
+
const useBearer = config.bearer || isAntigravity;
|
|
529
|
+
const authHeader: Record<string, string> = config.apiKey
|
|
530
|
+
? useBearer
|
|
531
|
+
? { authorization: `Bearer ${config.apiKey}` }
|
|
532
|
+
: { "x-goog-api-key": config.apiKey }
|
|
533
|
+
: {};
|
|
534
|
+
const antigravityUa: Record<string, string> = isAntigravity
|
|
535
|
+
? {
|
|
536
|
+
"user-agent": wireModel.startsWith("gemini-4-argon")
|
|
537
|
+
? ANTIGRAVITY_CLI_USER_AGENT
|
|
538
|
+
: ANTIGRAVITY_IDE_USER_AGENT,
|
|
510
539
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
...(part.thoughtSignature ? { thoughtSignature: part.thoughtSignature } : {}),
|
|
526
|
-
});
|
|
527
|
-
}
|
|
540
|
+
: {};
|
|
541
|
+
const headers: Record<string, string> = {
|
|
542
|
+
...antigravityUa,
|
|
543
|
+
...authHeader,
|
|
544
|
+
...rawHeaders,
|
|
545
|
+
...(config.sessionHeader
|
|
546
|
+
? { [config.sessionHeader]: opts.sessionId ?? instanceSessionId }
|
|
547
|
+
: {}),
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
const bases =
|
|
551
|
+
isAntigravity && baseUrl.replace(/\/+$/, "") === ANTIGRAVITY_DAILY_API
|
|
552
|
+
? [ANTIGRAVITY_DAILY_API, ANTIGRAVITY_PROD_API]
|
|
553
|
+
: [baseUrl];
|
|
528
554
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
yield { type: "delta", toolCalls };
|
|
534
|
-
}
|
|
555
|
+
let sawFunctionCalls = false;
|
|
556
|
+
let callIndex = 0;
|
|
557
|
+
let finished = false;
|
|
558
|
+
let emitted = false;
|
|
535
559
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
560
|
+
for (let baseIdx = 0; baseIdx < bases.length; baseIdx++) {
|
|
561
|
+
const currentBase = bases[baseIdx]!;
|
|
562
|
+
try {
|
|
563
|
+
for await (const data of streamSse({
|
|
564
|
+
// Without `?alt=sse` the response is one long JSON array that only
|
|
565
|
+
// parses once complete, which is not a stream.
|
|
566
|
+
url: apiUrl(currentBase, path),
|
|
567
|
+
headers,
|
|
568
|
+
body: wireBody,
|
|
569
|
+
provider: id,
|
|
570
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
571
|
+
...(config.fetchImpl ? { fetchImpl: config.fetchImpl } : {}),
|
|
572
|
+
...(opts.onActivity ? { onActivity: opts.onActivity } : {}),
|
|
573
|
+
...(opts.onRateLimit ? { onRateLimit: opts.onRateLimit } : {}),
|
|
574
|
+
// This wire has no terminal event to stop at, so the finish reason is
|
|
575
|
+
// the end: a read that dies after it can cost a late usage record, never
|
|
576
|
+
// the answer.
|
|
577
|
+
finished: () => finished,
|
|
578
|
+
})) {
|
|
579
|
+
let parsed: GeminiResponse & { response?: GeminiResponse };
|
|
580
|
+
try {
|
|
581
|
+
parsed = JSON.parse(data) as GeminiResponse & { response?: GeminiResponse };
|
|
582
|
+
} catch {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
const chunk = parsed.response ?? parsed;
|
|
586
|
+
|
|
587
|
+
// `?alt=sse` commits to 200 the moment the headers go out, so a
|
|
588
|
+
// throttle or an overload landing after that arrives here as a
|
|
589
|
+
// google.rpc.Status in the body rather than as a status line.
|
|
590
|
+
if (chunk.error) throw streamError(id, chunk.error);
|
|
591
|
+
// A blocked PROMPT has no candidate to finish, so it reads as a cut
|
|
592
|
+
// stream — retried, into the same block. It is a content failure.
|
|
593
|
+
const blocked = chunk.promptFeedback?.blockReason;
|
|
594
|
+
if (blocked) {
|
|
595
|
+
throw new ProviderError(id, "content", `${id}: the prompt was blocked (${blocked})`, {
|
|
596
|
+
code: blocked,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (chunk.usageMetadata) {
|
|
601
|
+
const usage = chunk.usageMetadata;
|
|
602
|
+
emitted = true;
|
|
603
|
+
yield {
|
|
604
|
+
type: "usage",
|
|
605
|
+
usage: {
|
|
606
|
+
inputTokens: usage.promptTokenCount ?? 0,
|
|
607
|
+
// Thoughts bill as OUTPUT, and Gemini reports them OUTSIDE
|
|
608
|
+
// candidatesTokenCount — leaving them out undercounts a thinking
|
|
609
|
+
// turn by most of what it cost.
|
|
610
|
+
outputTokens: (usage.candidatesTokenCount ?? 0) + (usage.thoughtsTokenCount ?? 0),
|
|
611
|
+
cachedInputTokens: usage.cachedContentTokenCount ?? 0,
|
|
612
|
+
...(usage.thoughtsTokenCount !== undefined
|
|
613
|
+
? { reasoningTokens: usage.thoughtsTokenCount }
|
|
614
|
+
: {}),
|
|
615
|
+
},
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const candidate = chunk.candidates?.[0];
|
|
620
|
+
if (!candidate) continue;
|
|
621
|
+
|
|
622
|
+
// One chunk can carry several parts of each kind. They are coalesced
|
|
623
|
+
// per kind so the seam sees one reasoning delta and one content delta
|
|
624
|
+
// per chunk, in that order, rather than interleaved fragments.
|
|
625
|
+
let reasoning = "";
|
|
626
|
+
let content = "";
|
|
627
|
+
const toolCalls: ToolCallDelta[] = [];
|
|
628
|
+
for (const part of candidate.content?.parts ?? []) {
|
|
629
|
+
if (part.text) {
|
|
630
|
+
if (part.thought) reasoning += part.text;
|
|
631
|
+
else content += part.text;
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
const call = part.functionCall;
|
|
635
|
+
if (!call) continue;
|
|
636
|
+
const index = callIndex++;
|
|
637
|
+
const identity = { index, id: call.id ?? "", name: call.name ?? "" };
|
|
638
|
+
// The part is the whole call, so it is settled here: one with no
|
|
639
|
+
// name can't run, and one with no id gets one the way the other
|
|
640
|
+
// three wires mint it. Gemini omits the id on a single-call turn, and
|
|
641
|
+
// the seam's consumers pair a result back to its call by id.
|
|
642
|
+
settleToolCalls(id, [identity]);
|
|
643
|
+
toolCalls.push({
|
|
644
|
+
...identity,
|
|
645
|
+
// Whole and already assembled — unlike the OpenAI shape, Gemini
|
|
646
|
+
// never fragments an argument object across chunks.
|
|
647
|
+
arguments: JSON.stringify(call.args ?? {}),
|
|
648
|
+
...(part.thoughtSignature ? { thoughtSignature: part.thoughtSignature } : {}),
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (reasoning) {
|
|
653
|
+
emitted = true;
|
|
654
|
+
yield { type: "delta", reasoning };
|
|
655
|
+
}
|
|
656
|
+
if (content) {
|
|
657
|
+
emitted = true;
|
|
658
|
+
yield { type: "delta", content };
|
|
659
|
+
}
|
|
660
|
+
if (toolCalls.length > 0) {
|
|
661
|
+
sawFunctionCalls = true;
|
|
662
|
+
emitted = true;
|
|
663
|
+
yield { type: "delta", toolCalls };
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (candidate.finishReason) {
|
|
667
|
+
// Mapped first: a reason that throws must not be dressed as tool_calls.
|
|
668
|
+
const finishReason = mapFinishReason(id, candidate.finishReason);
|
|
669
|
+
finished = true;
|
|
670
|
+
emitted = true;
|
|
671
|
+
yield {
|
|
672
|
+
type: "finish",
|
|
673
|
+
// Gemini routinely reports STOP on a turn that called tools, and a
|
|
674
|
+
// caller reading that as "done" drops the tool round entirely.
|
|
675
|
+
// Only STOP is promoted, as on the other three wires: a length cut
|
|
676
|
+
// or a filter may be what broke the call (invariant 10).
|
|
677
|
+
finishReason:
|
|
678
|
+
sawFunctionCalls && finishReason === "stop" ? "tool_calls" : finishReason,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
break;
|
|
683
|
+
} catch (err) {
|
|
684
|
+
const canFallback =
|
|
685
|
+
!emitted &&
|
|
686
|
+
baseIdx === 0 &&
|
|
687
|
+
bases.length > 1 &&
|
|
688
|
+
err instanceof ProviderError &&
|
|
689
|
+
(err.status === 403 ||
|
|
690
|
+
err.status === 404 ||
|
|
691
|
+
err.status === 500 ||
|
|
692
|
+
err.status === 502 ||
|
|
693
|
+
err.status === 503 ||
|
|
694
|
+
err.status === 504);
|
|
695
|
+
if (canFallback) continue;
|
|
696
|
+
throw err;
|
|
548
697
|
}
|
|
549
698
|
}
|
|
550
699
|
// Gemini states a finish reason on the last candidate of every turn it
|
package/src/providers/openai.ts
CHANGED
|
@@ -107,6 +107,13 @@ export interface OpenAIConfig extends ProviderFallbackConfig {
|
|
|
107
107
|
* no sessionId, the provider's own per-instance id rides instead, so the
|
|
108
108
|
* gateway never sees a call without one. */
|
|
109
109
|
sessionHeader?: string;
|
|
110
|
+
/** The id `sessionHeader` carries when a call has no `sessionId`. Absent, the
|
|
111
|
+
* adapter mints one when it is built. Pass one when something rebuilds the
|
|
112
|
+
* adapter (`withKeyPool` and `withToken` build one per attempt): each build
|
|
113
|
+
* mints a new id, and a gateway that pins a session to one upstream host
|
|
114
|
+
* then treats every call as a new session, re-routes it and misses its
|
|
115
|
+
* prompt cache. Ignored without `sessionHeader`. */
|
|
116
|
+
instanceSessionId?: string;
|
|
110
117
|
/** Send `X-Initiator`: `user` when the last message is the user's, else
|
|
111
118
|
* `agent`. GitHub Copilot bills a premium request per `user` turn and lets
|
|
112
119
|
* the agent's own follow-ups (tool results) ride free. */
|
|
@@ -315,7 +322,9 @@ function goEffortParams(effort: Effort, model = ""): Record<string, unknown> {
|
|
|
315
322
|
if (/glm-5[-.p]?[23]/.test(id)) {
|
|
316
323
|
return level(effort === "max" ? "max" : "high");
|
|
317
324
|
}
|
|
318
|
-
if (id.includes("deepseek-v4")
|
|
325
|
+
if (id.includes("deepseek-v4") || id.includes("minimax")) {
|
|
326
|
+
return level(effort === "none" ? "low" : effort);
|
|
327
|
+
}
|
|
319
328
|
if (id.includes("mimo"))
|
|
320
329
|
return effort === "max" ? refuse("none, low, medium or high") : level(effort);
|
|
321
330
|
return level(effort === "max" ? "high" : effort);
|
|
@@ -326,7 +335,7 @@ function goEffortParams(effort: Effort, model = ""): Record<string, unknown> {
|
|
|
326
335
|
function dialectFor(id: string): EffortDialect {
|
|
327
336
|
if (id === "openrouter") return "openrouter";
|
|
328
337
|
if (id === "deepseek") return "deepseek";
|
|
329
|
-
if (id === "opencode-go") return "opencode-go";
|
|
338
|
+
if (id === "opencode-go" || id === "opencode-zen") return "opencode-go";
|
|
330
339
|
return "openai";
|
|
331
340
|
}
|
|
332
341
|
|
|
@@ -531,7 +540,9 @@ export function createOpenAIProvider(config: OpenAIConfig): Provider {
|
|
|
531
540
|
// `usage.cost` is its field in its unit. Another gateway that sends a
|
|
532
541
|
// `cost` has not said what it means.
|
|
533
542
|
const reportsCost = isOpenRouter(baseUrl);
|
|
534
|
-
const instanceSessionId = config.sessionHeader
|
|
543
|
+
const instanceSessionId = config.sessionHeader
|
|
544
|
+
? (config.instanceSessionId ?? crypto.randomUUID())
|
|
545
|
+
: undefined;
|
|
535
546
|
|
|
536
547
|
const provider: Provider = {
|
|
537
548
|
id,
|
|
@@ -658,7 +669,7 @@ export function createOpenAIProvider(config: OpenAIConfig): Provider {
|
|
|
658
669
|
for await (const data of streamSse({
|
|
659
670
|
url: apiUrl(baseUrl, config.path ?? defaultPath),
|
|
660
671
|
headers: {
|
|
661
|
-
authorization: `Bearer ${config.apiKey}
|
|
672
|
+
...(config.apiKey ? { authorization: `Bearer ${config.apiKey}` } : {}),
|
|
662
673
|
...attributionHeaders(config, config.headers),
|
|
663
674
|
...(config.sessionHeader
|
|
664
675
|
? { [config.sessionHeader]: opts.sessionId ?? instanceSessionId }
|
|
@@ -48,6 +48,17 @@ export interface ResponsesConfig extends ProviderFallbackConfig {
|
|
|
48
48
|
/** Header that carries `StreamOptions.sessionId`. When set and a call has no
|
|
49
49
|
* sessionId, one stable id for this provider instance rides instead. */
|
|
50
50
|
sessionHeader?: string;
|
|
51
|
+
/** The id `sessionHeader` carries when a call has no `sessionId`. Absent, the
|
|
52
|
+
* adapter mints one when it is built. Pass one when something rebuilds the
|
|
53
|
+
* adapter (`withKeyPool` and `withToken` build one per attempt): each build
|
|
54
|
+
* mints a new id, and a gateway that pins a session to one upstream host
|
|
55
|
+
* then treats every call as a new session, re-routes it and misses its
|
|
56
|
+
* prompt cache. Ignored without `sessionHeader`.
|
|
57
|
+
*
|
|
58
|
+
* It is not sent as `prompt_cache_key`, which stays the per-call
|
|
59
|
+
* `sessionId`'s: this id names the adapter, so it is shared by every
|
|
60
|
+
* conversation the adapter serves, and a minted one never was a cache key. */
|
|
61
|
+
instanceSessionId?: string;
|
|
51
62
|
/**
|
|
52
63
|
* Where this backend serves the endpoint, when it is not `/v1/responses`.
|
|
53
64
|
* The ChatGPT subscription surface serves it at `/backend-api/codex/responses`
|
|
@@ -395,7 +406,9 @@ function unsentTail(snapshot: string, streamed: string): string {
|
|
|
395
406
|
export function createResponsesProvider(config: ResponsesConfig): Provider {
|
|
396
407
|
const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
397
408
|
const id = config.id ?? "openai-responses";
|
|
398
|
-
const instanceSessionId = config.sessionHeader
|
|
409
|
+
const instanceSessionId = config.sessionHeader
|
|
410
|
+
? (config.instanceSessionId ?? crypto.randomUUID())
|
|
411
|
+
: undefined;
|
|
399
412
|
|
|
400
413
|
const provider: Provider = {
|
|
401
414
|
id,
|