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.
- package/CHANGELOG.md +2 -0
- package/README.md +4 -6
- package/dist/web/assets/{Analytics-8dRv1aYW.js → Analytics-COEUXmd4.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BQRXSICO.js → ConfigTemplates-DLLkS53H.js} +1 -1
- package/dist/web/assets/{Home-BtzWhKIa.js → Home-DCEyLzwX.js} +1 -1
- package/dist/web/assets/{PluginManager-BTa-Ygf6.js → PluginManager-Oe1ZHoMO.js} +1 -1
- package/dist/web/assets/{ProjectList-F_cB8ldA.js → ProjectList-CWjuAFof.js} +1 -1
- package/dist/web/assets/{SessionList-LOrbPQvu.js → SessionList-v3ehGg9U.js} +1 -1
- package/dist/web/assets/{SkillManager-BzI_t-yV.js → SkillManager-DUF_qP5S.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Db9RGiz7.js → WorkspaceManager-CjAWBosO.js} +1 -1
- package/dist/web/assets/index-D5hyHPj7.js +2 -0
- package/dist/web/index.html +1 -1
- package/docs/superpowers/plans/2026-08-10-codex-conversion-compatibility.md +251 -0
- package/docs/superpowers/plans/2026-08-11-windows-command-window-hiding.md +90 -0
- package/docs/superpowers/specs/2026-08-10-codex-conversion-compatibility-design.md +61 -0
- package/docs/superpowers/specs/2026-08-11-windows-command-window-hiding-design.md +44 -0
- package/package.json +2 -2
- package/src/commands/update.js +1 -1
- package/src/config/paths.js +1 -0
- package/src/server/api/codex-sessions.js +14 -14
- package/src/server/api/gemini-sessions.js +52 -14
- package/src/server/api/omp-projects.js +2 -2
- package/src/server/api/omp-sessions.js +61 -23
- package/src/server/api/sessions.js +44 -2
- package/src/server/opencode-proxy-server.js +55 -1037
- package/src/server/services/claude-wire.js +694 -0
- package/src/server/services/codex-sessions.js +41 -233
- package/src/server/services/codex-wire.js +219 -0
- package/src/server/services/dashboard-snapshot-worker.js +5 -5
- package/src/server/services/gemini-sessions.js +67 -157
- package/src/server/services/gemini-wire.js +759 -0
- package/src/server/services/model-detector.js +49 -131
- package/src/server/services/omp-channels.js +23 -4
- package/src/server/services/omp-gateway.js +29 -1
- package/src/server/services/omp-sessions.js +102 -105
- package/src/server/services/omp-settings-manager.js +27 -3
- package/src/server/services/opencode-gateway-adapters.js +22 -1147
- package/src/server/services/opencode-normalization.js +546 -0
- package/src/server/services/opencode-sessions.js +339 -546
- package/src/server/services/session-converter.js +4 -4
- package/src/server/services/session-history-adapters/claude.js +235 -0
- package/src/server/services/session-history-adapters/codex.js +158 -0
- package/src/server/services/session-history-adapters/gemini.js +292 -0
- package/src/server/services/session-history-adapters/index.js +20 -0
- package/src/server/services/session-history-adapters/omp.js +256 -0
- package/src/server/services/session-history-index.js +930 -0
- package/src/server/services/session-history-worker.js +99 -0
- package/src/server/services/session-snapshot-worker.js +7 -7
- package/src/server/services/sessions.js +167 -775
- package/src/server/services/speed-test.js +85 -303
- package/src/server/services/sqlite-connection.js +78 -0
- package/src/server/services/workspace-service.js +3 -3
- 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
|
+
};
|