@adversity/coding-tool-x 3.1.0 → 3.1.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 +39 -18
- package/README.md +8 -8
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-DvcbKKdS.js +1 -0
- package/dist/web/assets/Home-BJKPCBuk.css +1 -0
- package/dist/web/assets/Home-Cw-F_Wnu.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/PluginManager-jy_4GVxI.js +1 -0
- package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
- package/dist/web/assets/ProjectList-Df1-NcNr.js +1 -0
- package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
- package/dist/web/assets/SessionList-UWcZtC2r.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/SkillManager-IRdseMKB.js +1 -0
- package/dist/web/assets/Terminal-BasTyDut.js +1 -0
- package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/WorkspaceManager-D-D2kK1V.js +1 -0
- package/dist/web/assets/icons-kcfLIMBB.js +1 -0
- package/dist/web/assets/index-CoB3zF0K.css +1 -0
- package/dist/web/assets/index-CryrSLv8.js +2 -0
- package/dist/web/assets/markdown-BfC0goYb.css +10 -0
- package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
- package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
- package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
- package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
- package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
- package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
- package/dist/web/index.html +8 -6
- package/package.json +4 -2
- package/src/commands/channels.js +48 -1
- package/src/commands/cli-type.js +4 -2
- package/src/commands/daemon.js +81 -12
- package/src/commands/doctor.js +10 -9
- package/src/commands/list.js +1 -1
- package/src/commands/logs.js +6 -4
- package/src/commands/port-config.js +24 -4
- package/src/commands/proxy-control.js +12 -6
- package/src/commands/search.js +1 -1
- package/src/commands/security.js +3 -2
- package/src/commands/stats.js +226 -52
- package/src/commands/switch.js +1 -1
- package/src/commands/toggle-proxy.js +31 -6
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +1 -1
- package/src/config/default.js +41 -2
- package/src/config/loader.js +74 -8
- package/src/config/model-metadata.js +415 -0
- package/src/config/model-pricing.js +23 -93
- package/src/config/paths.js +105 -33
- package/src/index.js +64 -3
- package/src/plugins/constants.js +3 -2
- package/src/plugins/plugin-api.js +1 -1
- package/src/reset-config.js +4 -2
- package/src/server/api/agents.js +57 -14
- package/src/server/api/channels.js +112 -33
- package/src/server/api/codex-channels.js +111 -18
- package/src/server/api/codex-proxy.js +14 -8
- package/src/server/api/commands.js +71 -18
- package/src/server/api/config-export.js +0 -6
- package/src/server/api/config-registry.js +11 -3
- package/src/server/api/config.js +376 -5
- package/src/server/api/convert.js +133 -0
- package/src/server/api/dashboard.js +22 -6
- package/src/server/api/gemini-channels.js +107 -18
- package/src/server/api/gemini-proxy.js +14 -8
- package/src/server/api/gemini-sessions.js +1 -1
- package/src/server/api/health-check.js +4 -3
- package/src/server/api/mcp.js +3 -3
- package/src/server/api/opencode-channels.js +497 -0
- package/src/server/api/opencode-projects.js +99 -0
- package/src/server/api/opencode-proxy.js +207 -0
- package/src/server/api/opencode-sessions.js +345 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +66 -19
- package/src/server/api/prompts.js +2 -2
- package/src/server/api/proxy.js +7 -4
- package/src/server/api/sessions.js +3 -0
- package/src/server/api/settings.js +111 -0
- package/src/server/api/skills.js +69 -18
- package/src/server/api/workspaces.js +78 -6
- package/src/server/codex-proxy-server.js +36 -22
- package/src/server/dev-server.js +1 -1
- package/src/server/gemini-proxy-server.js +21 -7
- package/src/server/index.js +174 -58
- package/src/server/opencode-proxy-server.js +5486 -0
- package/src/server/proxy-server.js +33 -22
- package/src/server/services/agents-service.js +61 -24
- package/src/server/services/channel-scheduler.js +9 -5
- package/src/server/services/channels.js +64 -37
- package/src/server/services/codex-channels.js +56 -43
- package/src/server/services/codex-sessions.js +105 -6
- package/src/server/services/codex-settings-manager.js +271 -49
- package/src/server/services/codex-statistics-service.js +2 -2
- package/src/server/services/commands-service.js +84 -25
- package/src/server/services/config-export-service.js +7 -45
- package/src/server/services/config-registry-service.js +63 -17
- package/src/server/services/config-sync-manager.js +160 -7
- package/src/server/services/config-templates-service.js +204 -51
- package/src/server/services/env-checker.js +50 -13
- package/src/server/services/env-manager.js +155 -19
- package/src/server/services/favorites.js +5 -3
- package/src/server/services/gemini-channels.js +33 -44
- package/src/server/services/gemini-statistics-service.js +2 -2
- package/src/server/services/mcp-service.js +350 -9
- package/src/server/services/model-detector.js +707 -221
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +208 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +931 -0
- package/src/server/services/opencode-settings-manager.js +478 -0
- package/src/server/services/opencode-statistics-service.js +255 -0
- package/src/server/services/plugins-service.js +479 -22
- package/src/server/services/prompts-service.js +53 -11
- package/src/server/services/proxy-runtime.js +1 -1
- package/src/server/services/repo-scanner-base.js +1 -1
- package/src/server/services/response-decoder.js +21 -0
- package/src/server/services/security-config.js +1 -1
- package/src/server/services/session-cache.js +1 -1
- package/src/server/services/skill-service.js +300 -46
- package/src/server/services/speed-test.js +464 -186
- package/src/server/services/statistics-service.js +2 -2
- package/src/server/services/terminal-commands.js +10 -3
- package/src/server/services/terminal-config.js +1 -1
- package/src/server/services/ui-config.js +1 -1
- package/src/server/services/workspace-service.js +57 -100
- package/src/server/websocket-server.js +156 -8
- package/src/ui/menu.js +49 -40
- package/src/utils/port-helper.js +22 -8
- package/src/utils/session.js +5 -4
- package/dist/web/assets/icons-CO_2OFES.js +0 -1
- package/dist/web/assets/index-DI8QOi-E.js +0 -14
- package/dist/web/assets/index-uLHGdeZh.css +0 -41
- package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
- package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
- package/src/server/api/oauth.js +0 -294
- package/src/server/api/permissions.js +0 -385
- package/src/server/config/oauth-providers.js +0 -68
- package/src/server/services/oauth-callback-server.js +0 -284
- package/src/server/services/oauth-service.js +0 -378
- package/src/server/services/oauth-token-storage.js +0 -135
- package/src/server/services/permission-templates-service.js +0 -308
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Gateway Converter
|
|
3
|
+
*
|
|
4
|
+
* Convert Claude Code / Codex / Gemini request payloads into
|
|
5
|
+
* OpenCode-compatible OpenAI wire format.
|
|
6
|
+
*
|
|
7
|
+
* Default target wire format:
|
|
8
|
+
* - OpenAI Responses API (/v1/responses)
|
|
9
|
+
*
|
|
10
|
+
* Optional target wire format:
|
|
11
|
+
* - OpenAI Chat Completions API (/v1/chat/completions)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const SUPPORTED_SOURCE_TYPES = ['claude', 'codex', 'gemini'];
|
|
15
|
+
const SUPPORTED_TARGET_APIS = ['responses', 'chat.completions'];
|
|
16
|
+
|
|
17
|
+
function normalizeSourceType(sourceType) {
|
|
18
|
+
const value = String(sourceType || '').trim().toLowerCase();
|
|
19
|
+
if (value === 'claude' || value === 'claude-code' || value === 'claude_code') return 'claude';
|
|
20
|
+
if (value === 'codex' || value === 'codex-cli' || value === 'codex_cli') return 'codex';
|
|
21
|
+
if (value === 'gemini' || value === 'gemini-cli' || value === 'gemini_cli') return 'gemini';
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeTargetApi(targetApi) {
|
|
26
|
+
if (targetApi === undefined || targetApi === null || targetApi === '') {
|
|
27
|
+
return 'responses';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const value = String(targetApi).trim().toLowerCase();
|
|
31
|
+
|
|
32
|
+
if (value === 'responses' || value === 'response') {
|
|
33
|
+
return 'responses';
|
|
34
|
+
}
|
|
35
|
+
if (
|
|
36
|
+
value === 'chat' ||
|
|
37
|
+
value === 'chat-completions' ||
|
|
38
|
+
value === 'chat_completions' ||
|
|
39
|
+
value === 'chat/completions' ||
|
|
40
|
+
value === 'chat.completions'
|
|
41
|
+
) {
|
|
42
|
+
return 'chat.completions';
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function safeClone(value) {
|
|
48
|
+
if (value === undefined) return undefined;
|
|
49
|
+
return JSON.parse(JSON.stringify(value));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isFiniteNumber(value) {
|
|
53
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function firstFiniteNumber(...values) {
|
|
57
|
+
for (const value of values) {
|
|
58
|
+
if (isFiniteNumber(value)) return value;
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function appendTextFragments(value, fragments, state) {
|
|
64
|
+
if (value === null || value === undefined) return;
|
|
65
|
+
|
|
66
|
+
if (typeof value === 'string') {
|
|
67
|
+
if (value.trim()) fragments.push(value);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
72
|
+
fragments.push(String(value));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
value.forEach(item => appendTextFragments(item, fragments, state));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (typeof value !== 'object') return;
|
|
82
|
+
|
|
83
|
+
if (typeof value.text === 'string') {
|
|
84
|
+
appendTextFragments(value.text, fragments, state);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (typeof value.input_text === 'string') {
|
|
88
|
+
appendTextFragments(value.input_text, fragments, state);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (typeof value.output_text === 'string') {
|
|
92
|
+
appendTextFragments(value.output_text, fragments, state);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (typeof value.content === 'string' || Array.isArray(value.content)) {
|
|
96
|
+
appendTextFragments(value.content, fragments, state);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (Array.isArray(value.parts)) {
|
|
100
|
+
appendTextFragments(value.parts, fragments, state);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (value.type && value.type !== 'text' && value.type !== 'input_text' && value.type !== 'output_text') {
|
|
105
|
+
state.nonTextItems += 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function extractText(value) {
|
|
110
|
+
const fragments = [];
|
|
111
|
+
const state = { nonTextItems: 0 };
|
|
112
|
+
appendTextFragments(value, fragments, state);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
text: fragments.join('\n').trim(),
|
|
116
|
+
nonTextItems: state.nonTextItems
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buildResponseMessage(role, text) {
|
|
121
|
+
return {
|
|
122
|
+
type: 'message',
|
|
123
|
+
role,
|
|
124
|
+
content: [
|
|
125
|
+
{
|
|
126
|
+
type: 'input_text',
|
|
127
|
+
text
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function coerceRole(role) {
|
|
134
|
+
const value = String(role || '').trim().toLowerCase();
|
|
135
|
+
if (value === 'assistant' || value === 'model') return 'assistant';
|
|
136
|
+
if (value === 'system') return 'system';
|
|
137
|
+
if (value === 'tool') return 'tool';
|
|
138
|
+
return 'user';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeOpenAiToolsToFunctions(tools = [], warnings = []) {
|
|
142
|
+
if (!Array.isArray(tools)) return [];
|
|
143
|
+
|
|
144
|
+
const mapped = [];
|
|
145
|
+
tools.forEach((tool, index) => {
|
|
146
|
+
if (!tool || typeof tool !== 'object') return;
|
|
147
|
+
|
|
148
|
+
if (tool.type === 'function' && tool.function && typeof tool.function === 'object') {
|
|
149
|
+
const fn = tool.function;
|
|
150
|
+
if (!fn.name) {
|
|
151
|
+
warnings.push(`Tool at index ${index} missing function.name and was ignored.`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
mapped.push({
|
|
155
|
+
type: 'function',
|
|
156
|
+
name: fn.name,
|
|
157
|
+
description: fn.description || '',
|
|
158
|
+
parameters: fn.parameters || { type: 'object', properties: {} }
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (tool.type === 'function' && tool.name) {
|
|
164
|
+
mapped.push({
|
|
165
|
+
type: 'function',
|
|
166
|
+
name: tool.name,
|
|
167
|
+
description: tool.description || '',
|
|
168
|
+
parameters: tool.parameters || { type: 'object', properties: {} }
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return mapped;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function normalizeClaudeTools(tools = [], warnings = []) {
|
|
177
|
+
if (!Array.isArray(tools)) return [];
|
|
178
|
+
|
|
179
|
+
const mapped = [];
|
|
180
|
+
tools.forEach((tool, index) => {
|
|
181
|
+
if (!tool || typeof tool !== 'object') return;
|
|
182
|
+
if (!tool.name) {
|
|
183
|
+
warnings.push(`Claude tool at index ${index} missing name and was ignored.`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
mapped.push({
|
|
187
|
+
type: 'function',
|
|
188
|
+
name: tool.name,
|
|
189
|
+
description: tool.description || '',
|
|
190
|
+
parameters: tool.input_schema || tool.parameters || { type: 'object', properties: {} }
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return mapped;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizeGeminiTools(tools = [], warnings = []) {
|
|
198
|
+
if (!Array.isArray(tools)) return [];
|
|
199
|
+
|
|
200
|
+
const mapped = [];
|
|
201
|
+
tools.forEach((tool, toolIndex) => {
|
|
202
|
+
if (!tool || typeof tool !== 'object') return;
|
|
203
|
+
|
|
204
|
+
if (Array.isArray(tool.functionDeclarations)) {
|
|
205
|
+
tool.functionDeclarations.forEach((decl, declIndex) => {
|
|
206
|
+
if (!decl || typeof decl !== 'object') return;
|
|
207
|
+
if (!decl.name) {
|
|
208
|
+
warnings.push(`Gemini function declaration at ${toolIndex}:${declIndex} missing name and was ignored.`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
mapped.push({
|
|
212
|
+
type: 'function',
|
|
213
|
+
name: decl.name,
|
|
214
|
+
description: decl.description || '',
|
|
215
|
+
parameters: decl.parameters || { type: 'object', properties: {} }
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (tool.type === 'function' && tool.name) {
|
|
222
|
+
mapped.push({
|
|
223
|
+
type: 'function',
|
|
224
|
+
name: tool.name,
|
|
225
|
+
description: tool.description || '',
|
|
226
|
+
parameters: tool.parameters || { type: 'object', properties: {} }
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return mapped;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function normalizeClaudePayload(payload, options, warnings) {
|
|
235
|
+
const systemText = extractText(payload.system).text;
|
|
236
|
+
const messages = [];
|
|
237
|
+
|
|
238
|
+
if (Array.isArray(payload.messages)) {
|
|
239
|
+
payload.messages.forEach((message, index) => {
|
|
240
|
+
if (!message || typeof message !== 'object') return;
|
|
241
|
+
const role = coerceRole(message.role);
|
|
242
|
+
const { text, nonTextItems } = extractText(message.content);
|
|
243
|
+
|
|
244
|
+
if (nonTextItems > 0) {
|
|
245
|
+
warnings.push(`Claude message ${index} contains non-text content; only text was preserved.`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!text) return;
|
|
249
|
+
|
|
250
|
+
if (role === 'system') {
|
|
251
|
+
warnings.push(`Claude message ${index} has role=system; merged into instructions.`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (role === 'system') return;
|
|
255
|
+
messages.push({ role: role === 'tool' ? 'assistant' : role, text });
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
model: payload.model || options.defaultModel || 'gpt-4o-mini',
|
|
261
|
+
systemText,
|
|
262
|
+
messages,
|
|
263
|
+
tools: normalizeClaudeTools(payload.tools, warnings),
|
|
264
|
+
toolChoice: payload.tool_choice,
|
|
265
|
+
maxOutputTokens: firstFiniteNumber(options.maxOutputTokens, payload.max_output_tokens, payload.max_tokens),
|
|
266
|
+
stream: typeof options.stream === 'boolean' ? options.stream : payload.stream,
|
|
267
|
+
store: typeof options.store === 'boolean' ? options.store : payload.store,
|
|
268
|
+
temperature: firstFiniteNumber(payload.temperature),
|
|
269
|
+
topP: firstFiniteNumber(payload.top_p)
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function normalizeOpenAiChatLikePayload(payload, options, warnings) {
|
|
274
|
+
const messages = [];
|
|
275
|
+
const systemParts = [];
|
|
276
|
+
|
|
277
|
+
if (Array.isArray(payload.messages)) {
|
|
278
|
+
payload.messages.forEach((message, index) => {
|
|
279
|
+
if (!message || typeof message !== 'object') return;
|
|
280
|
+
const role = coerceRole(message.role);
|
|
281
|
+
const { text, nonTextItems } = extractText(message.content);
|
|
282
|
+
|
|
283
|
+
if (nonTextItems > 0) {
|
|
284
|
+
warnings.push(`Message ${index} contains non-text content; only text was preserved.`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!text) return;
|
|
288
|
+
|
|
289
|
+
if (role === 'system') {
|
|
290
|
+
systemParts.push(text);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (role === 'tool') {
|
|
295
|
+
warnings.push(`Message ${index} has role=tool and was flattened to assistant text.`);
|
|
296
|
+
messages.push({ role: 'assistant', text });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
messages.push({ role, text });
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (typeof payload.input === 'string') {
|
|
305
|
+
messages.push({ role: 'user', text: payload.input });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
model: payload.model || options.defaultModel || 'gpt-4o-mini',
|
|
310
|
+
systemText: systemParts.join('\n\n').trim(),
|
|
311
|
+
messages,
|
|
312
|
+
tools: normalizeOpenAiToolsToFunctions(payload.tools, warnings),
|
|
313
|
+
toolChoice: payload.tool_choice,
|
|
314
|
+
maxOutputTokens: firstFiniteNumber(options.maxOutputTokens, payload.max_output_tokens, payload.max_tokens),
|
|
315
|
+
stream: typeof options.stream === 'boolean' ? options.stream : payload.stream,
|
|
316
|
+
store: typeof options.store === 'boolean' ? options.store : payload.store,
|
|
317
|
+
temperature: firstFiniteNumber(payload.temperature),
|
|
318
|
+
topP: firstFiniteNumber(payload.top_p),
|
|
319
|
+
stop: payload.stop
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function extractMessagesFromResponsesInput(input, warnings) {
|
|
324
|
+
if (!Array.isArray(input)) return [];
|
|
325
|
+
|
|
326
|
+
const messages = [];
|
|
327
|
+
input.forEach((item, index) => {
|
|
328
|
+
if (!item || typeof item !== 'object') return;
|
|
329
|
+
|
|
330
|
+
if (item.type === 'message') {
|
|
331
|
+
const role = coerceRole(item.role);
|
|
332
|
+
const { text, nonTextItems } = extractText(item.content);
|
|
333
|
+
if (nonTextItems > 0) {
|
|
334
|
+
warnings.push(`Responses input item ${index} contains non-text blocks; only text was preserved.`);
|
|
335
|
+
}
|
|
336
|
+
if (!text) return;
|
|
337
|
+
messages.push({ role: role === 'tool' ? 'assistant' : role, text });
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Compatibility path: some callers pass chat-like message items in input.
|
|
342
|
+
if (item.role && item.content !== undefined) {
|
|
343
|
+
const role = coerceRole(item.role);
|
|
344
|
+
const { text } = extractText(item.content);
|
|
345
|
+
if (!text) return;
|
|
346
|
+
messages.push({ role: role === 'tool' ? 'assistant' : role, text });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (item.type === 'function_call' || item.type === 'function_call_output') {
|
|
351
|
+
warnings.push(`Responses input item ${index} is ${item.type}; preserved for /v1/responses only.`);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
return messages;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function normalizeCodexPayload(payload, options, warnings) {
|
|
360
|
+
const isResponsesShape =
|
|
361
|
+
Array.isArray(payload.input) ||
|
|
362
|
+
typeof payload.input === 'string' ||
|
|
363
|
+
typeof payload.instructions === 'string';
|
|
364
|
+
|
|
365
|
+
if (isResponsesShape) {
|
|
366
|
+
const request = safeClone(payload);
|
|
367
|
+
|
|
368
|
+
if (typeof request.input === 'string') {
|
|
369
|
+
request.input = [buildResponseMessage('user', request.input)];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (!request.model) {
|
|
373
|
+
request.model = options.defaultModel || 'gpt-4o-mini';
|
|
374
|
+
}
|
|
375
|
+
if (typeof options.stream === 'boolean') {
|
|
376
|
+
request.stream = options.stream;
|
|
377
|
+
} else if (typeof request.stream !== 'boolean') {
|
|
378
|
+
request.stream = false;
|
|
379
|
+
}
|
|
380
|
+
if (typeof options.store === 'boolean') {
|
|
381
|
+
request.store = options.store;
|
|
382
|
+
} else if (typeof request.store !== 'boolean') {
|
|
383
|
+
request.store = false;
|
|
384
|
+
}
|
|
385
|
+
if (isFiniteNumber(options.maxOutputTokens)) {
|
|
386
|
+
request.max_output_tokens = options.maxOutputTokens;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
prebuiltResponses: request,
|
|
391
|
+
model: request.model,
|
|
392
|
+
systemText: typeof request.instructions === 'string' ? request.instructions : '',
|
|
393
|
+
messages: extractMessagesFromResponsesInput(request.input, warnings),
|
|
394
|
+
tools: normalizeOpenAiToolsToFunctions(request.tools, warnings),
|
|
395
|
+
toolChoice: request.tool_choice,
|
|
396
|
+
maxOutputTokens: firstFiniteNumber(request.max_output_tokens),
|
|
397
|
+
stream: request.stream,
|
|
398
|
+
store: request.store,
|
|
399
|
+
temperature: firstFiniteNumber(request.temperature),
|
|
400
|
+
topP: firstFiniteNumber(request.top_p),
|
|
401
|
+
stop: request.stop
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return normalizeOpenAiChatLikePayload(payload, options, warnings);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function normalizeGeminiPayload(payload, options, warnings) {
|
|
409
|
+
// Some gateways already expose Gemini as OpenAI-compatible chat format.
|
|
410
|
+
if (Array.isArray(payload.messages)) {
|
|
411
|
+
return normalizeOpenAiChatLikePayload(payload, options, warnings);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const messages = [];
|
|
415
|
+
const systemText = extractText(
|
|
416
|
+
payload.system_instruction ||
|
|
417
|
+
payload.systemInstruction ||
|
|
418
|
+
payload.system
|
|
419
|
+
).text;
|
|
420
|
+
|
|
421
|
+
if (Array.isArray(payload.contents)) {
|
|
422
|
+
payload.contents.forEach((content, index) => {
|
|
423
|
+
if (!content || typeof content !== 'object') return;
|
|
424
|
+
const role = coerceRole(content.role);
|
|
425
|
+
const { text, nonTextItems } = extractText(content.parts || content.content);
|
|
426
|
+
|
|
427
|
+
if (nonTextItems > 0) {
|
|
428
|
+
warnings.push(`Gemini content ${index} contains non-text parts; only text was preserved.`);
|
|
429
|
+
}
|
|
430
|
+
if (!text) return;
|
|
431
|
+
|
|
432
|
+
if (role === 'system') return;
|
|
433
|
+
messages.push({ role: role === 'tool' ? 'assistant' : role, text });
|
|
434
|
+
});
|
|
435
|
+
} else if (typeof payload.prompt === 'string' && payload.prompt.trim()) {
|
|
436
|
+
messages.push({ role: 'user', text: payload.prompt.trim() });
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const generationConfig = payload.generationConfig || payload.generation_config || {};
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
model: payload.model || options.defaultModel || 'gemini-2.5-pro',
|
|
443
|
+
systemText,
|
|
444
|
+
messages,
|
|
445
|
+
tools: normalizeGeminiTools(payload.tools, warnings),
|
|
446
|
+
maxOutputTokens: firstFiniteNumber(
|
|
447
|
+
options.maxOutputTokens,
|
|
448
|
+
payload.max_output_tokens,
|
|
449
|
+
payload.max_tokens,
|
|
450
|
+
generationConfig.maxOutputTokens
|
|
451
|
+
),
|
|
452
|
+
stream: typeof options.stream === 'boolean' ? options.stream : payload.stream,
|
|
453
|
+
store: typeof options.store === 'boolean' ? options.store : payload.store,
|
|
454
|
+
temperature: firstFiniteNumber(payload.temperature, generationConfig.temperature),
|
|
455
|
+
topP: firstFiniteNumber(payload.top_p, generationConfig.topP),
|
|
456
|
+
topK: firstFiniteNumber(payload.top_k, generationConfig.topK),
|
|
457
|
+
stop: payload.stop || generationConfig.stopSequences
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function buildResponsesRequest(normalized, options, warnings) {
|
|
462
|
+
if (normalized.prebuiltResponses) {
|
|
463
|
+
return safeClone(normalized.prebuiltResponses);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const input = normalized.messages.map(msg => buildResponseMessage(msg.role, msg.text));
|
|
467
|
+
if (input.length === 0) {
|
|
468
|
+
const fallbackPrompt = typeof options.fallbackUserPrompt === 'string'
|
|
469
|
+
? options.fallbackUserPrompt
|
|
470
|
+
: 'Hello';
|
|
471
|
+
warnings.push('No message content detected, inserted fallback user message.');
|
|
472
|
+
input.push(buildResponseMessage('user', fallbackPrompt));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const request = {
|
|
476
|
+
model: normalized.model,
|
|
477
|
+
input,
|
|
478
|
+
stream: typeof normalized.stream === 'boolean' ? normalized.stream : false,
|
|
479
|
+
store: typeof normalized.store === 'boolean' ? normalized.store : false
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
if (normalized.systemText) request.instructions = normalized.systemText;
|
|
483
|
+
if (isFiniteNumber(normalized.maxOutputTokens)) request.max_output_tokens = normalized.maxOutputTokens;
|
|
484
|
+
if (Array.isArray(normalized.tools) && normalized.tools.length > 0) request.tools = normalized.tools;
|
|
485
|
+
if (normalized.toolChoice !== undefined) request.tool_choice = normalized.toolChoice;
|
|
486
|
+
if (isFiniteNumber(normalized.temperature)) request.temperature = normalized.temperature;
|
|
487
|
+
if (isFiniteNumber(normalized.topP)) request.top_p = normalized.topP;
|
|
488
|
+
if (isFiniteNumber(normalized.topK)) request.top_k = normalized.topK;
|
|
489
|
+
if (normalized.stop !== undefined) request.stop = normalized.stop;
|
|
490
|
+
|
|
491
|
+
return request;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function toChatTool(tool) {
|
|
495
|
+
if (!tool || typeof tool !== 'object') return null;
|
|
496
|
+
|
|
497
|
+
if (tool.type === 'function' && tool.function && typeof tool.function === 'object') {
|
|
498
|
+
return tool;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (tool.type === 'function' && tool.name) {
|
|
502
|
+
return {
|
|
503
|
+
type: 'function',
|
|
504
|
+
function: {
|
|
505
|
+
name: tool.name,
|
|
506
|
+
description: tool.description || '',
|
|
507
|
+
parameters: tool.parameters || { type: 'object', properties: {} }
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function buildChatCompletionsRequest(normalized, options, warnings) {
|
|
516
|
+
const messages = [];
|
|
517
|
+
if (normalized.systemText) {
|
|
518
|
+
messages.push({
|
|
519
|
+
role: 'system',
|
|
520
|
+
content: normalized.systemText
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
normalized.messages.forEach(msg => {
|
|
525
|
+
const role = msg.role === 'assistant' ? 'assistant' : 'user';
|
|
526
|
+
messages.push({
|
|
527
|
+
role,
|
|
528
|
+
content: msg.text
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
if (messages.length === 0) {
|
|
533
|
+
const fallbackPrompt = typeof options.fallbackUserPrompt === 'string'
|
|
534
|
+
? options.fallbackUserPrompt
|
|
535
|
+
: 'Hello';
|
|
536
|
+
warnings.push('No message content detected, inserted fallback user message.');
|
|
537
|
+
messages.push({ role: 'user', content: fallbackPrompt });
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const request = {
|
|
541
|
+
model: normalized.model,
|
|
542
|
+
messages,
|
|
543
|
+
stream: typeof normalized.stream === 'boolean' ? normalized.stream : false
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
if (isFiniteNumber(normalized.maxOutputTokens)) request.max_tokens = normalized.maxOutputTokens;
|
|
547
|
+
if (isFiniteNumber(normalized.temperature)) request.temperature = normalized.temperature;
|
|
548
|
+
if (isFiniteNumber(normalized.topP)) request.top_p = normalized.topP;
|
|
549
|
+
if (normalized.stop !== undefined) request.stop = normalized.stop;
|
|
550
|
+
if (normalized.toolChoice !== undefined) request.tool_choice = normalized.toolChoice;
|
|
551
|
+
|
|
552
|
+
if (Array.isArray(normalized.tools) && normalized.tools.length > 0) {
|
|
553
|
+
const chatTools = normalized.tools.map(toChatTool).filter(Boolean);
|
|
554
|
+
if (chatTools.length > 0) request.tools = chatTools;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return request;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function convertNormalizedSourceToOpenCodePayload(normalizedSourceType, payload, options = {}) {
|
|
561
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
562
|
+
throw new Error('payload must be a JSON object');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const targetApi = normalizeTargetApi(options.targetApi);
|
|
566
|
+
if (!SUPPORTED_TARGET_APIS.includes(targetApi)) {
|
|
567
|
+
throw new Error(`Unsupported targetApi: ${options.targetApi}`);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const warnings = [];
|
|
571
|
+
let normalized;
|
|
572
|
+
|
|
573
|
+
if (normalizedSourceType === 'claude') {
|
|
574
|
+
normalized = normalizeClaudePayload(payload, options, warnings);
|
|
575
|
+
} else if (normalizedSourceType === 'codex') {
|
|
576
|
+
normalized = normalizeCodexPayload(payload, options, warnings);
|
|
577
|
+
} else {
|
|
578
|
+
normalized = normalizeGeminiPayload(payload, options, warnings);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (!normalized.model) {
|
|
582
|
+
normalized.model = options.defaultModel || 'gpt-4o-mini';
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const requestBody = targetApi === 'responses'
|
|
586
|
+
? buildResponsesRequest(normalized, options, warnings)
|
|
587
|
+
: buildChatCompletionsRequest(normalized, options, warnings);
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
sourceType: normalizedSourceType,
|
|
591
|
+
target: 'opencode',
|
|
592
|
+
targetApi,
|
|
593
|
+
endpoint: targetApi === 'responses' ? '/v1/responses' : '/v1/chat/completions',
|
|
594
|
+
requestBody,
|
|
595
|
+
warnings,
|
|
596
|
+
meta: {
|
|
597
|
+
model: requestBody.model,
|
|
598
|
+
messageCount: Array.isArray(normalized.messages) ? normalized.messages.length : 0,
|
|
599
|
+
hasSystemInstruction: Boolean(normalized.systemText)
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function convertClaudeToOpenCodePayload({ payload, options = {} }) {
|
|
605
|
+
return convertNormalizedSourceToOpenCodePayload('claude', payload, options);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function convertCodexToOpenCodePayload({ payload, options = {} }) {
|
|
609
|
+
return convertNormalizedSourceToOpenCodePayload('codex', payload, options);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function convertGeminiToOpenCodePayload({ payload, options = {} }) {
|
|
613
|
+
return convertNormalizedSourceToOpenCodePayload('gemini', payload, options);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function convertToOpenCodePayload({ sourceType, payload, options = {} }) {
|
|
617
|
+
const normalizedSourceType = normalizeSourceType(sourceType);
|
|
618
|
+
if (!SUPPORTED_SOURCE_TYPES.includes(normalizedSourceType)) {
|
|
619
|
+
throw new Error(`Unsupported sourceType: ${sourceType}`);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (normalizedSourceType === 'claude') {
|
|
623
|
+
return convertClaudeToOpenCodePayload({ payload, options });
|
|
624
|
+
}
|
|
625
|
+
if (normalizedSourceType === 'codex') {
|
|
626
|
+
return convertCodexToOpenCodePayload({ payload, options });
|
|
627
|
+
}
|
|
628
|
+
return convertGeminiToOpenCodePayload({ payload, options });
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
module.exports = {
|
|
632
|
+
SUPPORTED_SOURCE_TYPES,
|
|
633
|
+
SUPPORTED_TARGET_APIS,
|
|
634
|
+
convertToOpenCodePayload,
|
|
635
|
+
convertClaudeToOpenCodePayload,
|
|
636
|
+
convertCodexToOpenCodePayload,
|
|
637
|
+
convertGeminiToOpenCodePayload,
|
|
638
|
+
normalizeSourceType
|
|
639
|
+
};
|