@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.
- 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 +11 -1
- package/dist/providers/factory.d.ts.map +1 -1
- package/dist/providers/factory.js +172 -18
- 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.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 +229 -20
- package/src/providers/gemini.ts +273 -124
- package/src/providers/openai.ts +5 -3
- package/src/schema.ts +200 -72
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
|
@@ -322,7 +322,9 @@ function goEffortParams(effort: Effort, model = ""): Record<string, unknown> {
|
|
|
322
322
|
if (/glm-5[-.p]?[23]/.test(id)) {
|
|
323
323
|
return level(effort === "max" ? "max" : "high");
|
|
324
324
|
}
|
|
325
|
-
if (id.includes("deepseek-v4")
|
|
325
|
+
if (id.includes("deepseek-v4") || id.includes("minimax")) {
|
|
326
|
+
return level(effort === "none" ? "low" : effort);
|
|
327
|
+
}
|
|
326
328
|
if (id.includes("mimo"))
|
|
327
329
|
return effort === "max" ? refuse("none, low, medium or high") : level(effort);
|
|
328
330
|
return level(effort === "max" ? "high" : effort);
|
|
@@ -333,7 +335,7 @@ function goEffortParams(effort: Effort, model = ""): Record<string, unknown> {
|
|
|
333
335
|
function dialectFor(id: string): EffortDialect {
|
|
334
336
|
if (id === "openrouter") return "openrouter";
|
|
335
337
|
if (id === "deepseek") return "deepseek";
|
|
336
|
-
if (id === "opencode-go") return "opencode-go";
|
|
338
|
+
if (id === "opencode-go" || id === "opencode-zen") return "opencode-go";
|
|
337
339
|
return "openai";
|
|
338
340
|
}
|
|
339
341
|
|
|
@@ -667,7 +669,7 @@ export function createOpenAIProvider(config: OpenAIConfig): Provider {
|
|
|
667
669
|
for await (const data of streamSse({
|
|
668
670
|
url: apiUrl(baseUrl, config.path ?? defaultPath),
|
|
669
671
|
headers: {
|
|
670
|
-
authorization: `Bearer ${config.apiKey}
|
|
672
|
+
...(config.apiKey ? { authorization: `Bearer ${config.apiKey}` } : {}),
|
|
671
673
|
...attributionHeaders(config, config.headers),
|
|
672
674
|
...(config.sessionHeader
|
|
673
675
|
? { [config.sessionHeader]: opts.sessionId ?? instanceSessionId }
|