@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/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,88 @@ 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
|
+
/**
|
|
89
|
+
* The User-Agent Cloud Code Assist expects for a model: `gemini-4-argon` is served to
|
|
90
|
+
* the Antigravity CLI, every other model to the IDE. Exported for hosts that cannot
|
|
91
|
+
* set the header on `fetch` (Chromium ignores it) and stamp it another way.
|
|
92
|
+
*/
|
|
93
|
+
export function antigravityUserAgent(model: string): string {
|
|
94
|
+
return resolveAntigravityWireModel(model).startsWith("gemini-4-argon")
|
|
95
|
+
? ANTIGRAVITY_CLI_USER_AGENT
|
|
96
|
+
: ANTIGRAVITY_IDE_USER_AGENT;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Deterministic Antigravity session id (`-<uint64>`), matching cc-proxy. */
|
|
100
|
+
export async function antigravitySessionId(sessionId?: string): Promise<string> {
|
|
101
|
+
const seed = sessionId && sessionId.length > 0 ? sessionId : crypto.randomUUID();
|
|
102
|
+
const digest = new Uint8Array(
|
|
103
|
+
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(seed)),
|
|
104
|
+
);
|
|
105
|
+
const view = new DataView(digest.buffer, digest.byteOffset, 8);
|
|
106
|
+
const masked = view.getBigUint64(0, false) & 0x7fffffffffffffffn;
|
|
107
|
+
return `-${masked.toString()}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function cleanAntigravitySystemText(text: string): string {
|
|
111
|
+
return text
|
|
112
|
+
.replace(/^x-anthropic-billing-header:[^\n]*\n*/, "")
|
|
113
|
+
.split("\n\n")
|
|
114
|
+
.filter((p) => p !== REJECTED_CLAUDE_SDK_PARAGRAPH)
|
|
115
|
+
.join("\n\n");
|
|
116
|
+
}
|
|
56
117
|
|
|
57
118
|
/**
|
|
58
|
-
* Gemini 3
|
|
59
|
-
*
|
|
60
|
-
*
|
|
119
|
+
* Gemini 3+ thinking dial.
|
|
120
|
+
*
|
|
121
|
+
* Measured across all 17 text models on 2026-10-21:
|
|
122
|
+
* - Only `gemini-3-flash*`, `gemini-3.5-flash*`, `gemini-3.6-flash`, and the
|
|
123
|
+
* `flash-lite` models accept `MINIMAL` (which spends 0 thought tokens).
|
|
124
|
+
* - `gemini-3.7-flash`, `gemini-3.8-flash`, `gemini-4-*`, `gemini-flash-latest`,
|
|
125
|
+
* and every Pro model answer `MINIMAL` with `400 Thinking level MINIMAL is not
|
|
126
|
+
* supported for this model`, and take `LOW` as their lowest level.
|
|
61
127
|
*
|
|
62
128
|
* Sent as the REST enum NAMES. The SDK's `ThinkingLevel` members serialize to
|
|
63
129
|
* exactly these strings, so nothing is lost by writing them out.
|
|
64
130
|
*/
|
|
65
|
-
const THINKING_LEVEL: Record<Effort, string> = {
|
|
66
|
-
none: "MINIMAL",
|
|
131
|
+
const THINKING_LEVEL: Record<Exclude<Effort, "none">, string> = {
|
|
67
132
|
low: "LOW",
|
|
68
133
|
medium: "MEDIUM",
|
|
69
134
|
high: "HIGH",
|
|
70
135
|
max: "HIGH",
|
|
71
136
|
};
|
|
72
137
|
|
|
138
|
+
const TAKES_MINIMAL_THINKING =
|
|
139
|
+
/^gemini-(?:3(?:\.[056])?-flash(?:-preview|-lite(?:-preview)?)?|3\.1-flash-lite(?:-preview)?|flash-lite-latest)$/i;
|
|
140
|
+
|
|
141
|
+
function geminiThinkingLevel(model: string, effort: Effort): string {
|
|
142
|
+
if (effort !== "none") return THINKING_LEVEL[effort];
|
|
143
|
+
return TAKES_MINIMAL_THINKING.test(model) ? "MINIMAL" : "LOW";
|
|
144
|
+
}
|
|
145
|
+
|
|
73
146
|
/**
|
|
74
147
|
* Gemini 2.x thinking budget dial (tokens). Gemini 2.5 takes `thinkingBudget`
|
|
75
148
|
* (0 disables thinking) and has no level. Every newer model takes
|
|
@@ -327,6 +400,9 @@ interface GeminiResponse {
|
|
|
327
400
|
export function createGeminiProvider(config: GeminiConfig): Provider {
|
|
328
401
|
const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
329
402
|
const id = config.id ?? "gemini";
|
|
403
|
+
const isAntigravity = id === "antigravity";
|
|
404
|
+
const instanceSessionId =
|
|
405
|
+
config.sessionHeader || isAntigravity ? (config.instanceSessionId ?? crypto.randomUUID()) : "";
|
|
330
406
|
|
|
331
407
|
const provider: Provider = {
|
|
332
408
|
id,
|
|
@@ -347,6 +423,7 @@ export function createGeminiProvider(config: GeminiConfig): Provider {
|
|
|
347
423
|
// the doubled segment 404s as "model not found" — a confusing way to
|
|
348
424
|
// learn about a prefix.
|
|
349
425
|
const model = (opts.model ?? config.model).replace(/^models\//, "");
|
|
426
|
+
const wireModel = isAntigravity ? resolveAntigravityWireModel(model) : model;
|
|
350
427
|
const isGemini3 = /gemini-3/i.test(model);
|
|
351
428
|
// Not `!isGemini3`: `gemini-flash-latest` and models newer than 3 are not
|
|
352
429
|
// Gemini 3 either, and they must get a level.
|
|
@@ -361,10 +438,10 @@ export function createGeminiProvider(config: GeminiConfig): Provider {
|
|
|
361
438
|
// No effort means the model's own dynamic thinking. Sending MINIMAL / 0 here
|
|
362
439
|
// would switch that off for a caller who never asked, which is the whole
|
|
363
440
|
// reason the seam treats an absent effort as "never sent".
|
|
364
|
-
if (effort) {
|
|
441
|
+
if (effort && !isAntigravity) {
|
|
365
442
|
generationConfig.thinkingConfig = isGemini2
|
|
366
443
|
? { thinkingBudget: GEMINI_2_THINKING_BUDGET[effort] }
|
|
367
|
-
: { thinkingLevel:
|
|
444
|
+
: { thinkingLevel: geminiThinkingLevel(model, effort) };
|
|
368
445
|
}
|
|
369
446
|
// A schema that shares its call with tools pins the decoder, and a pinned
|
|
370
447
|
// decoder cannot emit a functionCall — the model narrates the call it
|
|
@@ -399,8 +476,9 @@ export function createGeminiProvider(config: GeminiConfig): Provider {
|
|
|
399
476
|
// The prompt-carried schema rides as a second part, after the caller's
|
|
400
477
|
// own text: this shape's cache is a prefix too, so a per-call schema in
|
|
401
478
|
// front of the conversation would cold-start it on every change.
|
|
479
|
+
const cleanedSystem = system && isAntigravity ? cleanAntigravitySystemText(system) : system;
|
|
402
480
|
const systemParts = [
|
|
403
|
-
...(
|
|
481
|
+
...(cleanedSystem ? [{ text: cleanedSystem }] : []),
|
|
404
482
|
...(opts.json && promptCarried ? [{ text: schemaPrompt(opts.json.schema) }] : []),
|
|
405
483
|
];
|
|
406
484
|
if (systemParts.length > 0) request.systemInstruction = { parts: systemParts };
|
|
@@ -420,9 +498,12 @@ export function createGeminiProvider(config: GeminiConfig): Provider {
|
|
|
420
498
|
functionDeclarations: tools.map((tool) => ({
|
|
421
499
|
name: tool.name,
|
|
422
500
|
description: tool.description,
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
parametersJsonSchema
|
|
501
|
+
// Antigravity (Cloud Code Assist) still expects the OpenAPI-subset
|
|
502
|
+
// `parameters` field; the Gemini API and OpenCode Zen take standard
|
|
503
|
+
// JSON Schema in `parametersJsonSchema` (cc-proxy 0.2.8).
|
|
504
|
+
...(isAntigravity
|
|
505
|
+
? { parameters: toAntigravityToolSchema(tool.inputSchema) }
|
|
506
|
+
: { parametersJsonSchema: toGeminiToolSchema(tool.inputSchema) }),
|
|
426
507
|
})),
|
|
427
508
|
},
|
|
428
509
|
];
|
|
@@ -432,119 +513,194 @@ export function createGeminiProvider(config: GeminiConfig): Provider {
|
|
|
432
513
|
if (toolConfig) request.toolConfig = toolConfig;
|
|
433
514
|
}
|
|
434
515
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
let sawFunctionCalls = false;
|
|
439
|
-
let callIndex = 0;
|
|
440
|
-
let finished = false;
|
|
516
|
+
const rawHeaders = { ...config.headers };
|
|
517
|
+
const headerProjectId = rawHeaders["x-antigravity-project"];
|
|
518
|
+
delete rawHeaders["x-antigravity-project"];
|
|
441
519
|
|
|
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;
|
|
520
|
+
let wireBody: Record<string, unknown> = request;
|
|
521
|
+
if (isAntigravity) {
|
|
522
|
+
request.sessionId = await antigravitySessionId(opts.sessionId ?? instanceSessionId);
|
|
523
|
+
if (wireModel.includes("claude") && !request.toolConfig) {
|
|
524
|
+
request.toolConfig = { functionCallingConfig: { mode: "VALIDATED" } };
|
|
463
525
|
}
|
|
526
|
+
wireBody = {
|
|
527
|
+
model: wireModel,
|
|
528
|
+
userAgent: "antigravity",
|
|
529
|
+
requestType: "agent",
|
|
530
|
+
project: config.projectId ?? headerProjectId ?? "",
|
|
531
|
+
requestId: `agent-${crypto.randomUUID()}`,
|
|
532
|
+
request,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
464
535
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
536
|
+
const versionedBase = /\/v\d+[^/]*$/i.test(baseUrl.replace(/\/+$/, ""));
|
|
537
|
+
const defaultPath = `${versionedBase ? "" : "/v1beta"}/models/${model}:streamGenerateContent?alt=sse`;
|
|
538
|
+
const path = config.path ?? defaultPath;
|
|
539
|
+
const useBearer = config.bearer || isAntigravity;
|
|
540
|
+
const authHeader: Record<string, string> = config.apiKey
|
|
541
|
+
? useBearer
|
|
542
|
+
? { authorization: `Bearer ${config.apiKey}` }
|
|
543
|
+
: { "x-goog-api-key": config.apiKey }
|
|
544
|
+
: {};
|
|
545
|
+
const antigravityUa: Record<string, string> = isAntigravity
|
|
546
|
+
? { "user-agent": antigravityUserAgent(model) }
|
|
547
|
+
: {};
|
|
548
|
+
const headers: Record<string, string> = {
|
|
549
|
+
...antigravityUa,
|
|
550
|
+
...authHeader,
|
|
551
|
+
...rawHeaders,
|
|
552
|
+
...(config.sessionHeader
|
|
553
|
+
? { [config.sessionHeader]: opts.sessionId ?? instanceSessionId }
|
|
554
|
+
: {}),
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const bases =
|
|
558
|
+
isAntigravity && baseUrl.replace(/\/+$/, "") === ANTIGRAVITY_DAILY_API
|
|
559
|
+
? [ANTIGRAVITY_DAILY_API, ANTIGRAVITY_PROD_API]
|
|
560
|
+
: [baseUrl];
|
|
477
561
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
};
|
|
494
|
-
}
|
|
562
|
+
let sawFunctionCalls = false;
|
|
563
|
+
let callIndex = 0;
|
|
564
|
+
let finished = false;
|
|
565
|
+
let emitted = false;
|
|
495
566
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
567
|
+
for (let baseIdx = 0; baseIdx < bases.length; baseIdx++) {
|
|
568
|
+
const currentBase = bases[baseIdx]!;
|
|
569
|
+
try {
|
|
570
|
+
for await (const data of streamSse({
|
|
571
|
+
// Without `?alt=sse` the response is one long JSON array that only
|
|
572
|
+
// parses once complete, which is not a stream.
|
|
573
|
+
url: apiUrl(currentBase, path),
|
|
574
|
+
headers,
|
|
575
|
+
body: wireBody,
|
|
576
|
+
provider: id,
|
|
577
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
578
|
+
...(config.fetchImpl ? { fetchImpl: config.fetchImpl } : {}),
|
|
579
|
+
...(opts.onActivity ? { onActivity: opts.onActivity } : {}),
|
|
580
|
+
...(opts.onRateLimit ? { onRateLimit: opts.onRateLimit } : {}),
|
|
581
|
+
// This wire has no terminal event to stop at, so the finish reason is
|
|
582
|
+
// the end: a read that dies after it can cost a late usage record, never
|
|
583
|
+
// the answer.
|
|
584
|
+
finished: () => finished,
|
|
585
|
+
})) {
|
|
586
|
+
let parsed: GeminiResponse & { response?: GeminiResponse };
|
|
587
|
+
try {
|
|
588
|
+
parsed = JSON.parse(data) as GeminiResponse & { response?: GeminiResponse };
|
|
589
|
+
} catch {
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
const chunk = parsed.response ?? parsed;
|
|
593
|
+
|
|
594
|
+
// `?alt=sse` commits to 200 the moment the headers go out, so a
|
|
595
|
+
// throttle or an overload landing after that arrives here as a
|
|
596
|
+
// google.rpc.Status in the body rather than as a status line.
|
|
597
|
+
if (chunk.error) throw streamError(id, chunk.error);
|
|
598
|
+
// A blocked PROMPT has no candidate to finish, so it reads as a cut
|
|
599
|
+
// stream — retried, into the same block. It is a content failure.
|
|
600
|
+
const blocked = chunk.promptFeedback?.blockReason;
|
|
601
|
+
if (blocked) {
|
|
602
|
+
throw new ProviderError(id, "content", `${id}: the prompt was blocked (${blocked})`, {
|
|
603
|
+
code: blocked,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (chunk.usageMetadata) {
|
|
608
|
+
const usage = chunk.usageMetadata;
|
|
609
|
+
emitted = true;
|
|
610
|
+
yield {
|
|
611
|
+
type: "usage",
|
|
612
|
+
usage: {
|
|
613
|
+
inputTokens: usage.promptTokenCount ?? 0,
|
|
614
|
+
// Thoughts bill as OUTPUT, and Gemini reports them OUTSIDE
|
|
615
|
+
// candidatesTokenCount — leaving them out undercounts a thinking
|
|
616
|
+
// turn by most of what it cost.
|
|
617
|
+
outputTokens: (usage.candidatesTokenCount ?? 0) + (usage.thoughtsTokenCount ?? 0),
|
|
618
|
+
cachedInputTokens: usage.cachedContentTokenCount ?? 0,
|
|
619
|
+
...(usage.thoughtsTokenCount !== undefined
|
|
620
|
+
? { reasoningTokens: usage.thoughtsTokenCount }
|
|
621
|
+
: {}),
|
|
622
|
+
},
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const candidate = chunk.candidates?.[0];
|
|
627
|
+
if (!candidate) continue;
|
|
628
|
+
|
|
629
|
+
// One chunk can carry several parts of each kind. They are coalesced
|
|
630
|
+
// per kind so the seam sees one reasoning delta and one content delta
|
|
631
|
+
// per chunk, in that order, rather than interleaved fragments.
|
|
632
|
+
let reasoning = "";
|
|
633
|
+
let content = "";
|
|
634
|
+
const toolCalls: ToolCallDelta[] = [];
|
|
635
|
+
for (const part of candidate.content?.parts ?? []) {
|
|
636
|
+
if (part.text) {
|
|
637
|
+
if (part.thought) reasoning += part.text;
|
|
638
|
+
else content += part.text;
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
const call = part.functionCall;
|
|
642
|
+
if (!call) continue;
|
|
643
|
+
const index = callIndex++;
|
|
644
|
+
const identity = { index, id: call.id ?? "", name: call.name ?? "" };
|
|
645
|
+
// The part is the whole call, so it is settled here: one with no
|
|
646
|
+
// name can't run, and one with no id gets one the way the other
|
|
647
|
+
// three wires mint it. Gemini omits the id on a single-call turn, and
|
|
648
|
+
// the seam's consumers pair a result back to its call by id.
|
|
649
|
+
settleToolCalls(id, [identity]);
|
|
650
|
+
toolCalls.push({
|
|
651
|
+
...identity,
|
|
652
|
+
// Whole and already assembled — unlike the OpenAI shape, Gemini
|
|
653
|
+
// never fragments an argument object across chunks.
|
|
654
|
+
arguments: JSON.stringify(call.args ?? {}),
|
|
655
|
+
...(part.thoughtSignature ? { thoughtSignature: part.thoughtSignature } : {}),
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (reasoning) {
|
|
660
|
+
emitted = true;
|
|
661
|
+
yield { type: "delta", reasoning };
|
|
662
|
+
}
|
|
663
|
+
if (content) {
|
|
664
|
+
emitted = true;
|
|
665
|
+
yield { type: "delta", content };
|
|
666
|
+
}
|
|
667
|
+
if (toolCalls.length > 0) {
|
|
668
|
+
sawFunctionCalls = true;
|
|
669
|
+
emitted = true;
|
|
670
|
+
yield { type: "delta", toolCalls };
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (candidate.finishReason) {
|
|
674
|
+
// Mapped first: a reason that throws must not be dressed as tool_calls.
|
|
675
|
+
const finishReason = mapFinishReason(id, candidate.finishReason);
|
|
676
|
+
finished = true;
|
|
677
|
+
emitted = true;
|
|
678
|
+
yield {
|
|
679
|
+
type: "finish",
|
|
680
|
+
// Gemini routinely reports STOP on a turn that called tools, and a
|
|
681
|
+
// caller reading that as "done" drops the tool round entirely.
|
|
682
|
+
// Only STOP is promoted, as on the other three wires: a length cut
|
|
683
|
+
// or a filter may be what broke the call (invariant 10).
|
|
684
|
+
finishReason:
|
|
685
|
+
sawFunctionCalls && finishReason === "stop" ? "tool_calls" : finishReason,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
510
688
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
if (reasoning) yield { type: "delta", reasoning };
|
|
530
|
-
if (content) yield { type: "delta", content };
|
|
531
|
-
if (toolCalls.length > 0) {
|
|
532
|
-
sawFunctionCalls = true;
|
|
533
|
-
yield { type: "delta", toolCalls };
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
if (candidate.finishReason) {
|
|
537
|
-
// Mapped first: a reason that throws must not be dressed as tool_calls.
|
|
538
|
-
const finishReason = mapFinishReason(id, candidate.finishReason);
|
|
539
|
-
finished = true;
|
|
540
|
-
yield {
|
|
541
|
-
type: "finish",
|
|
542
|
-
// Gemini routinely reports STOP on a turn that called tools, and a
|
|
543
|
-
// caller reading that as "done" drops the tool round entirely.
|
|
544
|
-
// Only STOP is promoted, as on the other three wires: a length cut
|
|
545
|
-
// or a filter may be what broke the call (invariant 10).
|
|
546
|
-
finishReason: sawFunctionCalls && finishReason === "stop" ? "tool_calls" : finishReason,
|
|
547
|
-
};
|
|
689
|
+
break;
|
|
690
|
+
} catch (err) {
|
|
691
|
+
const canFallback =
|
|
692
|
+
!emitted &&
|
|
693
|
+
baseIdx === 0 &&
|
|
694
|
+
bases.length > 1 &&
|
|
695
|
+
err instanceof ProviderError &&
|
|
696
|
+
(err.status === 403 ||
|
|
697
|
+
err.status === 404 ||
|
|
698
|
+
err.status === 500 ||
|
|
699
|
+
err.status === 502 ||
|
|
700
|
+
err.status === 503 ||
|
|
701
|
+
err.status === 504);
|
|
702
|
+
if (canFallback) continue;
|
|
703
|
+
throw err;
|
|
548
704
|
}
|
|
549
705
|
}
|
|
550
706
|
// 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 }
|