@framers/agentos 0.10.20 → 0.10.22
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 +1 -7
- package/dist/api/embedText.d.ts +9 -4
- package/dist/api/embedText.d.ts.map +1 -1
- package/dist/api/embedText.js +114 -8
- package/dist/api/embedText.js.map +1 -1
- package/dist/api/generateText.d.ts +57 -1
- package/dist/api/generateText.d.ts.map +1 -1
- package/dist/api/generateText.js +56 -14
- package/dist/api/generateText.js.map +1 -1
- package/dist/api/runtime/provider-defaults.d.ts.map +1 -1
- package/dist/api/runtime/provider-defaults.js +6 -2
- package/dist/api/runtime/provider-defaults.js.map +1 -1
- package/dist/api/sessionTranscript.d.ts +6 -0
- package/dist/api/sessionTranscript.d.ts.map +1 -1
- package/dist/api/sessionTranscript.js.map +1 -1
- package/dist/api/streamText.d.ts.map +1 -1
- package/dist/api/streamText.js +9 -6
- package/dist/api/streamText.js.map +1 -1
- package/dist/cognition/substrate/ConversationHistoryManager.d.ts.map +1 -1
- package/dist/cognition/substrate/ConversationHistoryManager.js +2 -0
- package/dist/cognition/substrate/ConversationHistoryManager.js.map +1 -1
- package/dist/cognition/substrate/GMI.d.ts.map +1 -1
- package/dist/cognition/substrate/GMI.js +3 -1
- package/dist/cognition/substrate/GMI.js.map +1 -1
- package/dist/cognition/substrate/IGMI.d.ts +5 -0
- package/dist/cognition/substrate/IGMI.d.ts.map +1 -1
- package/dist/cognition/substrate/IGMI.js.map +1 -1
- package/dist/core/llm/PromptEngine.d.ts.map +1 -1
- package/dist/core/llm/PromptEngine.js +1 -0
- package/dist/core/llm/PromptEngine.js.map +1 -1
- package/dist/core/llm/providers/IProvider.d.ts +9 -0
- package/dist/core/llm/providers/IProvider.d.ts.map +1 -1
- package/dist/core/llm/providers/errors/GeminiProviderError.d.ts +11 -0
- package/dist/core/llm/providers/errors/GeminiProviderError.d.ts.map +1 -1
- package/dist/core/llm/providers/errors/GeminiProviderError.js.map +1 -1
- package/dist/core/llm/providers/implementations/AnthropicProvider.d.ts +27 -6
- package/dist/core/llm/providers/implementations/AnthropicProvider.d.ts.map +1 -1
- package/dist/core/llm/providers/implementations/AnthropicProvider.js +78 -15
- package/dist/core/llm/providers/implementations/AnthropicProvider.js.map +1 -1
- package/dist/core/llm/providers/implementations/ClaudeCodeProvider.d.ts +1 -1
- package/dist/core/llm/providers/implementations/ClaudeCodeProvider.d.ts.map +1 -1
- package/dist/core/llm/providers/implementations/ClaudeCodeProvider.js +70 -7
- package/dist/core/llm/providers/implementations/ClaudeCodeProvider.js.map +1 -1
- package/dist/core/llm/providers/implementations/GeminiCLIBridge.js +1 -1
- package/dist/core/llm/providers/implementations/GeminiCLIProvider.d.ts.map +1 -1
- package/dist/core/llm/providers/implementations/GeminiCLIProvider.js +56 -4
- package/dist/core/llm/providers/implementations/GeminiCLIProvider.js.map +1 -1
- package/dist/core/llm/providers/implementations/GeminiProvider.d.ts +22 -5
- package/dist/core/llm/providers/implementations/GeminiProvider.d.ts.map +1 -1
- package/dist/core/llm/providers/implementations/GeminiProvider.js +512 -48
- package/dist/core/llm/providers/implementations/GeminiProvider.js.map +1 -1
- package/dist/core/llm/providers/implementations/OpenAIProvider.d.ts +61 -0
- package/dist/core/llm/providers/implementations/OpenAIProvider.d.ts.map +1 -1
- package/dist/core/llm/providers/implementations/OpenAIProvider.js +164 -22
- package/dist/core/llm/providers/implementations/OpenAIProvider.js.map +1 -1
- package/dist/core/llm/providers/implementations/OpenRouterProvider.d.ts.map +1 -1
- package/dist/core/llm/providers/implementations/OpenRouterProvider.js +6 -2
- package/dist/core/llm/providers/implementations/OpenRouterProvider.js.map +1 -1
- package/dist/core/llm/providers/model-forced-tool-choice.d.ts +32 -15
- package/dist/core/llm/providers/model-forced-tool-choice.d.ts.map +1 -1
- package/dist/core/llm/providers/model-forced-tool-choice.js +33 -16
- package/dist/core/llm/providers/model-forced-tool-choice.js.map +1 -1
- package/dist/core/llm/streaming/StreamingBatcher.d.ts.map +1 -1
- package/dist/core/llm/streaming/StreamingBatcher.js +3 -0
- package/dist/core/llm/streaming/StreamingBatcher.js.map +1 -1
- package/dist/core/llm/streaming/StreamingReconstructor.d.ts +2 -0
- package/dist/core/llm/streaming/StreamingReconstructor.d.ts.map +1 -1
- package/dist/core/llm/streaming/StreamingReconstructor.js +12 -1
- package/dist/core/llm/streaming/StreamingReconstructor.js.map +1 -1
- package/dist/io/vision/providers/LLMVisionProvider.d.ts +2 -2
- package/dist/io/vision/providers/LLMVisionProvider.js +1 -1
- package/dist/io/vision/types.d.ts +1 -1
- package/dist/orchestration/pipeline/query/types.d.ts.map +1 -1
- package/dist/orchestration/pipeline/query/types.js +3 -2
- package/dist/orchestration/pipeline/query/types.js.map +1 -1
- package/dist/orchestration/planning/ProviderAssignmentEngine.d.ts.map +1 -1
- package/dist/orchestration/planning/ProviderAssignmentEngine.js +2 -1
- package/dist/orchestration/planning/ProviderAssignmentEngine.js.map +1 -1
- package/package.json +1 -1
|
@@ -4,29 +4,296 @@ import { GeminiProviderError } from '../errors/GeminiProviderError.js';
|
|
|
4
4
|
import { ApiKeyPool } from '../../../providers/ApiKeyPool.js';
|
|
5
5
|
import { computeRetryBackoffMs } from './retry-backoff.js';
|
|
6
6
|
import { sanitizeGeminiResponseSchema } from './geminiResponseSchema.js';
|
|
7
|
+
/**
|
|
8
|
+
* Most requests `models/{model}:batchEmbedContents` accepts in one call. The
|
|
9
|
+
* API returns HTTP 400 "at most 100 requests can be in one batch" above this
|
|
10
|
+
* (probed 2026-09-29 on gemini-embedding-001).
|
|
11
|
+
*/
|
|
12
|
+
const GEMINI_EMBED_BATCH_LIMIT = 100;
|
|
13
|
+
/**
|
|
14
|
+
* Placeholder signature Google documents for replaying a function call whose
|
|
15
|
+
* real signature is unavailable, such as a call made by another provider in a
|
|
16
|
+
* fallback chain. Gemini 3 returns HTTP 400 for a replayed call with no
|
|
17
|
+
* signature and accepts this value, and Gemini 2.5 accepts it too (probed
|
|
18
|
+
* 2026-09-29).
|
|
19
|
+
*/
|
|
20
|
+
const GEMINI_SKIP_THOUGHT_SIGNATURE = 'skip_thought_signature_validator';
|
|
21
|
+
/**
|
|
22
|
+
* The credentials part of a base URL, read from the raw string: the text
|
|
23
|
+
* between the authority start (after a leading `scheme://` or `//`, else the
|
|
24
|
+
* start of a scheme-less URL) and the last `@`. The last `@` is used because an email-style username
|
|
25
|
+
* carries its own `@`, and the raw string because a URL that fails to parse
|
|
26
|
+
* (for example with a `/` in the password) still appears verbatim in fetch's
|
|
27
|
+
* error.
|
|
28
|
+
*
|
|
29
|
+
* @param baseURL Configured base URL.
|
|
30
|
+
* @returns The credentials substring, or undefined when there is none.
|
|
31
|
+
*/
|
|
32
|
+
function baseUrlCredentials(baseURL) {
|
|
33
|
+
if (!baseURL)
|
|
34
|
+
return undefined;
|
|
35
|
+
// Only a leading scheme or `//` opens the authority; a `//` later in the
|
|
36
|
+
// path must not move the start past the credentials.
|
|
37
|
+
const scheme = /^[a-z][a-z\d+.-]*:\/\//i.exec(baseURL);
|
|
38
|
+
const start = scheme ? scheme[0].length : baseURL.startsWith('//') ? 2 : 0;
|
|
39
|
+
const at = baseURL.lastIndexOf('@');
|
|
40
|
+
return at > start ? baseURL.slice(start, at) : undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Masks the secrets a request URL carries out of an error message: the API
|
|
44
|
+
* key the request sent as `key=` and any credentials in the base URL. The
|
|
45
|
+
* known values are replaced literally, since a pattern cannot tell where
|
|
46
|
+
* arbitrary credentials end. A fetch that rejects its URL (relative,
|
|
47
|
+
* scheme-less or credential-bearing) names the whole URL in its error.
|
|
48
|
+
*
|
|
49
|
+
* @param message Error message that may contain the request URL.
|
|
50
|
+
* @param apiKey The key the request carried.
|
|
51
|
+
* @param baseURL Configured base URL.
|
|
52
|
+
* @returns The message with those secrets replaced by `[redacted]`.
|
|
53
|
+
*/
|
|
54
|
+
function redactUrlSecrets(message, apiKey, baseURL) {
|
|
55
|
+
// Longest first, so a key that also appears inside the credentials cannot
|
|
56
|
+
// split them before the whole credentials string is replaced.
|
|
57
|
+
const secrets = [baseUrlCredentials(baseURL), apiKey]
|
|
58
|
+
.filter((secret) => Boolean(secret))
|
|
59
|
+
.sort((a, b) => b.length - a.length);
|
|
60
|
+
let out = message;
|
|
61
|
+
for (const secret of secrets)
|
|
62
|
+
out = out.split(secret).join('[redacted]');
|
|
63
|
+
// Backstops for any other key= value or //user:password@ credentials.
|
|
64
|
+
return out
|
|
65
|
+
.replace(/([?&]key=)[^&\s"')]+/g, '$1[redacted]')
|
|
66
|
+
.replace(/(\/\/)[^/\s@"')]+@/g, '$1[redacted]@');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Thinking levels each model accepts, keyed by exact model id and limited to
|
|
70
|
+
* what was verified against the live API (2026-09-30). The set differs per
|
|
71
|
+
* model, not per family: the 3.x image models take `minimal` | `high`, the
|
|
72
|
+
* 2.5 family rejects any level with 400 INVALID_ARGUMENT, and an alias can
|
|
73
|
+
* move to a model with a different set. A model missing here never receives
|
|
74
|
+
* a level, which is always accepted.
|
|
75
|
+
*/
|
|
76
|
+
const GEMINI_THINKING_LEVELS = {
|
|
77
|
+
'gemini-3.1-pro-preview': new Set(['low', 'medium', 'high']),
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* The thinking level to send for the provider-neutral `effort` on `modelId`,
|
|
81
|
+
* or `undefined` when the API default should stand: no effort, an effort
|
|
82
|
+
* with no Gemini equivalent, or a model that does not take that level.
|
|
83
|
+
*/
|
|
84
|
+
function geminiThinkingLevelFor(modelId, effort) {
|
|
85
|
+
const level = effort === 'low' || effort === 'medium'
|
|
86
|
+
? effort
|
|
87
|
+
: effort === 'high' || effort === 'xhigh' || effort === 'max'
|
|
88
|
+
? 'high'
|
|
89
|
+
: undefined;
|
|
90
|
+
if (!level)
|
|
91
|
+
return undefined;
|
|
92
|
+
return GEMINI_THINKING_LEVELS[modelId]?.has(level) ? level : undefined;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Pinned model ids and the alias that keeps serving their tier once Google
|
|
96
|
+
* retires them. Preview ids are retired without a redirect (the retired id
|
|
97
|
+
* answers HTTP 404); when a request for a key here fails that way, the
|
|
98
|
+
* provider retries it once on the alias.
|
|
99
|
+
*/
|
|
100
|
+
const GEMINI_RETIRED_MODEL_ALIAS = {
|
|
101
|
+
'gemini-3.1-pro-preview': 'gemini-pro-latest',
|
|
102
|
+
};
|
|
103
|
+
/** Pinned ids already reported as retired, so the warning prints once each. */
|
|
104
|
+
const warnedRetiredGeminiModels = new Set();
|
|
105
|
+
/**
|
|
106
|
+
* Long-context price tier: above `abovePromptTokens` prompt tokens the whole
|
|
107
|
+
* request bills at these per-1M rates instead of the catalog's.
|
|
108
|
+
*/
|
|
109
|
+
const GEMINI_LONG_CONTEXT_PRICING = {
|
|
110
|
+
'gemini-3.1-pro-preview': { abovePromptTokens: 200000, input: 4.00, output: 18.00 },
|
|
111
|
+
'gemini-pro-latest': { abovePromptTokens: 200000, input: 4.00, output: 18.00 },
|
|
112
|
+
'gemini-2.5-pro': { abovePromptTokens: 200000, input: 2.50, output: 15.00 },
|
|
113
|
+
};
|
|
7
114
|
// ---------------------------------------------------------------------------
|
|
8
115
|
// Known model catalog
|
|
9
116
|
// ---------------------------------------------------------------------------
|
|
10
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* Static catalog of well-known Gemini models and their metadata.
|
|
119
|
+
*
|
|
120
|
+
* Ids and token limits come from a live GET /v1beta/models probe and rates from
|
|
121
|
+
* ai.google.dev/gemini-api/docs/pricing, both on 2026-09-29. `getModelInfo` is
|
|
122
|
+
* an exact-id lookup over this array, so a model missing here resolves to
|
|
123
|
+
* undefined and its calls carry no context limit and no price.
|
|
124
|
+
*
|
|
125
|
+
* Tiers advance independently. Flash has reached 3.8 while Pro is at
|
|
126
|
+
* 3.1-preview, the only Pro id served. `gemini-3-pro-preview` appears in
|
|
127
|
+
* Google's doc index and is absent from the live listing.
|
|
128
|
+
*
|
|
129
|
+
* Rates are the standard tier. Prompts over 200K tokens on 3.1 Pro and 2.5
|
|
130
|
+
* Pro bill at the tier in GEMINI_LONG_CONTEXT_PRICING, and Gemini 3.6 / 3.7 /
|
|
131
|
+
* 3.8 Flash list at promotional rates through 2026-12-31 and meter at the
|
|
132
|
+
* post-promotion rate.
|
|
133
|
+
*/
|
|
11
134
|
const GEMINI_MODELS = [
|
|
135
|
+
// --- Pro ---
|
|
12
136
|
{
|
|
13
|
-
modelId: 'gemini-
|
|
137
|
+
modelId: 'gemini-3.1-pro-preview',
|
|
14
138
|
providerId: 'gemini',
|
|
15
|
-
displayName: 'Gemini
|
|
16
|
-
description: '
|
|
139
|
+
displayName: 'Gemini 3.1 Pro Preview',
|
|
140
|
+
description: 'Top pro-tier Gemini model. Always thinks; thinking tokens bill as output.',
|
|
141
|
+
capabilities: ['chat', 'tool_use', 'vision_input', 'json_mode'],
|
|
142
|
+
contextWindowSize: 1048576,
|
|
143
|
+
outputTokenLimit: 65536,
|
|
144
|
+
// Prompts up to 200k tokens; see GEMINI_LONG_CONTEXT_PRICING above that.
|
|
145
|
+
pricePer1MTokensInput: 2.00,
|
|
146
|
+
pricePer1MTokensOutput: 12.00,
|
|
147
|
+
supportsStreaming: true,
|
|
148
|
+
status: 'active',
|
|
149
|
+
},
|
|
150
|
+
// --- Flash. 3.6, 3.7 and 3.8 list at $0.75 / $3.75 through 2026-12-31 and
|
|
151
|
+
// $1.50 / $7.50 from 2027-01-01. They meter at $1.50 / $7.50, as
|
|
152
|
+
// claude-sonnet-5 meters at its sticker rate in the Anthropic catalog, so
|
|
153
|
+
// cost rollups stay conservative and stay right after the promotion. ---
|
|
154
|
+
{
|
|
155
|
+
modelId: 'gemini-3.8-flash',
|
|
156
|
+
providerId: 'gemini',
|
|
157
|
+
displayName: 'Gemini 3.8 Flash',
|
|
158
|
+
description: 'Newest Flash model. Fast and low-cost, with reasoning and multimodal input.',
|
|
159
|
+
capabilities: ['chat', 'tool_use', 'vision_input', 'json_mode'],
|
|
160
|
+
contextWindowSize: 1048576,
|
|
161
|
+
outputTokenLimit: 65536,
|
|
162
|
+
pricePer1MTokensInput: 1.50,
|
|
163
|
+
pricePer1MTokensOutput: 7.50,
|
|
164
|
+
supportsStreaming: true,
|
|
165
|
+
status: 'active',
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
modelId: 'gemini-3.7-flash',
|
|
169
|
+
providerId: 'gemini',
|
|
170
|
+
displayName: 'Gemini 3.7 Flash',
|
|
171
|
+
description: 'Flash model with reasoning and multimodal input.',
|
|
172
|
+
capabilities: ['chat', 'tool_use', 'vision_input', 'json_mode'],
|
|
173
|
+
contextWindowSize: 1048576,
|
|
174
|
+
outputTokenLimit: 65536,
|
|
175
|
+
pricePer1MTokensInput: 1.50,
|
|
176
|
+
pricePer1MTokensOutput: 7.50,
|
|
177
|
+
supportsStreaming: true,
|
|
178
|
+
status: 'active',
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
modelId: 'gemini-3.6-flash',
|
|
182
|
+
providerId: 'gemini',
|
|
183
|
+
displayName: 'Gemini 3.6 Flash',
|
|
184
|
+
description: 'Flash model with reasoning and multimodal input.',
|
|
185
|
+
capabilities: ['chat', 'tool_use', 'vision_input', 'json_mode'],
|
|
186
|
+
contextWindowSize: 1048576,
|
|
187
|
+
outputTokenLimit: 65536,
|
|
188
|
+
pricePer1MTokensInput: 1.50,
|
|
189
|
+
pricePer1MTokensOutput: 7.50,
|
|
190
|
+
supportsStreaming: true,
|
|
191
|
+
status: 'active',
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
modelId: 'gemini-3.5-flash',
|
|
195
|
+
providerId: 'gemini',
|
|
196
|
+
displayName: 'Gemini 3.5 Flash',
|
|
197
|
+
description: 'Flash model with reasoning and multimodal input, at standard pricing.',
|
|
198
|
+
capabilities: ['chat', 'tool_use', 'vision_input', 'json_mode'],
|
|
199
|
+
contextWindowSize: 1048576,
|
|
200
|
+
outputTokenLimit: 65536,
|
|
201
|
+
pricePer1MTokensInput: 1.50,
|
|
202
|
+
pricePer1MTokensOutput: 9.00,
|
|
203
|
+
supportsStreaming: true,
|
|
204
|
+
status: 'active',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
modelId: 'gemini-3-flash-preview',
|
|
208
|
+
providerId: 'gemini',
|
|
209
|
+
displayName: 'Gemini 3 Flash Preview',
|
|
210
|
+
description: 'First Gemini 3 Flash preview.',
|
|
211
|
+
capabilities: ['chat', 'tool_use', 'vision_input', 'json_mode'],
|
|
212
|
+
contextWindowSize: 1048576,
|
|
213
|
+
outputTokenLimit: 65536,
|
|
214
|
+
pricePer1MTokensInput: 0.50,
|
|
215
|
+
pricePer1MTokensOutput: 3.00,
|
|
216
|
+
supportsStreaming: true,
|
|
217
|
+
status: 'active',
|
|
218
|
+
},
|
|
219
|
+
// --- Flash-Lite ---
|
|
220
|
+
{
|
|
221
|
+
modelId: 'gemini-3.5-flash-lite',
|
|
222
|
+
providerId: 'gemini',
|
|
223
|
+
displayName: 'Gemini 3.5 Flash-Lite',
|
|
224
|
+
description: 'Lowest-cost Gemini 3.5 tier, for high-volume and latency-sensitive work.',
|
|
225
|
+
capabilities: ['chat', 'tool_use', 'vision_input', 'json_mode'],
|
|
226
|
+
contextWindowSize: 1048576,
|
|
227
|
+
outputTokenLimit: 65536,
|
|
228
|
+
pricePer1MTokensInput: 0.30,
|
|
229
|
+
pricePer1MTokensOutput: 2.50,
|
|
230
|
+
supportsStreaming: true,
|
|
231
|
+
status: 'active',
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
modelId: 'gemini-3.1-flash-lite',
|
|
235
|
+
providerId: 'gemini',
|
|
236
|
+
displayName: 'Gemini 3.1 Flash-Lite',
|
|
237
|
+
description: 'Lowest-cost Gemini 3.1 tier. Google lists a shutdown date of 2027-05-07.',
|
|
238
|
+
capabilities: ['chat', 'tool_use', 'vision_input', 'json_mode'],
|
|
239
|
+
contextWindowSize: 1048576,
|
|
240
|
+
outputTokenLimit: 65536,
|
|
241
|
+
pricePer1MTokensInput: 0.25,
|
|
242
|
+
pricePer1MTokensOutput: 1.50,
|
|
243
|
+
supportsStreaming: true,
|
|
244
|
+
status: 'active',
|
|
245
|
+
},
|
|
246
|
+
// --- Floating aliases. Google can repoint these without changing the id, so
|
|
247
|
+
// a price here can go stale unnoticed. They are listed because an unlisted id
|
|
248
|
+
// resolves to undefined and meters at zero. generateContent reported these
|
|
249
|
+
// targets on 2026-09-29 (response modelVersion): pro-latest to 3.1 Pro,
|
|
250
|
+
// flash-latest to 3.8 Flash, flash-lite-latest to 3.5 Flash-Lite. ---
|
|
251
|
+
{
|
|
252
|
+
modelId: 'gemini-pro-latest',
|
|
253
|
+
providerId: 'gemini',
|
|
254
|
+
displayName: 'Gemini Pro (latest alias)',
|
|
255
|
+
description: 'Alias of the current pro-tier model (gemini-3.1-pro-preview as of 2026-09-30).',
|
|
256
|
+
capabilities: ['chat', 'tool_use', 'vision_input', 'json_mode'],
|
|
257
|
+
contextWindowSize: 1048576,
|
|
258
|
+
outputTokenLimit: 65536,
|
|
259
|
+
// Priced as the model the alias resolves to today.
|
|
260
|
+
pricePer1MTokensInput: 2.00,
|
|
261
|
+
pricePer1MTokensOutput: 12.00,
|
|
262
|
+
supportsStreaming: true,
|
|
263
|
+
status: 'active',
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
modelId: 'gemini-flash-latest',
|
|
267
|
+
providerId: 'gemini',
|
|
268
|
+
displayName: 'Gemini Flash (latest)',
|
|
269
|
+
description: 'Floating alias for the current Gemini Flash tier.',
|
|
17
270
|
capabilities: ['chat', 'tool_use', 'vision_input', 'json_mode'],
|
|
18
271
|
contextWindowSize: 1048576,
|
|
19
272
|
outputTokenLimit: 65536,
|
|
20
|
-
pricePer1MTokensInput:
|
|
21
|
-
pricePer1MTokensOutput:
|
|
273
|
+
pricePer1MTokensInput: 1.50,
|
|
274
|
+
pricePer1MTokensOutput: 7.50,
|
|
22
275
|
supportsStreaming: true,
|
|
23
276
|
status: 'active',
|
|
24
277
|
},
|
|
278
|
+
{
|
|
279
|
+
modelId: 'gemini-flash-lite-latest',
|
|
280
|
+
providerId: 'gemini',
|
|
281
|
+
displayName: 'Gemini Flash-Lite (latest)',
|
|
282
|
+
description: 'Floating alias for the current Gemini Flash-Lite tier.',
|
|
283
|
+
capabilities: ['chat', 'tool_use', 'vision_input', 'json_mode'],
|
|
284
|
+
contextWindowSize: 1048576,
|
|
285
|
+
outputTokenLimit: 65536,
|
|
286
|
+
pricePer1MTokensInput: 0.30,
|
|
287
|
+
pricePer1MTokensOutput: 2.50,
|
|
288
|
+
supportsStreaming: true,
|
|
289
|
+
status: 'active',
|
|
290
|
+
},
|
|
291
|
+
// --- Gemini 2.5 ---
|
|
25
292
|
{
|
|
26
293
|
modelId: 'gemini-2.5-pro',
|
|
27
294
|
providerId: 'gemini',
|
|
28
295
|
displayName: 'Gemini 2.5 Pro',
|
|
29
|
-
description: '
|
|
296
|
+
description: 'Previous-generation Pro model.',
|
|
30
297
|
capabilities: ['chat', 'tool_use', 'vision_input', 'json_mode'],
|
|
31
298
|
contextWindowSize: 1048576,
|
|
32
299
|
outputTokenLimit: 65536,
|
|
@@ -35,31 +302,93 @@ const GEMINI_MODELS = [
|
|
|
35
302
|
supportsStreaming: true,
|
|
36
303
|
status: 'active',
|
|
37
304
|
},
|
|
305
|
+
{
|
|
306
|
+
modelId: 'gemini-2.5-flash',
|
|
307
|
+
providerId: 'gemini',
|
|
308
|
+
displayName: 'Gemini 2.5 Flash',
|
|
309
|
+
description: 'Previous-generation Flash model with reasoning and multimodal input.',
|
|
310
|
+
capabilities: ['chat', 'tool_use', 'vision_input', 'json_mode'],
|
|
311
|
+
contextWindowSize: 1048576,
|
|
312
|
+
outputTokenLimit: 65536,
|
|
313
|
+
pricePer1MTokensInput: 0.30,
|
|
314
|
+
pricePer1MTokensOutput: 2.50,
|
|
315
|
+
supportsStreaming: true,
|
|
316
|
+
status: 'active',
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
modelId: 'gemini-2.5-flash-lite',
|
|
320
|
+
providerId: 'gemini',
|
|
321
|
+
displayName: 'Gemini 2.5 Flash-Lite',
|
|
322
|
+
description: 'Cheapest Gemini model served.',
|
|
323
|
+
capabilities: ['chat', 'tool_use', 'vision_input', 'json_mode'],
|
|
324
|
+
contextWindowSize: 1048576,
|
|
325
|
+
outputTokenLimit: 65536,
|
|
326
|
+
pricePer1MTokensInput: 0.10,
|
|
327
|
+
pricePer1MTokensOutput: 0.40,
|
|
328
|
+
supportsStreaming: true,
|
|
329
|
+
status: 'active',
|
|
330
|
+
},
|
|
331
|
+
// --- Embeddings. Both ids accept embedContent and batchEmbedContents and
|
|
332
|
+
// return 3072-dimension vectors (probed 2026-09-29). Google's pricing page
|
|
333
|
+
// lists gemini-embedding-2 at $0.20 per 1M text tokens and carries no price
|
|
334
|
+
// for gemini-embedding-001, so that row leaves its price unset: an unset
|
|
335
|
+
// price reads as unknown to a caller, where 0 would read as free. ---
|
|
336
|
+
{
|
|
337
|
+
modelId: 'gemini-embedding-2',
|
|
338
|
+
providerId: 'gemini',
|
|
339
|
+
displayName: 'Gemini Embedding 2',
|
|
340
|
+
description: 'Multimodal embedding model mapping text, images, video, audio and PDFs into one vector space.',
|
|
341
|
+
capabilities: ['embeddings'],
|
|
342
|
+
contextWindowSize: 8192,
|
|
343
|
+
inputTokenLimit: 8192,
|
|
344
|
+
embeddingDimension: 3072,
|
|
345
|
+
pricePer1MTokensInput: 0.20,
|
|
346
|
+
pricePer1MTokensTotal: 0.20,
|
|
347
|
+
supportsStreaming: false,
|
|
348
|
+
status: 'active',
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
modelId: 'gemini-embedding-001',
|
|
352
|
+
providerId: 'gemini',
|
|
353
|
+
displayName: 'Gemini Embedding 001',
|
|
354
|
+
description: 'Text embedding model. An index built with it needs it for queries too, since vectors from different embedding models are not comparable.',
|
|
355
|
+
capabilities: ['embeddings'],
|
|
356
|
+
contextWindowSize: 2048,
|
|
357
|
+
inputTokenLimit: 2048,
|
|
358
|
+
embeddingDimension: 3072,
|
|
359
|
+
supportsStreaming: false,
|
|
360
|
+
status: 'active',
|
|
361
|
+
},
|
|
362
|
+
// --- Retired by Google. generateContent returns HTTP 404 for both ids
|
|
363
|
+
// ("This model models/gemini-2.0-flash is no longer available" for 2.0
|
|
364
|
+
// Flash), probed 2026-09-29 with a gemini-2.5-flash-lite control returning
|
|
365
|
+
// 200. The rows stay as `deprecated` so getModelInfo still answers for a
|
|
366
|
+
// caller holding either id. ---
|
|
38
367
|
{
|
|
39
368
|
modelId: 'gemini-2.0-flash',
|
|
40
369
|
providerId: 'gemini',
|
|
41
|
-
displayName: 'Gemini 2.0 Flash',
|
|
42
|
-
description: '
|
|
370
|
+
displayName: 'Gemini 2.0 Flash (retired)',
|
|
371
|
+
description: 'Retired by Google and no longer served. Google names gemini-3.8-flash as its replacement.',
|
|
43
372
|
capabilities: ['chat', 'tool_use', 'vision_input', 'json_mode'],
|
|
44
373
|
contextWindowSize: 1048576,
|
|
45
374
|
outputTokenLimit: 8192,
|
|
46
375
|
pricePer1MTokensInput: 0.10,
|
|
47
376
|
pricePer1MTokensOutput: 0.40,
|
|
48
377
|
supportsStreaming: true,
|
|
49
|
-
status: '
|
|
378
|
+
status: 'deprecated',
|
|
50
379
|
},
|
|
51
380
|
{
|
|
52
381
|
modelId: 'gemini-1.5-pro',
|
|
53
382
|
providerId: 'gemini',
|
|
54
|
-
displayName: 'Gemini 1.5 Pro',
|
|
55
|
-
description: '
|
|
383
|
+
displayName: 'Gemini 1.5 Pro (retired)',
|
|
384
|
+
description: 'Retired by Google and no longer served. Use gemini-3.1-pro-preview.',
|
|
56
385
|
capabilities: ['chat', 'tool_use', 'vision_input', 'json_mode'],
|
|
57
386
|
contextWindowSize: 2097152,
|
|
58
387
|
outputTokenLimit: 8192,
|
|
59
388
|
pricePer1MTokensInput: 1.25,
|
|
60
389
|
pricePer1MTokensOutput: 5.00,
|
|
61
390
|
supportsStreaming: true,
|
|
62
|
-
status: '
|
|
391
|
+
status: 'deprecated',
|
|
63
392
|
},
|
|
64
393
|
];
|
|
65
394
|
// ---------------------------------------------------------------------------
|
|
@@ -152,9 +481,39 @@ export class GeminiProvider {
|
|
|
152
481
|
const payload = this.buildRequestPayload(modelId, messages, options);
|
|
153
482
|
// Gemini uses model-scoped endpoints: /models/{model}:generateContent
|
|
154
483
|
const endpoint = `/models/${modelId}:generateContent`;
|
|
155
|
-
|
|
484
|
+
let apiResponse;
|
|
485
|
+
try {
|
|
486
|
+
apiResponse = await this.makeApiRequest(endpoint, payload, options.requestTimeout);
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
// A retired pinned id: serve the call from its alias, whose response
|
|
490
|
+
// reports the alias as the model that answered.
|
|
491
|
+
const alias = this.retiredModelAlias(modelId, error);
|
|
492
|
+
if (!alias)
|
|
493
|
+
throw error;
|
|
494
|
+
return this.generateCompletion(alias, messages, options);
|
|
495
|
+
}
|
|
156
496
|
return this.mapResponseToCompletion(apiResponse, modelId);
|
|
157
497
|
}
|
|
498
|
+
/**
|
|
499
|
+
* The alias to retry on when a request for `modelId` failed because the id
|
|
500
|
+
* is gone: HTTP 404 on an id listed in {@link GEMINI_RETIRED_MODEL_ALIAS}.
|
|
501
|
+
* `undefined` for every other error and every other id, which propagate.
|
|
502
|
+
* Warns once per retired id.
|
|
503
|
+
*/
|
|
504
|
+
retiredModelAlias(modelId, error) {
|
|
505
|
+
if (!(error instanceof GeminiProviderError) || error.httpStatus !== 404)
|
|
506
|
+
return undefined;
|
|
507
|
+
const alias = GEMINI_RETIRED_MODEL_ALIAS[modelId];
|
|
508
|
+
if (!alias)
|
|
509
|
+
return undefined;
|
|
510
|
+
if (!warnedRetiredGeminiModels.has(modelId)) {
|
|
511
|
+
warnedRetiredGeminiModels.add(modelId);
|
|
512
|
+
console.warn(`[GeminiProvider] ${modelId} answered 404 (retired, or not available to this key); ` +
|
|
513
|
+
`serving ${alias} instead. Update the pin.`);
|
|
514
|
+
}
|
|
515
|
+
return alias;
|
|
516
|
+
}
|
|
158
517
|
// -------------------------------------------------------------------------
|
|
159
518
|
// Chat completions (streaming)
|
|
160
519
|
// -------------------------------------------------------------------------
|
|
@@ -186,7 +545,19 @@ export class GeminiProvider {
|
|
|
186
545
|
}
|
|
187
546
|
// Streaming endpoint uses ?alt=sse and the API key query param
|
|
188
547
|
const endpoint = `/models/${modelId}:streamGenerateContent`;
|
|
189
|
-
|
|
548
|
+
let stream;
|
|
549
|
+
try {
|
|
550
|
+
stream = await this.makeStreamRequest(endpoint, payload, options.requestTimeout);
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
// A retired pinned id fails here, before any chunk: serve the whole
|
|
554
|
+
// stream from its alias (its chunks carry the alias as modelId).
|
|
555
|
+
const alias = this.retiredModelAlias(modelId, error);
|
|
556
|
+
if (!alias)
|
|
557
|
+
throw error;
|
|
558
|
+
yield* this.generateCompletionStream(alias, messages, options);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
190
561
|
// Accumulators for building the complete response
|
|
191
562
|
let accumulatedContent = '';
|
|
192
563
|
let lastFinishReason = null;
|
|
@@ -240,6 +611,9 @@ export class GeminiProvider {
|
|
|
240
611
|
}
|
|
241
612
|
const parts = candidate.content?.parts ?? [];
|
|
242
613
|
for (const part of parts) {
|
|
614
|
+
// A thought summary is the model's reasoning; it stays out of the answer.
|
|
615
|
+
if (part.thought)
|
|
616
|
+
continue;
|
|
243
617
|
if (part.text !== undefined) {
|
|
244
618
|
// Text delta
|
|
245
619
|
accumulatedContent += part.text;
|
|
@@ -263,6 +637,7 @@ export class GeminiProvider {
|
|
|
263
637
|
toolCallAccum.set(idx, {
|
|
264
638
|
name: part.functionCall.name,
|
|
265
639
|
args: part.functionCall.args,
|
|
640
|
+
thoughtSignature: part.thoughtSignature,
|
|
266
641
|
});
|
|
267
642
|
yield {
|
|
268
643
|
id: responseId,
|
|
@@ -283,6 +658,7 @@ export class GeminiProvider {
|
|
|
283
658
|
// Gemini delivers complete args, so emit them as a single delta
|
|
284
659
|
arguments_delta: JSON.stringify(part.functionCall.args),
|
|
285
660
|
},
|
|
661
|
+
...(part.thoughtSignature ? { thoughtSignature: part.thoughtSignature } : {}),
|
|
286
662
|
}],
|
|
287
663
|
};
|
|
288
664
|
}
|
|
@@ -347,10 +723,20 @@ export class GeminiProvider {
|
|
|
347
723
|
/**
|
|
348
724
|
* Generates embeddings using Gemini's embedding models.
|
|
349
725
|
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
726
|
+
* Sends the texts through `models/{model}:batchEmbedContents` in chunks of
|
|
727
|
+
* at most {@link GEMINI_EMBED_BATCH_LIMIT}, one request per chunk, and
|
|
728
|
+
* returns the vectors in input order. gemini-embedding-001 and
|
|
729
|
+
* gemini-embedding-2 both accept batchEmbedContents (probed 2026-09-29),
|
|
730
|
+
* although neither lists it in `supportedGenerationMethods`.
|
|
731
|
+
* `options.dimensions` is sent as `outputDimensionality`, which both models
|
|
732
|
+
* honor (768 and 1536 probed 2026-09-29); without it they return 3072.
|
|
733
|
+
* Usage sums each call's `usageMetadata.promptTokenCount`, which
|
|
734
|
+
* gemini-embedding-2 reports and gemini-embedding-001 omits, so the older
|
|
735
|
+
* model's calls count zero tokens (probed 2026-09-30). When a later batch
|
|
736
|
+
* fails, the thrown error carries the earlier batches' usage as
|
|
737
|
+
* `partialUsage`, since those calls were billed.
|
|
352
738
|
*
|
|
353
|
-
* @param {string} modelId - Embedding model (e.g., "
|
|
739
|
+
* @param {string} modelId - Embedding model (e.g., "gemini-embedding-001").
|
|
354
740
|
* @param {string[]} texts - Input texts to embed.
|
|
355
741
|
* @param {ProviderEmbeddingOptions} [options] - Optional embedding parameters.
|
|
356
742
|
* @returns {Promise<ProviderEmbeddingResponse>} Embedding vectors.
|
|
@@ -358,25 +744,46 @@ export class GeminiProvider {
|
|
|
358
744
|
*/
|
|
359
745
|
async generateEmbeddings(modelId, texts, options) {
|
|
360
746
|
this.ensureInitialized();
|
|
361
|
-
// Gemini's batch embedding endpoint
|
|
362
747
|
const endpoint = `/models/${modelId}:batchEmbedContents`;
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
748
|
+
const pricePer1M = GEMINI_MODELS.find(m => m.modelId === modelId)?.pricePer1MTokensInput;
|
|
749
|
+
const usageFor = (tokens) => ({
|
|
750
|
+
prompt_tokens: tokens,
|
|
751
|
+
total_tokens: tokens,
|
|
752
|
+
...(pricePer1M !== undefined && tokens > 0 ? { costUSD: (tokens / 1000000) * pricePer1M } : {}),
|
|
753
|
+
});
|
|
754
|
+
const vectors = [];
|
|
755
|
+
let promptTokens = 0;
|
|
756
|
+
for (let start = 0; start < texts.length; start += GEMINI_EMBED_BATCH_LIMIT) {
|
|
757
|
+
const requests = texts.slice(start, start + GEMINI_EMBED_BATCH_LIMIT).map(text => ({
|
|
758
|
+
model: `models/${modelId}`,
|
|
759
|
+
content: { parts: [{ text }] },
|
|
760
|
+
...(options?.dimensions ? { outputDimensionality: options.dimensions } : {}),
|
|
761
|
+
}));
|
|
762
|
+
let apiResponse;
|
|
763
|
+
try {
|
|
764
|
+
apiResponse = await this.makeApiRequest(endpoint, { requests });
|
|
765
|
+
}
|
|
766
|
+
catch (error) {
|
|
767
|
+
// The batches before this one were billed; keep their usage on the
|
|
768
|
+
// error so the caller can still record it.
|
|
769
|
+
if (start > 0 && error instanceof GeminiProviderError) {
|
|
770
|
+
error.partialUsage = usageFor(promptTokens);
|
|
771
|
+
}
|
|
772
|
+
throw error;
|
|
773
|
+
}
|
|
774
|
+
for (const emb of apiResponse.embeddings)
|
|
775
|
+
vectors.push(emb.values);
|
|
776
|
+
promptTokens += apiResponse.usageMetadata?.promptTokenCount ?? 0;
|
|
777
|
+
}
|
|
368
778
|
return {
|
|
369
779
|
object: 'list',
|
|
370
|
-
data:
|
|
780
|
+
data: vectors.map((embedding, index) => ({
|
|
371
781
|
object: 'embedding',
|
|
372
|
-
embedding
|
|
782
|
+
embedding,
|
|
373
783
|
index,
|
|
374
784
|
})),
|
|
375
785
|
model: modelId,
|
|
376
|
-
usage:
|
|
377
|
-
prompt_tokens: 0, // Gemini does not report embedding token counts
|
|
378
|
-
total_tokens: 0,
|
|
379
|
-
},
|
|
786
|
+
usage: usageFor(promptTokens),
|
|
380
787
|
};
|
|
381
788
|
}
|
|
382
789
|
// -------------------------------------------------------------------------
|
|
@@ -458,13 +865,13 @@ export class GeminiProvider {
|
|
|
458
865
|
* 3. Tool messages mapped to `functionResponse` parts within user turns.
|
|
459
866
|
* 4. OpenAI-style tool definitions converted to `functionDeclarations`.
|
|
460
867
|
*
|
|
461
|
-
* @param {string}
|
|
868
|
+
* @param {string} modelId - Target model (endpoint; also its output ceiling and thinking levels).
|
|
462
869
|
* @param {ChatMessage[]} messages - Conversation messages.
|
|
463
870
|
* @param {ModelCompletionOptions} options - Completion options.
|
|
464
871
|
* @returns {Record<string, unknown>} The request body for Gemini's API.
|
|
465
872
|
* @private
|
|
466
873
|
*/
|
|
467
|
-
buildRequestPayload(
|
|
874
|
+
buildRequestPayload(modelId, messages, options) {
|
|
468
875
|
// --- Extract system messages into systemInstruction ---
|
|
469
876
|
// Gemini treats system instructions as a separate top-level field,
|
|
470
877
|
// similar to Anthropic but with a `parts` array wrapper.
|
|
@@ -499,8 +906,15 @@ export class GeminiProvider {
|
|
|
499
906
|
const generationConfig = {};
|
|
500
907
|
if (options.temperature !== undefined)
|
|
501
908
|
generationConfig.temperature = options.temperature;
|
|
502
|
-
if (options.maxTokens !== undefined)
|
|
503
|
-
|
|
909
|
+
if (options.maxTokens !== undefined) {
|
|
910
|
+
// Never above the model's output ceiling: the API rejects the whole
|
|
911
|
+
// request, and a rescue hop's headroom
|
|
912
|
+
// (FallbackProviderEntry.maxTokensHeadroom) can lift a large budget
|
|
913
|
+
// past it. Models outside the catalog pass through unclamped.
|
|
914
|
+
const outputLimit = GEMINI_MODELS.find(m => m.modelId === modelId)?.outputTokenLimit;
|
|
915
|
+
generationConfig.maxOutputTokens =
|
|
916
|
+
typeof outputLimit === 'number' ? Math.min(options.maxTokens, outputLimit) : options.maxTokens;
|
|
917
|
+
}
|
|
504
918
|
if (options.topP !== undefined)
|
|
505
919
|
generationConfig.topP = options.topP;
|
|
506
920
|
if (options.stopSequences?.length)
|
|
@@ -530,6 +944,22 @@ export class GeminiProvider {
|
|
|
530
944
|
if (options.customModelParams?.topK !== undefined) {
|
|
531
945
|
generationConfig.topK = options.customModelParams.topK;
|
|
532
946
|
}
|
|
947
|
+
// Thinking depth. A caller-supplied thinkingConfig is forwarded as is;
|
|
948
|
+
// otherwise the provider-neutral `effort` becomes the model's thinking
|
|
949
|
+
// level when the model is known to take that level. Thinking shares the
|
|
950
|
+
// maxOutputTokens cap: room for it on a rescue hop comes from
|
|
951
|
+
// FallbackProviderEntry.maxTokensHeadroom, not from this provider. The
|
|
952
|
+
// shared `thinking` option is Anthropic's on-switch and is not translated:
|
|
953
|
+
// on a fallback hop it would turn "thinking on" into a thinking-token cap.
|
|
954
|
+
const customThinking = options.customModelParams?.thinkingConfig;
|
|
955
|
+
if (customThinking && typeof customThinking === 'object') {
|
|
956
|
+
generationConfig.thinkingConfig = customThinking;
|
|
957
|
+
}
|
|
958
|
+
else {
|
|
959
|
+
const thinkingLevel = geminiThinkingLevelFor(modelId, options.effort);
|
|
960
|
+
if (thinkingLevel)
|
|
961
|
+
generationConfig.thinkingConfig = { thinkingLevel };
|
|
962
|
+
}
|
|
533
963
|
if (Object.keys(generationConfig).length > 0) {
|
|
534
964
|
payload.generationConfig = generationConfig;
|
|
535
965
|
}
|
|
@@ -542,7 +972,9 @@ export class GeminiProvider {
|
|
|
542
972
|
// and never OpenRouter's routing controls — Gemini 400s on unknown
|
|
543
973
|
// top-level names like `provider`; see openrouter-only-params).
|
|
544
974
|
if (options.customModelParams) {
|
|
545
|
-
|
|
975
|
+
// thinkingConfig belongs inside generationConfig (set above); at the
|
|
976
|
+
// payload root Gemini rejects it as an unknown field.
|
|
977
|
+
const { topK, thinkingConfig, ...rest } = options.customModelParams;
|
|
546
978
|
const passthrough = stripOpenRouterOnlyParams(rest);
|
|
547
979
|
if (passthrough) {
|
|
548
980
|
Object.assign(payload, passthrough);
|
|
@@ -572,9 +1004,14 @@ export class GeminiProvider {
|
|
|
572
1004
|
if (typeof msg.content === 'string' && msg.content) {
|
|
573
1005
|
parts.push({ text: msg.content });
|
|
574
1006
|
}
|
|
575
|
-
// Convert tool_calls to functionCall parts
|
|
1007
|
+
// Convert tool_calls to functionCall parts. Each call's thought
|
|
1008
|
+
// signature is replayed verbatim. A turn with no signature at all (a
|
|
1009
|
+
// call made by another provider, or history that dropped it) gets the
|
|
1010
|
+
// documented placeholder on its first call, which is where Gemini puts
|
|
1011
|
+
// its own; without one Gemini 3 rejects the request with HTTP 400.
|
|
576
1012
|
if (msg.tool_calls?.length) {
|
|
577
|
-
|
|
1013
|
+
const turnHasSignature = msg.tool_calls.some(tc => tc.thoughtSignature);
|
|
1014
|
+
msg.tool_calls.forEach((tc, i) => {
|
|
578
1015
|
let parsedArgs;
|
|
579
1016
|
try {
|
|
580
1017
|
parsedArgs = typeof tc.function.arguments === 'string'
|
|
@@ -584,10 +1021,17 @@ export class GeminiProvider {
|
|
|
584
1021
|
catch {
|
|
585
1022
|
parsedArgs = {};
|
|
586
1023
|
}
|
|
587
|
-
|
|
1024
|
+
const part = {
|
|
588
1025
|
functionCall: { name: tc.function.name, args: parsedArgs },
|
|
589
|
-
}
|
|
590
|
-
|
|
1026
|
+
};
|
|
1027
|
+
if (tc.thoughtSignature) {
|
|
1028
|
+
part.thoughtSignature = tc.thoughtSignature;
|
|
1029
|
+
}
|
|
1030
|
+
else if (!turnHasSignature && i === 0) {
|
|
1031
|
+
part.thoughtSignature = GEMINI_SKIP_THOUGHT_SIGNATURE;
|
|
1032
|
+
}
|
|
1033
|
+
parts.push(part);
|
|
1034
|
+
});
|
|
591
1035
|
}
|
|
592
1036
|
// Ensure at least one part — Gemini requires non-empty parts
|
|
593
1037
|
if (parts.length === 0) {
|
|
@@ -692,8 +1136,10 @@ export class GeminiProvider {
|
|
|
692
1136
|
const candidate = apiResponse.candidates?.[0];
|
|
693
1137
|
const parts = candidate?.content?.parts ?? [];
|
|
694
1138
|
// Collect text from all text parts
|
|
1139
|
+
// Thought-summary parts (present only with includeThoughts) are the
|
|
1140
|
+
// model's reasoning and stay out of the answer.
|
|
695
1141
|
const textParts = parts
|
|
696
|
-
.filter(p => p.text !== undefined)
|
|
1142
|
+
.filter(p => p.text !== undefined && !p.thought)
|
|
697
1143
|
.map(p => p.text);
|
|
698
1144
|
const fullText = textParts.join('');
|
|
699
1145
|
// Collect function calls and convert to OpenAI-style tool_calls
|
|
@@ -706,6 +1152,7 @@ export class GeminiProvider {
|
|
|
706
1152
|
name: p.functionCall.name,
|
|
707
1153
|
arguments: JSON.stringify(p.functionCall.args ?? {}),
|
|
708
1154
|
},
|
|
1155
|
+
...(p.thoughtSignature ? { thoughtSignature: p.thoughtSignature } : {}),
|
|
709
1156
|
}));
|
|
710
1157
|
const hasToolCalls = toolCalls.length > 0;
|
|
711
1158
|
const finishReason = this.mapFinishReason(candidate?.finishReason ?? null);
|
|
@@ -768,7 +1215,12 @@ export class GeminiProvider {
|
|
|
768
1215
|
*/
|
|
769
1216
|
mapUsage(meta, modelId) {
|
|
770
1217
|
const promptTokens = meta?.promptTokenCount ?? 0;
|
|
771
|
-
|
|
1218
|
+
// Thinking tokens are output the model generated and Google bills at the
|
|
1219
|
+
// output rate ("output price, including thinking tokens"), reported apart
|
|
1220
|
+
// from candidatesTokenCount. Counted here so completion tokens and cost
|
|
1221
|
+
// match the bill — the same convention as OpenAI's completion_tokens,
|
|
1222
|
+
// which includes reasoning tokens.
|
|
1223
|
+
const completionTokens = (meta?.candidatesTokenCount ?? 0) + (meta?.thoughtsTokenCount ?? 0);
|
|
772
1224
|
const totalTokens = meta?.totalTokenCount ?? (promptTokens + completionTokens);
|
|
773
1225
|
// Gemini implicit caching (default-on for 2.5+) reports the cached
|
|
774
1226
|
// subset of promptTokenCount in cachedContentTokenCount; normalize it
|
|
@@ -805,6 +1257,7 @@ export class GeminiProvider {
|
|
|
805
1257
|
name: tc.name,
|
|
806
1258
|
arguments: JSON.stringify(tc.args ?? {}),
|
|
807
1259
|
},
|
|
1260
|
+
...(tc.thoughtSignature ? { thoughtSignature: tc.thoughtSignature } : {}),
|
|
808
1261
|
}));
|
|
809
1262
|
}
|
|
810
1263
|
/**
|
|
@@ -823,8 +1276,13 @@ export class GeminiProvider {
|
|
|
823
1276
|
const info = GEMINI_MODELS.find(m => m.modelId === modelId);
|
|
824
1277
|
if (!info?.pricePer1MTokensInput || !info?.pricePer1MTokensOutput)
|
|
825
1278
|
return undefined;
|
|
826
|
-
|
|
827
|
-
|
|
1279
|
+
// Long prompts bill the whole request at a higher tier on some models.
|
|
1280
|
+
const longContext = GEMINI_LONG_CONTEXT_PRICING[modelId];
|
|
1281
|
+
const isLong = longContext !== undefined && inputTokens > longContext.abovePromptTokens;
|
|
1282
|
+
const inputRate = isLong ? longContext.input : info.pricePer1MTokensInput;
|
|
1283
|
+
const outputRate = isLong ? longContext.output : info.pricePer1MTokensOutput;
|
|
1284
|
+
return ((inputTokens / 1000000) * inputRate +
|
|
1285
|
+
(outputTokens / 1000000) * outputRate);
|
|
828
1286
|
}
|
|
829
1287
|
/**
|
|
830
1288
|
* Builds an abort chunk for early stream termination.
|
|
@@ -862,7 +1320,8 @@ export class GeminiProvider {
|
|
|
862
1320
|
*/
|
|
863
1321
|
async makeApiRequest(endpoint, body, requestTimeoutOverride) {
|
|
864
1322
|
// API key is passed as query parameter — Gemini's auth convention
|
|
865
|
-
const
|
|
1323
|
+
const apiKey = this.keyPool?.hasKeys ? this.keyPool.next() : this.config.apiKey;
|
|
1324
|
+
const url = `${this.config.baseURL}${endpoint}?key=${apiKey}`;
|
|
866
1325
|
const headers = {
|
|
867
1326
|
'Content-Type': 'application/json',
|
|
868
1327
|
'User-Agent': 'AgentOS/1.0 (GeminiProvider)',
|
|
@@ -922,7 +1381,9 @@ export class GeminiProvider {
|
|
|
922
1381
|
lastError = new GeminiProviderError(`Request timed out after ${effectiveTimeout}ms.`, 'REQUEST_TIMEOUT');
|
|
923
1382
|
}
|
|
924
1383
|
else {
|
|
925
|
-
lastError = new GeminiProviderError(error instanceof Error
|
|
1384
|
+
lastError = new GeminiProviderError(error instanceof Error
|
|
1385
|
+
? redactUrlSecrets(error.message, apiKey, this.config.baseURL)
|
|
1386
|
+
: 'Network or unknown error', 'NETWORK_ERROR');
|
|
926
1387
|
}
|
|
927
1388
|
if (attempt === this.config.maxRetries - 1)
|
|
928
1389
|
break;
|
|
@@ -947,7 +1408,8 @@ export class GeminiProvider {
|
|
|
947
1408
|
*/
|
|
948
1409
|
async makeStreamRequest(endpoint, body, requestTimeoutOverride) {
|
|
949
1410
|
// Both alt=sse and key= are query params
|
|
950
|
-
const
|
|
1411
|
+
const apiKey = this.keyPool?.hasKeys ? this.keyPool.next() : this.config.apiKey;
|
|
1412
|
+
const url = `${this.config.baseURL}${endpoint}?alt=sse&key=${apiKey}`;
|
|
951
1413
|
const headers = {
|
|
952
1414
|
'Content-Type': 'application/json',
|
|
953
1415
|
'User-Agent': 'AgentOS/1.0 (GeminiProvider)',
|
|
@@ -980,7 +1442,9 @@ export class GeminiProvider {
|
|
|
980
1442
|
clearTimeout(timeoutId);
|
|
981
1443
|
if (error instanceof GeminiProviderError)
|
|
982
1444
|
throw error;
|
|
983
|
-
throw new GeminiProviderError(error instanceof Error
|
|
1445
|
+
throw new GeminiProviderError(error instanceof Error
|
|
1446
|
+
? redactUrlSecrets(error.message, apiKey, this.config.baseURL)
|
|
1447
|
+
: 'Failed to connect to Gemini stream.', 'STREAM_CONNECTION_FAILED');
|
|
984
1448
|
}
|
|
985
1449
|
}
|
|
986
1450
|
// -------------------------------------------------------------------------
|