coding-tool-x 3.8.0 → 3.8.2

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 (53) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +4 -6
  3. package/dist/web/assets/{Analytics-8dRv1aYW.js → Analytics-COEUXmd4.js} +1 -1
  4. package/dist/web/assets/{ConfigTemplates-BQRXSICO.js → ConfigTemplates-DLLkS53H.js} +1 -1
  5. package/dist/web/assets/{Home-BtzWhKIa.js → Home-DCEyLzwX.js} +1 -1
  6. package/dist/web/assets/{PluginManager-BTa-Ygf6.js → PluginManager-Oe1ZHoMO.js} +1 -1
  7. package/dist/web/assets/{ProjectList-F_cB8ldA.js → ProjectList-CWjuAFof.js} +1 -1
  8. package/dist/web/assets/{SessionList-LOrbPQvu.js → SessionList-v3ehGg9U.js} +1 -1
  9. package/dist/web/assets/{SkillManager-BzI_t-yV.js → SkillManager-DUF_qP5S.js} +1 -1
  10. package/dist/web/assets/{WorkspaceManager-Db9RGiz7.js → WorkspaceManager-CjAWBosO.js} +1 -1
  11. package/dist/web/assets/index-D5hyHPj7.js +2 -0
  12. package/dist/web/index.html +1 -1
  13. package/docs/superpowers/plans/2026-08-10-codex-conversion-compatibility.md +251 -0
  14. package/docs/superpowers/plans/2026-08-11-windows-command-window-hiding.md +90 -0
  15. package/docs/superpowers/specs/2026-08-10-codex-conversion-compatibility-design.md +61 -0
  16. package/docs/superpowers/specs/2026-08-11-windows-command-window-hiding-design.md +44 -0
  17. package/package.json +2 -2
  18. package/src/commands/update.js +1 -1
  19. package/src/config/paths.js +1 -0
  20. package/src/server/api/codex-sessions.js +14 -14
  21. package/src/server/api/gemini-sessions.js +52 -14
  22. package/src/server/api/omp-projects.js +2 -2
  23. package/src/server/api/omp-sessions.js +61 -23
  24. package/src/server/api/sessions.js +44 -2
  25. package/src/server/opencode-proxy-server.js +55 -1037
  26. package/src/server/services/claude-wire.js +694 -0
  27. package/src/server/services/codex-sessions.js +41 -233
  28. package/src/server/services/codex-wire.js +219 -0
  29. package/src/server/services/dashboard-snapshot-worker.js +5 -5
  30. package/src/server/services/gemini-sessions.js +67 -157
  31. package/src/server/services/gemini-wire.js +759 -0
  32. package/src/server/services/model-detector.js +49 -131
  33. package/src/server/services/omp-channels.js +23 -4
  34. package/src/server/services/omp-gateway.js +29 -1
  35. package/src/server/services/omp-sessions.js +102 -105
  36. package/src/server/services/omp-settings-manager.js +27 -3
  37. package/src/server/services/opencode-gateway-adapters.js +22 -1147
  38. package/src/server/services/opencode-normalization.js +546 -0
  39. package/src/server/services/opencode-sessions.js +339 -546
  40. package/src/server/services/session-converter.js +4 -4
  41. package/src/server/services/session-history-adapters/claude.js +235 -0
  42. package/src/server/services/session-history-adapters/codex.js +158 -0
  43. package/src/server/services/session-history-adapters/gemini.js +292 -0
  44. package/src/server/services/session-history-adapters/index.js +20 -0
  45. package/src/server/services/session-history-adapters/omp.js +256 -0
  46. package/src/server/services/session-history-index.js +930 -0
  47. package/src/server/services/session-history-worker.js +99 -0
  48. package/src/server/services/session-snapshot-worker.js +7 -7
  49. package/src/server/services/sessions.js +167 -775
  50. package/src/server/services/speed-test.js +85 -303
  51. package/src/server/services/sqlite-connection.js +78 -0
  52. package/src/server/services/workspace-service.js +3 -3
  53. package/dist/web/assets/index-DIWJmyL6.js +0 -2
@@ -0,0 +1,759 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ normalizeOpenCodeMessages,
5
+ generateToolCallId,
6
+ normalizeToolResultContent,
7
+ extractText,
8
+ normalizeStopSequences
9
+ } = require('./opencode-normalization');
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Gemini CLI constants
13
+ // ---------------------------------------------------------------------------
14
+ const GEMINI_CLI_USER_AGENT = 'google-api-nodejs-client/9.15.1';
15
+ const GEMINI_CLI_API_CLIENT = 'gl-node/22.17.0';
16
+ const GEMINI_CLI_CLIENT_METADATA =
17
+ 'ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI';
18
+ const GEMINI_PUBLIC_USER_AGENT = 'google-genai-sdk/0.8.0';
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Header override helper
22
+ // ---------------------------------------------------------------------------
23
+ function normalizeHeaderName(name) {
24
+ return String(name || '').toLowerCase().trim();
25
+ }
26
+
27
+ /**
28
+ * Merge explicit header overrides from options.headers and
29
+ * options.providerConfig?.headers into a base headers map.
30
+ * - Merges case-insensitively; explicit value wins.
31
+ * - If either credential header (authorization or x-goog-api-key) is explicit,
32
+ * the generated opposite credential header is removed.
33
+ */
34
+ function applyGeminiHeaderOverrides(baseHeaders, options = {}) {
35
+ const merged = { ...baseHeaders };
36
+
37
+ const overrideEntries = [];
38
+
39
+ const headersSource = options.headers;
40
+ if (headersSource && typeof headersSource === 'object' && !Array.isArray(headersSource)) {
41
+ for (const [key, value] of Object.entries(headersSource)) {
42
+ overrideEntries.push([normalizeHeaderName(key), String(value)]);
43
+ }
44
+ }
45
+
46
+ const providerConfig = options.providerConfig;
47
+ const providerHeaders =
48
+ providerConfig &&
49
+ typeof providerConfig === 'object' &&
50
+ !Array.isArray(providerConfig)
51
+ ? providerConfig.headers
52
+ : undefined;
53
+ if (providerHeaders && typeof providerHeaders === 'object' && !Array.isArray(providerHeaders)) {
54
+ for (const [key, value] of Object.entries(providerHeaders)) {
55
+ overrideEntries.push([normalizeHeaderName(key), String(value)]);
56
+ }
57
+ }
58
+
59
+ for (const [lcKey, value] of overrideEntries) {
60
+ merged[lcKey] = value;
61
+ }
62
+
63
+ // If either credential header is explicitly overridden, remove the
64
+ // generated opposite credential.
65
+ const hasExplicitAuth = overrideEntries.some(
66
+ ([lcKey]) => lcKey === 'authorization'
67
+ );
68
+ const hasExplicitApiKey = overrideEntries.some(
69
+ ([lcKey]) => lcKey === 'x-goog-api-key'
70
+ );
71
+
72
+ if (hasExplicitAuth && merged['x-goog-api-key'] !== undefined) {
73
+ delete merged['x-goog-api-key'];
74
+ }
75
+ if (hasExplicitApiKey && merged['authorization'] !== undefined) {
76
+ delete merged['authorization'];
77
+ }
78
+
79
+ return merged;
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Gemini-specific conversion helpers
84
+ // ---------------------------------------------------------------------------
85
+ function normalizeOpenAiToolsToGemini(tools = []) {
86
+ if (!Array.isArray(tools)) return [];
87
+
88
+ const functionDeclarations = [];
89
+ const builtInTools = [];
90
+ const appendBuiltInTool = toolNode => {
91
+ if (!toolNode || typeof toolNode !== 'object') return;
92
+ builtInTools.push(toolNode);
93
+ };
94
+
95
+ for (const tool of tools) {
96
+ if (!tool || typeof tool !== 'object') continue;
97
+
98
+ if (
99
+ tool.type === 'function' &&
100
+ tool.function &&
101
+ typeof tool.function === 'object'
102
+ ) {
103
+ const fn = tool.function;
104
+ if (!fn.name) continue;
105
+ functionDeclarations.push({
106
+ name: fn.name,
107
+ description: fn.description || '',
108
+ parameters: fn.parameters || { type: 'object', properties: {} }
109
+ });
110
+ continue;
111
+ }
112
+
113
+ if (tool.type === 'function' && tool.name) {
114
+ functionDeclarations.push({
115
+ name: tool.name,
116
+ description: tool.description || '',
117
+ parameters: tool.parameters || { type: 'object', properties: {} }
118
+ });
119
+ continue;
120
+ }
121
+
122
+ const normalizedType = String(tool.type || '').trim().toLowerCase();
123
+
124
+ if (
125
+ tool.google_search &&
126
+ typeof tool.google_search === 'object'
127
+ ) {
128
+ appendBuiltInTool({ googleSearch: tool.google_search });
129
+ continue;
130
+ }
131
+ if (
132
+ tool.code_execution &&
133
+ typeof tool.code_execution === 'object'
134
+ ) {
135
+ appendBuiltInTool({ codeExecution: tool.code_execution });
136
+ continue;
137
+ }
138
+ if (
139
+ tool.url_context &&
140
+ typeof tool.url_context === 'object'
141
+ ) {
142
+ appendBuiltInTool({ urlContext: tool.url_context });
143
+ continue;
144
+ }
145
+
146
+ if (
147
+ normalizedType === 'google_search' ||
148
+ normalizedType === 'web_search' ||
149
+ normalizedType === 'web_search_preview'
150
+ ) {
151
+ const searchConfig =
152
+ tool.web_search && typeof tool.web_search === 'object'
153
+ ? tool.web_search
154
+ : tool.googleSearch && typeof tool.googleSearch === 'object'
155
+ ? tool.googleSearch
156
+ : {};
157
+ appendBuiltInTool({ googleSearch: searchConfig });
158
+ continue;
159
+ }
160
+
161
+ if (
162
+ normalizedType === 'code_execution' ||
163
+ normalizedType === 'code_interpreter'
164
+ ) {
165
+ const executionConfig =
166
+ tool.codeExecution && typeof tool.codeExecution === 'object'
167
+ ? tool.codeExecution
168
+ : {};
169
+ appendBuiltInTool({ codeExecution: executionConfig });
170
+ continue;
171
+ }
172
+
173
+ if (normalizedType === 'url_context') {
174
+ const urlContextConfig =
175
+ tool.urlContext && typeof tool.urlContext === 'object'
176
+ ? tool.urlContext
177
+ : {};
178
+ appendBuiltInTool({ urlContext: urlContextConfig });
179
+ }
180
+ }
181
+
182
+ const normalizedTools = [];
183
+ if (functionDeclarations.length > 0) {
184
+ normalizedTools.push({ functionDeclarations });
185
+ }
186
+ if (builtInTools.length > 0) {
187
+ normalizedTools.push(...builtInTools);
188
+ }
189
+ return normalizedTools;
190
+ }
191
+
192
+ function normalizeToolChoiceToGemini(toolChoice) {
193
+ if (!toolChoice) return undefined;
194
+
195
+ if (typeof toolChoice === 'string') {
196
+ if (toolChoice === 'auto')
197
+ return { functionCallingConfig: { mode: 'AUTO' } };
198
+ if (toolChoice === 'required')
199
+ return { functionCallingConfig: { mode: 'ANY' } };
200
+ if (toolChoice === 'none')
201
+ return { functionCallingConfig: { mode: 'NONE' } };
202
+ return undefined;
203
+ }
204
+
205
+ if (typeof toolChoice === 'object') {
206
+ const functionName = toolChoice.function?.name || toolChoice.name;
207
+ if (toolChoice.type === 'function' && functionName) {
208
+ return {
209
+ functionCallingConfig: {
210
+ mode: 'ANY',
211
+ allowedFunctionNames: [functionName]
212
+ }
213
+ };
214
+ }
215
+ if (toolChoice.type === 'auto')
216
+ return { functionCallingConfig: { mode: 'AUTO' } };
217
+ if (toolChoice.type === 'required')
218
+ return { functionCallingConfig: { mode: 'ANY' } };
219
+ if (toolChoice.type === 'none')
220
+ return { functionCallingConfig: { mode: 'NONE' } };
221
+ }
222
+
223
+ return undefined;
224
+ }
225
+
226
+ function normalizeReasoningEffortToGemini(reasoningEffort) {
227
+ const effort = String(reasoningEffort || '').trim().toLowerCase();
228
+ if (!effort) return undefined;
229
+ if (effort === 'none') {
230
+ return {
231
+ includeThoughts: false,
232
+ thinkingBudget: 0
233
+ };
234
+ }
235
+ if (effort === 'auto') {
236
+ return {
237
+ includeThoughts: true,
238
+ thinkingBudget: -1
239
+ };
240
+ }
241
+ if (effort === 'low' || effort === 'medium' || effort === 'high') {
242
+ return {
243
+ includeThoughts: true,
244
+ thinkingLevel: effort
245
+ };
246
+ }
247
+ return undefined;
248
+ }
249
+
250
+ function normalizeGeminiResponseModalities(modalities) {
251
+ if (!Array.isArray(modalities)) return undefined;
252
+ const mapped = modalities
253
+ .map(item => String(item || '').trim().toLowerCase())
254
+ .filter(Boolean)
255
+ .map(item => {
256
+ if (item === 'text') return 'TEXT';
257
+ if (item === 'image') return 'IMAGE';
258
+ return '';
259
+ })
260
+ .filter(Boolean);
261
+ return mapped.length > 0 ? mapped : undefined;
262
+ }
263
+
264
+ function normalizeGeminiFunctionResponsePayload(value) {
265
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
266
+ return value;
267
+ }
268
+ if (typeof value === 'string') {
269
+ const trimmed = value.trim();
270
+ if (!trimmed) return { content: '' };
271
+ try {
272
+ const parsed = JSON.parse(trimmed);
273
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
274
+ return parsed;
275
+ }
276
+ } catch {
277
+ return { content: value };
278
+ }
279
+ return { content: value };
280
+ }
281
+ return { content: normalizeToolResultContent(value) };
282
+ }
283
+
284
+ function normalizeGeminiMediaType(
285
+ value,
286
+ fallback = 'application/octet-stream'
287
+ ) {
288
+ const mediaType = typeof value === 'string' ? value.trim() : '';
289
+ return mediaType || fallback;
290
+ }
291
+
292
+ function buildGeminiPartFromClaudeMediaBlock(block) {
293
+ if (!block || typeof block !== 'object') return null;
294
+ const source =
295
+ block.source && typeof block.source === 'object'
296
+ ? block.source
297
+ : null;
298
+ if (!source) return null;
299
+
300
+ const blockType = String(block.type || '').trim().toLowerCase();
301
+ const defaultMimeType =
302
+ blockType === 'image' ? 'image/png' : 'application/octet-stream';
303
+ const sourceType = String(source.type || '').trim().toLowerCase();
304
+ const mediaType = normalizeGeminiMediaType(
305
+ source.media_type || source.mime_type,
306
+ defaultMimeType
307
+ );
308
+
309
+ if (
310
+ sourceType === 'base64' &&
311
+ typeof source.data === 'string' &&
312
+ source.data.trim()
313
+ ) {
314
+ return {
315
+ inlineData: {
316
+ mimeType: mediaType,
317
+ data: source.data
318
+ }
319
+ };
320
+ }
321
+
322
+ if (
323
+ sourceType === 'url' &&
324
+ typeof source.url === 'string' &&
325
+ source.url.trim()
326
+ ) {
327
+ return {
328
+ fileData: {
329
+ mimeType: mediaType,
330
+ fileUri: source.url.trim()
331
+ }
332
+ };
333
+ }
334
+
335
+ return null;
336
+ }
337
+
338
+ function buildGeminiContents(messages = []) {
339
+ const contents = [];
340
+ const toolNameById = new Map();
341
+
342
+ for (const message of messages) {
343
+ if (!message || typeof message !== 'object') continue;
344
+ const role = message.role === 'assistant' ? 'model' : 'user';
345
+ const contentBlocks = Array.isArray(message.content)
346
+ ? message.content
347
+ : [message.content];
348
+ const parts = [];
349
+
350
+ for (const block of contentBlocks) {
351
+ if (!block || typeof block !== 'object') {
352
+ const text = extractText(block);
353
+ if (text) parts.push({ text });
354
+ continue;
355
+ }
356
+
357
+ if (block.type === 'tool_use' && block.name) {
358
+ const callId = String(block.id || generateToolCallId());
359
+ const args =
360
+ block.input &&
361
+ typeof block.input === 'object' &&
362
+ !Array.isArray(block.input)
363
+ ? block.input
364
+ : {};
365
+ toolNameById.set(callId, block.name);
366
+ parts.push({
367
+ functionCall: {
368
+ name: block.name,
369
+ args
370
+ }
371
+ });
372
+ continue;
373
+ }
374
+
375
+ if (block.type === 'tool_result') {
376
+ const toolUseId = String(block.tool_use_id || block.id || '');
377
+ const toolName = block.name || toolNameById.get(toolUseId);
378
+ if (!toolName) {
379
+ const text = normalizeToolResultContent(block.content);
380
+ if (text) parts.push({ text });
381
+ continue;
382
+ }
383
+
384
+ parts.push({
385
+ functionResponse: {
386
+ name: toolName,
387
+ response: normalizeGeminiFunctionResponsePayload(
388
+ block.content
389
+ )
390
+ }
391
+ });
392
+ continue;
393
+ }
394
+
395
+ if (block.type === 'image' || block.type === 'document') {
396
+ const mediaPart = buildGeminiPartFromClaudeMediaBlock(block);
397
+ if (mediaPart) {
398
+ parts.push(mediaPart);
399
+ continue;
400
+ }
401
+ }
402
+
403
+ const text = extractText(block);
404
+ if (text) parts.push({ text });
405
+ }
406
+
407
+ if (parts.length === 0) continue;
408
+ contents.push({ role, parts });
409
+ }
410
+
411
+ return contents;
412
+ }
413
+
414
+ // ---------------------------------------------------------------------------
415
+ // Header construction
416
+ // ---------------------------------------------------------------------------
417
+ function buildGeminiPublicHeaders(apiKey, stream, networkHeaders, headerOptions) {
418
+ if (!networkHeaders) {
419
+ return applyGeminiHeaderOverrides({}, headerOptions);
420
+ }
421
+
422
+ const baseHeaders = {
423
+ 'x-goog-api-key': apiKey || '',
424
+ 'content-type': 'application/json',
425
+ accept: stream ? 'text/event-stream' : 'application/json',
426
+ 'user-agent': GEMINI_PUBLIC_USER_AGENT
427
+ };
428
+
429
+ return applyGeminiHeaderOverrides(baseHeaders, headerOptions);
430
+ }
431
+
432
+ function buildGeminiCliHeaders(apiKey, stream, networkHeaders, headerOptions) {
433
+ if (!networkHeaders) {
434
+ return applyGeminiHeaderOverrides({}, headerOptions);
435
+ }
436
+
437
+ const baseHeaders = {
438
+ authorization: apiKey ? `Bearer ${apiKey}` : '',
439
+ 'x-goog-api-key': apiKey || '',
440
+ 'content-type': 'application/json',
441
+ accept: stream ? 'text/event-stream' : 'application/json',
442
+ 'user-agent': GEMINI_CLI_USER_AGENT,
443
+ 'x-goog-api-client': GEMINI_CLI_API_CLIENT,
444
+ 'client-metadata': GEMINI_CLI_CLIENT_METADATA
445
+ };
446
+
447
+ return applyGeminiHeaderOverrides(baseHeaders, headerOptions);
448
+ }
449
+
450
+ // ---------------------------------------------------------------------------
451
+ // URL construction
452
+ // ---------------------------------------------------------------------------
453
+
454
+ /**
455
+ * Detect whether a Gemini base URL expects CLI (Cloud Code Assist) format.
456
+ * Mirrors the proxy's shouldUseGeminiCliFormat.
457
+ */
458
+ function shouldUseGeminiCliFormat(baseUrl = '') {
459
+ let parsedUrl;
460
+ try {
461
+ parsedUrl = new URL(
462
+ String(baseUrl || '').trim() ||
463
+ 'https://generativelanguage.googleapis.com'
464
+ );
465
+ } catch {
466
+ return false;
467
+ }
468
+
469
+ const host = String(parsedUrl.hostname || '').toLowerCase();
470
+ const pathname = parsedUrl.pathname.replace(/\/+$/, '');
471
+
472
+ if (pathname.includes('/v1beta') || pathname.includes('/models/')) {
473
+ return false;
474
+ }
475
+ if (
476
+ pathname.includes('/v1internal') ||
477
+ pathname.endsWith(':generateContent') ||
478
+ pathname.endsWith(':streamGenerateContent')
479
+ ) {
480
+ return true;
481
+ }
482
+ if (host.includes('cloudcode-pa.googleapis.com')) {
483
+ return true;
484
+ }
485
+ if (!pathname || pathname === '/') {
486
+ return (
487
+ !host.includes('generativelanguage.googleapis.com') &&
488
+ !host.includes('aiplatform.googleapis.com')
489
+ );
490
+ }
491
+ return false;
492
+ }
493
+
494
+ function buildGeminiCliTargetPath(parsedUrl, stream = false) {
495
+ let pathname = parsedUrl.pathname.replace(/\/+$/, '');
496
+ const method = stream ? 'streamGenerateContent' : 'generateContent';
497
+ if (pathname.includes('/v1beta') || pathname.includes('/models/')) {
498
+ return `/v1internal:${method}`;
499
+ }
500
+
501
+ if (!pathname || pathname === '/') {
502
+ return `/v1internal:${method}`;
503
+ }
504
+ if (pathname.endsWith(':streamGenerateContent')) {
505
+ return stream
506
+ ? pathname
507
+ : pathname.replace(/:streamGenerateContent$/, ':generateContent');
508
+ }
509
+ if (pathname.endsWith(':generateContent')) {
510
+ return stream
511
+ ? pathname.replace(/:generateContent$/, ':streamGenerateContent')
512
+ : pathname;
513
+ }
514
+ if (pathname.endsWith('/v1internal')) {
515
+ return `${pathname}:${method}`;
516
+ }
517
+ return `${pathname}/v1internal:${method}`;
518
+ }
519
+
520
+ function buildGeminiNativeTargetUrl(
521
+ baseUrl = '',
522
+ model = '',
523
+ apiKey = '',
524
+ options = {}
525
+ ) {
526
+ const modelName = String(model || '').trim();
527
+ if (!modelName) return '';
528
+ const stream = !!options.stream;
529
+
530
+ const trimmed = String(baseUrl || '').trim();
531
+ let targetUrl;
532
+ try {
533
+ targetUrl = new URL(
534
+ trimmed || 'https://generativelanguage.googleapis.com'
535
+ );
536
+ } catch {
537
+ // Non-empty but malformed base URL returns empty string
538
+ if (trimmed) return '';
539
+ targetUrl = new URL('https://generativelanguage.googleapis.com');
540
+ }
541
+
542
+ // Malformed URLs that parse without hostname (e.g. 'not-a-url:::')
543
+ if (trimmed && !targetUrl.hostname) return '';
544
+
545
+ let pathname = targetUrl.pathname.replace(/\/+$/, '');
546
+ const modelsIndex = pathname.indexOf('/models');
547
+ if (modelsIndex >= 0) {
548
+ pathname = pathname.slice(0, modelsIndex);
549
+ }
550
+
551
+ let apiBasePath;
552
+ if (!pathname || pathname === '/') {
553
+ apiBasePath = '/v1beta';
554
+ } else if (
555
+ pathname.endsWith('/v1beta') ||
556
+ pathname.endsWith('/v1')
557
+ ) {
558
+ apiBasePath = pathname;
559
+ } else {
560
+ apiBasePath = `${pathname}/v1beta`;
561
+ }
562
+
563
+ const method = stream ? 'streamGenerateContent' : 'generateContent';
564
+ targetUrl.pathname = `${apiBasePath}/models/${encodeURIComponent(modelName)}:${method}`;
565
+ if (apiKey) {
566
+ targetUrl.searchParams.set('key', apiKey);
567
+ }
568
+ if (stream) {
569
+ targetUrl.searchParams.set('alt', 'sse');
570
+ }
571
+
572
+ return targetUrl.toString();
573
+ }
574
+
575
+ /**
576
+ * Build the Gemini target URL from a base URL, model, and api key.
577
+ *
578
+ * @param {string} baseUrl
579
+ * @param {string} model
580
+ * @param {string} apiKey
581
+ * @param {object} [options={}] - { stream, useCli }
582
+ * @returns {string} target URL or empty string for invalid
583
+ */
584
+ function buildGeminiTargetUrl(baseUrl = '', model = '', apiKey = '', options = {}) {
585
+ if (options.useCli) {
586
+ const trimmed = String(baseUrl || '').trim();
587
+ let targetUrl;
588
+ try {
589
+ targetUrl = new URL(
590
+ trimmed || 'https://cloudcode-pa.googleapis.com'
591
+ );
592
+ } catch {
593
+ // Non-empty but malformed base URL returns empty string
594
+ if (trimmed) return '';
595
+ targetUrl = new URL('https://cloudcode-pa.googleapis.com');
596
+ }
597
+
598
+ // Malformed URLs that parse without hostname (e.g. 'not-a-url:::')
599
+ if (trimmed && !targetUrl.hostname) return '';
600
+
601
+ targetUrl.pathname = buildGeminiCliTargetPath(
602
+ targetUrl,
603
+ !!options.stream
604
+ );
605
+ if (options.stream) {
606
+ targetUrl.searchParams.set('alt', 'sse');
607
+ }
608
+ return targetUrl.toString();
609
+ }
610
+
611
+ return buildGeminiNativeTargetUrl(baseUrl, model, apiKey, options);
612
+ }
613
+
614
+ // ---------------------------------------------------------------------------
615
+ // Public API
616
+ // ---------------------------------------------------------------------------
617
+
618
+ /**
619
+ * Create a Gemini API request from an OpenCode-compatible payload.
620
+ *
621
+ * @param {string} pathname - request path (e.g. /v1/chat/completions, /v1/responses)
622
+ * @param {object} [payload={}] - OpenCode request body
623
+ * @param {object} [options={}] -
624
+ * apiKey, fallbackModel, stream, useCli, networkHeaders, headers, providerConfig
625
+ * @returns {{ body: object, headers: object, model: string, useCli: boolean }}
626
+ */
627
+ function createGeminiRequest(pathname, payload = {}, options = {}) {
628
+ const {
629
+ apiKey = '',
630
+ fallbackModel = '',
631
+ stream = false,
632
+ useCli = false,
633
+ networkHeaders = true
634
+ } = options;
635
+
636
+ const model =
637
+ payload.model || fallbackModel || '';
638
+
639
+ const normalized = normalizeOpenCodeMessages(pathname, payload);
640
+ const maxTokens = Number(payload.max_output_tokens ?? payload.max_tokens);
641
+ const stopSequencesVal = normalizeStopSequences(payload.stop);
642
+ const tools = normalizeOpenAiToolsToGemini(payload.tools || []);
643
+ const toolConfig = normalizeToolChoiceToGemini(payload.tool_choice);
644
+ const thinkingConfig = normalizeReasoningEffortToGemini(
645
+ payload.reasoning_effort
646
+ );
647
+ const candidateCount = Number(payload.n);
648
+ const responseModalities = normalizeGeminiResponseModalities(
649
+ payload.modalities
650
+ );
651
+ const imageConfig =
652
+ payload.image_config &&
653
+ typeof payload.image_config === 'object' &&
654
+ !Array.isArray(payload.image_config)
655
+ ? payload.image_config
656
+ : null;
657
+
658
+ const innerBody = {
659
+ contents: buildGeminiContents(normalized.messages)
660
+ };
661
+
662
+ if (
663
+ normalized.systemBlocks &&
664
+ normalized.systemBlocks.length > 0
665
+ ) {
666
+ innerBody.systemInstruction = {
667
+ parts: normalized.systemBlocks
668
+ .map(block => ({ text: block.text || '' }))
669
+ .filter(p => p.text)
670
+ };
671
+ }
672
+
673
+ const generationConfig = {};
674
+ if (Number.isFinite(maxTokens) && maxTokens > 0) {
675
+ generationConfig.maxOutputTokens = Math.round(maxTokens);
676
+ }
677
+ if (Number.isFinite(Number(payload.temperature))) {
678
+ generationConfig.temperature = Number(payload.temperature);
679
+ }
680
+ if (Number.isFinite(Number(payload.top_p))) {
681
+ generationConfig.topP = Number(payload.top_p);
682
+ }
683
+ if (Number.isFinite(Number(payload.top_k))) {
684
+ generationConfig.topK = Number(payload.top_k);
685
+ }
686
+ if (stopSequencesVal) {
687
+ generationConfig.stopSequences = stopSequencesVal;
688
+ }
689
+ if (thinkingConfig) {
690
+ generationConfig.thinkingConfig = thinkingConfig;
691
+ }
692
+ if (Number.isFinite(candidateCount) && candidateCount > 1) {
693
+ generationConfig.candidateCount = Math.round(candidateCount);
694
+ }
695
+ if (responseModalities) {
696
+ generationConfig.responseModalities = responseModalities;
697
+ }
698
+ if (imageConfig) {
699
+ const mappedImageConfig = {};
700
+ if (
701
+ typeof imageConfig.aspect_ratio === 'string' &&
702
+ imageConfig.aspect_ratio.trim()
703
+ ) {
704
+ mappedImageConfig.aspectRatio = imageConfig.aspect_ratio.trim();
705
+ }
706
+ if (
707
+ typeof imageConfig.image_size === 'string' &&
708
+ imageConfig.image_size.trim()
709
+ ) {
710
+ mappedImageConfig.imageSize = imageConfig.image_size.trim();
711
+ }
712
+ if (Object.keys(mappedImageConfig).length > 0) {
713
+ generationConfig.imageConfig = mappedImageConfig;
714
+ }
715
+ }
716
+ if (Object.keys(generationConfig).length > 0) {
717
+ innerBody.generationConfig = generationConfig;
718
+ }
719
+
720
+ if (tools.length > 0) {
721
+ innerBody.tools = tools;
722
+ }
723
+ if (toolConfig) {
724
+ innerBody.toolConfig = toolConfig;
725
+ }
726
+
727
+ const body = useCli
728
+ ? {
729
+ project: '',
730
+ model,
731
+ request: innerBody
732
+ }
733
+ : innerBody;
734
+
735
+ const headerOptions = {
736
+ headers: options.headers,
737
+ providerConfig: options.providerConfig
738
+ };
739
+
740
+ const headers = useCli
741
+ ? buildGeminiCliHeaders(apiKey, stream, networkHeaders, headerOptions)
742
+ : buildGeminiPublicHeaders(apiKey, stream, networkHeaders, headerOptions);
743
+
744
+ return {
745
+ body,
746
+ headers,
747
+ model,
748
+ useCli
749
+ };
750
+ }
751
+
752
+ // ---------------------------------------------------------------------------
753
+ // Exports
754
+ // ---------------------------------------------------------------------------
755
+ module.exports = {
756
+ createGeminiRequest,
757
+ buildGeminiTargetUrl,
758
+ shouldUseGeminiCliFormat
759
+ };