@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.
Files changed (79) hide show
  1. package/README.md +1 -7
  2. package/dist/api/embedText.d.ts +9 -4
  3. package/dist/api/embedText.d.ts.map +1 -1
  4. package/dist/api/embedText.js +114 -8
  5. package/dist/api/embedText.js.map +1 -1
  6. package/dist/api/generateText.d.ts +57 -1
  7. package/dist/api/generateText.d.ts.map +1 -1
  8. package/dist/api/generateText.js +56 -14
  9. package/dist/api/generateText.js.map +1 -1
  10. package/dist/api/runtime/provider-defaults.d.ts.map +1 -1
  11. package/dist/api/runtime/provider-defaults.js +6 -2
  12. package/dist/api/runtime/provider-defaults.js.map +1 -1
  13. package/dist/api/sessionTranscript.d.ts +6 -0
  14. package/dist/api/sessionTranscript.d.ts.map +1 -1
  15. package/dist/api/sessionTranscript.js.map +1 -1
  16. package/dist/api/streamText.d.ts.map +1 -1
  17. package/dist/api/streamText.js +9 -6
  18. package/dist/api/streamText.js.map +1 -1
  19. package/dist/cognition/substrate/ConversationHistoryManager.d.ts.map +1 -1
  20. package/dist/cognition/substrate/ConversationHistoryManager.js +2 -0
  21. package/dist/cognition/substrate/ConversationHistoryManager.js.map +1 -1
  22. package/dist/cognition/substrate/GMI.d.ts.map +1 -1
  23. package/dist/cognition/substrate/GMI.js +3 -1
  24. package/dist/cognition/substrate/GMI.js.map +1 -1
  25. package/dist/cognition/substrate/IGMI.d.ts +5 -0
  26. package/dist/cognition/substrate/IGMI.d.ts.map +1 -1
  27. package/dist/cognition/substrate/IGMI.js.map +1 -1
  28. package/dist/core/llm/PromptEngine.d.ts.map +1 -1
  29. package/dist/core/llm/PromptEngine.js +1 -0
  30. package/dist/core/llm/PromptEngine.js.map +1 -1
  31. package/dist/core/llm/providers/IProvider.d.ts +9 -0
  32. package/dist/core/llm/providers/IProvider.d.ts.map +1 -1
  33. package/dist/core/llm/providers/errors/GeminiProviderError.d.ts +11 -0
  34. package/dist/core/llm/providers/errors/GeminiProviderError.d.ts.map +1 -1
  35. package/dist/core/llm/providers/errors/GeminiProviderError.js.map +1 -1
  36. package/dist/core/llm/providers/implementations/AnthropicProvider.d.ts +27 -6
  37. package/dist/core/llm/providers/implementations/AnthropicProvider.d.ts.map +1 -1
  38. package/dist/core/llm/providers/implementations/AnthropicProvider.js +78 -15
  39. package/dist/core/llm/providers/implementations/AnthropicProvider.js.map +1 -1
  40. package/dist/core/llm/providers/implementations/ClaudeCodeProvider.d.ts +1 -1
  41. package/dist/core/llm/providers/implementations/ClaudeCodeProvider.d.ts.map +1 -1
  42. package/dist/core/llm/providers/implementations/ClaudeCodeProvider.js +70 -7
  43. package/dist/core/llm/providers/implementations/ClaudeCodeProvider.js.map +1 -1
  44. package/dist/core/llm/providers/implementations/GeminiCLIBridge.js +1 -1
  45. package/dist/core/llm/providers/implementations/GeminiCLIProvider.d.ts.map +1 -1
  46. package/dist/core/llm/providers/implementations/GeminiCLIProvider.js +56 -4
  47. package/dist/core/llm/providers/implementations/GeminiCLIProvider.js.map +1 -1
  48. package/dist/core/llm/providers/implementations/GeminiProvider.d.ts +22 -5
  49. package/dist/core/llm/providers/implementations/GeminiProvider.d.ts.map +1 -1
  50. package/dist/core/llm/providers/implementations/GeminiProvider.js +512 -48
  51. package/dist/core/llm/providers/implementations/GeminiProvider.js.map +1 -1
  52. package/dist/core/llm/providers/implementations/OpenAIProvider.d.ts +61 -0
  53. package/dist/core/llm/providers/implementations/OpenAIProvider.d.ts.map +1 -1
  54. package/dist/core/llm/providers/implementations/OpenAIProvider.js +164 -22
  55. package/dist/core/llm/providers/implementations/OpenAIProvider.js.map +1 -1
  56. package/dist/core/llm/providers/implementations/OpenRouterProvider.d.ts.map +1 -1
  57. package/dist/core/llm/providers/implementations/OpenRouterProvider.js +6 -2
  58. package/dist/core/llm/providers/implementations/OpenRouterProvider.js.map +1 -1
  59. package/dist/core/llm/providers/model-forced-tool-choice.d.ts +32 -15
  60. package/dist/core/llm/providers/model-forced-tool-choice.d.ts.map +1 -1
  61. package/dist/core/llm/providers/model-forced-tool-choice.js +33 -16
  62. package/dist/core/llm/providers/model-forced-tool-choice.js.map +1 -1
  63. package/dist/core/llm/streaming/StreamingBatcher.d.ts.map +1 -1
  64. package/dist/core/llm/streaming/StreamingBatcher.js +3 -0
  65. package/dist/core/llm/streaming/StreamingBatcher.js.map +1 -1
  66. package/dist/core/llm/streaming/StreamingReconstructor.d.ts +2 -0
  67. package/dist/core/llm/streaming/StreamingReconstructor.d.ts.map +1 -1
  68. package/dist/core/llm/streaming/StreamingReconstructor.js +12 -1
  69. package/dist/core/llm/streaming/StreamingReconstructor.js.map +1 -1
  70. package/dist/io/vision/providers/LLMVisionProvider.d.ts +2 -2
  71. package/dist/io/vision/providers/LLMVisionProvider.js +1 -1
  72. package/dist/io/vision/types.d.ts +1 -1
  73. package/dist/orchestration/pipeline/query/types.d.ts.map +1 -1
  74. package/dist/orchestration/pipeline/query/types.js +3 -2
  75. package/dist/orchestration/pipeline/query/types.js.map +1 -1
  76. package/dist/orchestration/planning/ProviderAssignmentEngine.d.ts.map +1 -1
  77. package/dist/orchestration/planning/ProviderAssignmentEngine.js +2 -1
  78. package/dist/orchestration/planning/ProviderAssignmentEngine.js.map +1 -1
  79. 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
- /** Static catalog of well-known Gemini models and their metadata. */
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-2.5-flash',
137
+ modelId: 'gemini-3.1-pro-preview',
14
138
  providerId: 'gemini',
15
- displayName: 'Gemini 2.5 Flash',
16
- description: 'Fast, cost-effective model with strong reasoning and multimodal capabilities.',
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: 0.15,
21
- pricePer1MTokensOutput: 0.60,
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: 'Most capable Gemini model for complex reasoning and analysis.',
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: 'Previous-generation fast model with strong performance.',
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: 'active',
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: 'Stable model with 2M context window for long-document tasks.',
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: 'active',
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
- const apiResponse = await this.makeApiRequest(endpoint, payload, options.requestTimeout);
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
- const stream = await this.makeStreamRequest(endpoint, payload, options.requestTimeout);
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
- * Uses the `models/{model}:embedContent` endpoint. Currently Gemini
351
- * supports embedding one text at a time, so we batch sequentially.
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., "text-embedding-004").
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 requests = texts.map(text => ({
364
- model: `models/${modelId}`,
365
- content: { parts: [{ text }] },
366
- }));
367
- const apiResponse = await this.makeApiRequest(endpoint, { requests });
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: apiResponse.embeddings.map((emb, index) => ({
780
+ data: vectors.map((embedding, index) => ({
371
781
  object: 'embedding',
372
- embedding: emb.values,
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} _modelId - Target model (used for endpoint, not in body).
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(_modelId, messages, options) {
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
- generationConfig.maxOutputTokens = options.maxTokens;
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
- const { topK, ...rest } = options.customModelParams;
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
- for (const tc of msg.tool_calls) {
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
- parts.push({
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
- const completionTokens = meta?.candidatesTokenCount ?? 0;
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
- return ((inputTokens / 1000000) * info.pricePer1MTokensInput +
827
- (outputTokens / 1000000) * info.pricePer1MTokensOutput);
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 url = `${this.config.baseURL}${endpoint}?key=${this.keyPool?.hasKeys ? this.keyPool.next() : this.config.apiKey}`;
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 ? error.message : 'Network or unknown error', 'NETWORK_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 url = `${this.config.baseURL}${endpoint}?alt=sse&key=${this.keyPool?.hasKeys ? this.keyPool.next() : this.config.apiKey}`;
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 ? error.message : 'Failed to connect to Gemini stream.', 'STREAM_CONNECTION_FAILED');
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
  // -------------------------------------------------------------------------