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,694 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
normalizeOpenCodeMessages,
|
|
7
|
+
normalizeStopSequences
|
|
8
|
+
} = require('./opencode-normalization');
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Constants
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
const CLAUDE_CODE_USER_AGENT = 'claude-cli/2.1.59 (external, cli)';
|
|
14
|
+
const CLAUDE_MESSAGES_BETA_FLAGS = Object.freeze([
|
|
15
|
+
'claude-code-20250219',
|
|
16
|
+
'interleaved-thinking-2025-05-14',
|
|
17
|
+
'prompt-caching-scope-2026-01-05',
|
|
18
|
+
'effort-2025-11-24'
|
|
19
|
+
]);
|
|
20
|
+
const CLAUDE_ADVANCED_TOOL_USE_BETA = 'advanced-tool-use-2025-11-20';
|
|
21
|
+
const CLAUDE_COUNT_TOKENS_BETA_FLAGS = Object.freeze([
|
|
22
|
+
'claude-code-20250219',
|
|
23
|
+
'token-counting-2024-11-01'
|
|
24
|
+
]);
|
|
25
|
+
const DEFAULT_CLAUDE_CODE_SYSTEM_PROMPT =
|
|
26
|
+
"You are Claude Code, Anthropic's official CLI for Claude.";
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Header override helper
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
function normalizeHeaderName(name) {
|
|
31
|
+
return String(name || '').toLowerCase().trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Merge explicit header overrides from options.headers and
|
|
36
|
+
* options.providerConfig?.headers into a base headers map.
|
|
37
|
+
* - Merges case-insensitively; explicit value wins.
|
|
38
|
+
* - If either credential header (authorization or x-api-key) is explicit,
|
|
39
|
+
* the generated opposite credential header is removed.
|
|
40
|
+
*/
|
|
41
|
+
function applyHeaderOverrides(baseHeaders, options = {}) {
|
|
42
|
+
const merged = { ...baseHeaders };
|
|
43
|
+
|
|
44
|
+
// Collect explicit overrides from both sources, normalizing keys
|
|
45
|
+
const overrideEntries = [];
|
|
46
|
+
|
|
47
|
+
const headersSource = options.headers;
|
|
48
|
+
if (headersSource && typeof headersSource === 'object' && !Array.isArray(headersSource)) {
|
|
49
|
+
for (const [key, value] of Object.entries(headersSource)) {
|
|
50
|
+
overrideEntries.push([normalizeHeaderName(key), String(value)]);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const providerConfig = options.providerConfig;
|
|
55
|
+
const providerHeaders =
|
|
56
|
+
providerConfig &&
|
|
57
|
+
typeof providerConfig === 'object' &&
|
|
58
|
+
!Array.isArray(providerConfig)
|
|
59
|
+
? providerConfig.headers
|
|
60
|
+
: undefined;
|
|
61
|
+
if (providerHeaders && typeof providerHeaders === 'object' && !Array.isArray(providerHeaders)) {
|
|
62
|
+
for (const [key, value] of Object.entries(providerHeaders)) {
|
|
63
|
+
overrideEntries.push([normalizeHeaderName(key), String(value)]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Apply overrides to merged (case-insensitive)
|
|
68
|
+
for (const [lcKey, value] of overrideEntries) {
|
|
69
|
+
merged[lcKey] = value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// If either credential header is explicitly overridden, remove the
|
|
73
|
+
// generated opposite credential.
|
|
74
|
+
const hasExplicitAuth = overrideEntries.some(
|
|
75
|
+
([lcKey]) => lcKey === 'authorization'
|
|
76
|
+
);
|
|
77
|
+
const hasExplicitApiKey = overrideEntries.some(
|
|
78
|
+
([lcKey]) => lcKey === 'x-api-key'
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (hasExplicitAuth && merged['x-api-key'] !== undefined) {
|
|
82
|
+
delete merged['x-api-key'];
|
|
83
|
+
}
|
|
84
|
+
if (hasExplicitApiKey && merged['authorization'] !== undefined) {
|
|
85
|
+
delete merged['authorization'];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return merged;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Claude-specific helpers
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
function ensureClaudeToolNamePrefix(name) {
|
|
95
|
+
const trimmed = typeof name === 'string' ? name.trim() : '';
|
|
96
|
+
if (!trimmed) return '';
|
|
97
|
+
return trimmed.startsWith('mcp_') ? trimmed : `mcp_${trimmed}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function stripClaudeToolNamePrefix(name) {
|
|
101
|
+
const trimmed = typeof name === 'string' ? name.trim() : '';
|
|
102
|
+
if (!trimmed) return '';
|
|
103
|
+
if (!trimmed.startsWith('mcp_')) return trimmed;
|
|
104
|
+
const stripped = trimmed.slice(4).trim();
|
|
105
|
+
return stripped || trimmed;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function transformIdentityTextToClaudeCode(text) {
|
|
109
|
+
if (typeof text !== 'string' || !text) return text;
|
|
110
|
+
return text.replace(/open\s*code/gi, 'Claude Code');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function normalizeReasoningEffortToClaude(reasoningEffort) {
|
|
114
|
+
const effort = String(reasoningEffort || '').trim().toLowerCase();
|
|
115
|
+
if (!effort) return undefined;
|
|
116
|
+
if (effort === 'none') return { type: 'disabled' };
|
|
117
|
+
if (effort === 'auto') return { type: 'enabled' };
|
|
118
|
+
if (effort === 'low') return { type: 'enabled', budget_tokens: 2048 };
|
|
119
|
+
if (effort === 'medium') return { type: 'enabled', budget_tokens: 8192 };
|
|
120
|
+
if (effort === 'high') return { type: 'enabled', budget_tokens: 24576 };
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeToolChoiceToClaude(toolChoice) {
|
|
125
|
+
if (!toolChoice) return undefined;
|
|
126
|
+
|
|
127
|
+
if (typeof toolChoice === 'string') {
|
|
128
|
+
if (toolChoice === 'auto') return { type: 'auto' };
|
|
129
|
+
if (toolChoice === 'required') return { type: 'any' };
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (typeof toolChoice === 'object') {
|
|
134
|
+
if (toolChoice.type === 'function' && toolChoice.function?.name) {
|
|
135
|
+
return {
|
|
136
|
+
type: 'tool',
|
|
137
|
+
name: ensureClaudeToolNamePrefix(toolChoice.function.name)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (toolChoice.type === 'function' && toolChoice.name) {
|
|
141
|
+
return {
|
|
142
|
+
type: 'tool',
|
|
143
|
+
name: ensureClaudeToolNamePrefix(toolChoice.name)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (toolChoice.type === 'auto') return { type: 'auto' };
|
|
147
|
+
if (toolChoice.type === 'required') return { type: 'any' };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizeOpenAiToolsToClaude(tools = []) {
|
|
154
|
+
if (!Array.isArray(tools)) return [];
|
|
155
|
+
|
|
156
|
+
const normalized = [];
|
|
157
|
+
for (const tool of tools) {
|
|
158
|
+
if (!tool || typeof tool !== 'object') continue;
|
|
159
|
+
|
|
160
|
+
if (
|
|
161
|
+
tool.type === 'function' &&
|
|
162
|
+
tool.function &&
|
|
163
|
+
typeof tool.function === 'object'
|
|
164
|
+
) {
|
|
165
|
+
const fn = tool.function;
|
|
166
|
+
if (!fn.name) continue;
|
|
167
|
+
normalized.push({
|
|
168
|
+
name: ensureClaudeToolNamePrefix(fn.name),
|
|
169
|
+
description: transformIdentityTextToClaudeCode(fn.description || ''),
|
|
170
|
+
input_schema: fn.parameters || { type: 'object', properties: {} }
|
|
171
|
+
});
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (tool.type === 'function' && tool.name) {
|
|
176
|
+
normalized.push({
|
|
177
|
+
name: ensureClaudeToolNamePrefix(tool.name),
|
|
178
|
+
description: transformIdentityTextToClaudeCode(
|
|
179
|
+
tool.description || ''
|
|
180
|
+
),
|
|
181
|
+
input_schema: tool.parameters || { type: 'object', properties: {} }
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return normalized;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Deep clone helper
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
function deepClone(value) {
|
|
193
|
+
if (value === null || typeof value !== 'object') return value;
|
|
194
|
+
if (Array.isArray(value)) return value.map(deepClone);
|
|
195
|
+
const cloned = {};
|
|
196
|
+
for (const key of Object.keys(value)) {
|
|
197
|
+
cloned[key] = deepClone(value[key]);
|
|
198
|
+
}
|
|
199
|
+
return cloned;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Prompt caching (mutates a cloned payload)
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
function applyPromptCachingToClaudePayload(converted) {
|
|
206
|
+
const EPHEMERAL = { type: 'ephemeral' };
|
|
207
|
+
|
|
208
|
+
let messageBreakpoints = 0;
|
|
209
|
+
if (Array.isArray(converted.messages)) {
|
|
210
|
+
converted.messages.forEach(msg => {
|
|
211
|
+
if (Array.isArray(msg.content)) {
|
|
212
|
+
msg.content.forEach(block => {
|
|
213
|
+
if (block.cache_control) messageBreakpoints++;
|
|
214
|
+
if (
|
|
215
|
+
block.type === 'tool_result' &&
|
|
216
|
+
Array.isArray(block.content)
|
|
217
|
+
) {
|
|
218
|
+
block.content.forEach(inner => {
|
|
219
|
+
if (inner.cache_control) messageBreakpoints++;
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let systemBreakpoints = 0;
|
|
228
|
+
if (Array.isArray(converted.system)) {
|
|
229
|
+
converted.system.forEach(block => {
|
|
230
|
+
if (block.cache_control) systemBreakpoints++;
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (
|
|
235
|
+
systemBreakpoints === 0 &&
|
|
236
|
+
Array.isArray(converted.system) &&
|
|
237
|
+
converted.system.length > 0
|
|
238
|
+
) {
|
|
239
|
+
const last = converted.system[converted.system.length - 1];
|
|
240
|
+
if (!last.cache_control) last.cache_control = EPHEMERAL;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (messageBreakpoints === 0 && systemBreakpoints === 0) {
|
|
244
|
+
if (
|
|
245
|
+
Array.isArray(converted.messages) &&
|
|
246
|
+
converted.messages.length > 0
|
|
247
|
+
) {
|
|
248
|
+
for (const msg of converted.messages.slice(-2)) {
|
|
249
|
+
if (Array.isArray(msg.content) && msg.content.length > 0) {
|
|
250
|
+
const last = msg.content[msg.content.length - 1];
|
|
251
|
+
if (!last.cache_control) last.cache_control = EPHEMERAL;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function applyClaudeToolNamePrefixToMessages(messages = []) {
|
|
259
|
+
if (!Array.isArray(messages)) return [];
|
|
260
|
+
return messages.map(message => {
|
|
261
|
+
if (
|
|
262
|
+
!message ||
|
|
263
|
+
typeof message !== 'object' ||
|
|
264
|
+
!Array.isArray(message.content)
|
|
265
|
+
) {
|
|
266
|
+
return deepClone(message);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const content = message.content.map(block => {
|
|
270
|
+
if (!block || typeof block !== 'object') return deepClone(block);
|
|
271
|
+
if (block.type !== 'tool_use' || !block.name) return deepClone(block);
|
|
272
|
+
return {
|
|
273
|
+
...block,
|
|
274
|
+
name: ensureClaudeToolNamePrefix(block.name)
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
...message,
|
|
280
|
+
content
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function buildClaudeCodeUserId(seed = '') {
|
|
286
|
+
const normalizedSeed = normalizeClaudeSessionSeed(seed);
|
|
287
|
+
const sessionId =
|
|
288
|
+
normalizedSeed ||
|
|
289
|
+
`${Date.now().toString(36)}${Math.random().toString(36).slice(2, 12)}`;
|
|
290
|
+
return `user_${'0'.repeat(64)}_account__session_${sessionId}`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function normalizeSessionKeyValue(value) {
|
|
294
|
+
const source = Array.isArray(value) ? value[0] : value;
|
|
295
|
+
if (typeof source !== 'string') return '';
|
|
296
|
+
const normalized = source.trim();
|
|
297
|
+
if (!normalized) return '';
|
|
298
|
+
const lowered = normalized.toLowerCase();
|
|
299
|
+
if (
|
|
300
|
+
lowered === 'undefined' ||
|
|
301
|
+
lowered === '[undefined]' ||
|
|
302
|
+
lowered === 'null' ||
|
|
303
|
+
lowered === '[null]' ||
|
|
304
|
+
lowered === 'nan' ||
|
|
305
|
+
lowered === '[nan]'
|
|
306
|
+
) {
|
|
307
|
+
return '';
|
|
308
|
+
}
|
|
309
|
+
return normalized;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function normalizeClaudeSessionSeed(value) {
|
|
313
|
+
const normalized = normalizeSessionKeyValue(value);
|
|
314
|
+
if (!normalized) return '';
|
|
315
|
+
return normalized
|
|
316
|
+
.replace(/^user_[0-9a-f]{64}_account__session_/i, '')
|
|
317
|
+
.replace(/[^a-zA-Z0-9_-]/g, '')
|
|
318
|
+
.slice(0, 64);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function extractClaudeSessionSeedFromUserId(value = '') {
|
|
322
|
+
const normalized = normalizeSessionKeyValue(value);
|
|
323
|
+
if (!normalized) return '';
|
|
324
|
+
const matched = normalized.match(
|
|
325
|
+
/^user_[0-9a-f]{64}_account__session_(.+)$/i
|
|
326
|
+
);
|
|
327
|
+
if (!matched) return '';
|
|
328
|
+
return normalizeClaudeSessionSeed(matched[1] || '');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function getClaudeSessionSeedAllowList() {
|
|
332
|
+
const envSeeds = String(
|
|
333
|
+
process.env.OPENCODE_CLAUDE_FALLBACK_SESSION_SEEDS ||
|
|
334
|
+
'session_test,session,sessiontest,session_'
|
|
335
|
+
)
|
|
336
|
+
.split(',')
|
|
337
|
+
.map(seed => normalizeClaudeSessionSeed(seed))
|
|
338
|
+
.filter(Boolean);
|
|
339
|
+
const fallbackSeed = normalizeClaudeSessionSeed(
|
|
340
|
+
process.env.OPENCODE_CLAUDE_FALLBACK_SESSION_SEED || 'session_test'
|
|
341
|
+
);
|
|
342
|
+
return Array.from(
|
|
343
|
+
new Set(
|
|
344
|
+
[
|
|
345
|
+
...envSeeds,
|
|
346
|
+
fallbackSeed,
|
|
347
|
+
'session_test',
|
|
348
|
+
'session',
|
|
349
|
+
'sessiontest',
|
|
350
|
+
'session_'
|
|
351
|
+
].filter(Boolean)
|
|
352
|
+
)
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function isValidClaudeCodeUserId(value = '', options = {}) {
|
|
357
|
+
const normalized = normalizeSessionKeyValue(value);
|
|
358
|
+
if (!normalized) return false;
|
|
359
|
+
const matched = normalized.match(
|
|
360
|
+
/^user_[0-9a-f]{64}_account__session_([a-zA-Z0-9_-]{1,64})$/i
|
|
361
|
+
);
|
|
362
|
+
if (!matched) return false;
|
|
363
|
+
if (!options.enforceAllowedSeed) return true;
|
|
364
|
+
const allowedSeeds =
|
|
365
|
+
Array.isArray(options.allowedSeeds) && options.allowedSeeds.length > 0
|
|
366
|
+
? options.allowedSeeds
|
|
367
|
+
: getClaudeSessionSeedAllowList();
|
|
368
|
+
const seed = normalizeClaudeSessionSeed(matched[1] || '');
|
|
369
|
+
return !!seed && allowedSeeds.includes(seed);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function normalizeClaudeMetadata(metadata, fallbackUserId = '') {
|
|
373
|
+
const normalized =
|
|
374
|
+
metadata && typeof metadata === 'object' && !Array.isArray(metadata)
|
|
375
|
+
? { ...metadata }
|
|
376
|
+
: {};
|
|
377
|
+
const allowedSeeds = getClaudeSessionSeedAllowList();
|
|
378
|
+
const userId = normalizeSessionKeyValue(normalized.user_id);
|
|
379
|
+
const fallbackValue = normalizeSessionKeyValue(fallbackUserId);
|
|
380
|
+
if (
|
|
381
|
+
isValidClaudeCodeUserId(userId, {
|
|
382
|
+
enforceAllowedSeed: true,
|
|
383
|
+
allowedSeeds
|
|
384
|
+
})
|
|
385
|
+
) {
|
|
386
|
+
normalized.user_id = userId;
|
|
387
|
+
return normalized;
|
|
388
|
+
}
|
|
389
|
+
if (
|
|
390
|
+
isValidClaudeCodeUserId(fallbackValue, {
|
|
391
|
+
enforceAllowedSeed: true,
|
|
392
|
+
allowedSeeds
|
|
393
|
+
})
|
|
394
|
+
) {
|
|
395
|
+
normalized.user_id = fallbackValue;
|
|
396
|
+
return normalized;
|
|
397
|
+
}
|
|
398
|
+
const fallbackSeedRaw =
|
|
399
|
+
extractClaudeSessionSeedFromUserId(fallbackValue) ||
|
|
400
|
+
normalizeClaudeSessionSeed(fallbackValue);
|
|
401
|
+
const fallbackSeed = allowedSeeds.includes(fallbackSeedRaw)
|
|
402
|
+
? fallbackSeedRaw
|
|
403
|
+
: allowedSeeds[0] || 'session_test';
|
|
404
|
+
normalized.user_id = buildClaudeCodeUserId(fallbackSeed);
|
|
405
|
+
return normalized;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
// Header & URL construction
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
function isOfficialAnthropicEndpoint(baseUrl = '') {
|
|
412
|
+
try {
|
|
413
|
+
const hostname = new URL(String(baseUrl || '').trim()).hostname;
|
|
414
|
+
return hostname === 'api.anthropic.com';
|
|
415
|
+
} catch {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function buildClaudeBetaHeader(options = {}) {
|
|
421
|
+
const requestType = options.requestType === 'count_tokens' ? 'count_tokens' : 'messages';
|
|
422
|
+
const betaFlags = requestType === 'count_tokens'
|
|
423
|
+
? [...CLAUDE_COUNT_TOKENS_BETA_FLAGS]
|
|
424
|
+
: [...CLAUDE_MESSAGES_BETA_FLAGS];
|
|
425
|
+
if (requestType === 'messages' && options.hasTools) {
|
|
426
|
+
betaFlags.push(CLAUDE_ADVANCED_TOOL_USE_BETA);
|
|
427
|
+
}
|
|
428
|
+
return betaFlags.join(',');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function generateClientRequestId() {
|
|
432
|
+
try {
|
|
433
|
+
return crypto.randomUUID();
|
|
434
|
+
} catch {
|
|
435
|
+
return `${Date.now().toString(36)}-${Math.random()
|
|
436
|
+
.toString(36)
|
|
437
|
+
.slice(2, 10)}`;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function buildClaudeRequestHeaders(apiKey, options = {}) {
|
|
442
|
+
const {
|
|
443
|
+
stream = false,
|
|
444
|
+
hasTools = false,
|
|
445
|
+
officialEndpoint = false,
|
|
446
|
+
networkHeaders = true,
|
|
447
|
+
requestType = 'messages'
|
|
448
|
+
} = options;
|
|
449
|
+
|
|
450
|
+
if (!networkHeaders) {
|
|
451
|
+
return applyHeaderOverrides({}, options);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const baseHeaders = {
|
|
455
|
+
'content-type': 'application/json',
|
|
456
|
+
'anthropic-version': '2023-06-01',
|
|
457
|
+
'anthropic-beta': buildClaudeBetaHeader({ hasTools, requestType }),
|
|
458
|
+
'anthropic-dangerous-direct-browser-access': 'true',
|
|
459
|
+
'x-app': 'cli',
|
|
460
|
+
'x-client-request-id': generateClientRequestId(),
|
|
461
|
+
'user-agent': CLAUDE_CODE_USER_AGENT,
|
|
462
|
+
accept: stream ? 'text/event-stream' : 'application/json'
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
if (officialEndpoint && apiKey) {
|
|
466
|
+
baseHeaders['x-api-key'] = apiKey;
|
|
467
|
+
} else if (!officialEndpoint && apiKey) {
|
|
468
|
+
baseHeaders['authorization'] = `Bearer ${apiKey}`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return applyHeaderOverrides(baseHeaders, options);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Build request headers for the /v1/messages/count_tokens preflight.
|
|
476
|
+
* Follows the shared auth semantics (official X-Api-Key vs custom Bearer)
|
|
477
|
+
* and honors explicit header overrides from options.headers /
|
|
478
|
+
* options.providerConfig?.headers.
|
|
479
|
+
*/
|
|
480
|
+
function buildClaudeCountTokensHeaders(apiKey, options = {}) {
|
|
481
|
+
const { baseUrl = '', hasTools = false, headers, providerConfig } = options;
|
|
482
|
+
return buildClaudeRequestHeaders(apiKey, {
|
|
483
|
+
stream: false,
|
|
484
|
+
hasTools,
|
|
485
|
+
officialEndpoint: isOfficialAnthropicEndpoint(baseUrl),
|
|
486
|
+
requestType: 'count_tokens',
|
|
487
|
+
headers,
|
|
488
|
+
providerConfig
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function buildClaudeTargetUrl(baseUrl = '', _options = {}) {
|
|
493
|
+
const trimmed = String(baseUrl || '').trim();
|
|
494
|
+
if (!trimmed) {
|
|
495
|
+
const defaultUrl = new URL('https://api.anthropic.com');
|
|
496
|
+
defaultUrl.pathname = '/v1/messages';
|
|
497
|
+
defaultUrl.searchParams.set('beta', 'true');
|
|
498
|
+
return defaultUrl.toString();
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
let targetUrl;
|
|
502
|
+
try {
|
|
503
|
+
targetUrl = new URL(trimmed);
|
|
504
|
+
} catch {
|
|
505
|
+
return '';
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (!targetUrl.hostname) return '';
|
|
509
|
+
|
|
510
|
+
let pathname = targetUrl.pathname.replace(/\/+$/, '');
|
|
511
|
+
if (!pathname || pathname === '/') {
|
|
512
|
+
pathname = '/v1/messages';
|
|
513
|
+
} else if (pathname.endsWith('/messages')) {
|
|
514
|
+
// preserve existing /messages path
|
|
515
|
+
} else if (pathname.endsWith('/v1')) {
|
|
516
|
+
pathname = `${pathname}/messages`;
|
|
517
|
+
} else {
|
|
518
|
+
pathname = `${pathname}/v1/messages`;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
targetUrl.pathname = pathname;
|
|
522
|
+
targetUrl.searchParams.set('beta', 'true');
|
|
523
|
+
return targetUrl.toString();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function buildClaudeCountTokensTargetUrl(baseUrl = '') {
|
|
527
|
+
const trimmed = String(baseUrl || '').trim();
|
|
528
|
+
if (!trimmed) {
|
|
529
|
+
const defaultUrl = new URL('https://api.anthropic.com');
|
|
530
|
+
defaultUrl.pathname = '/v1/messages/count_tokens';
|
|
531
|
+
defaultUrl.searchParams.set('beta', 'true');
|
|
532
|
+
return defaultUrl.toString();
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
let targetUrl;
|
|
536
|
+
try {
|
|
537
|
+
targetUrl = new URL(trimmed);
|
|
538
|
+
} catch {
|
|
539
|
+
return '';
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (!targetUrl.hostname) return '';
|
|
543
|
+
|
|
544
|
+
let pathname = targetUrl.pathname.replace(/\/+$/, '');
|
|
545
|
+
if (!pathname || pathname === '/') {
|
|
546
|
+
pathname = '/v1/messages/count_tokens';
|
|
547
|
+
} else if (pathname.endsWith('/messages/count_tokens')) {
|
|
548
|
+
// preserve existing path
|
|
549
|
+
} else if (pathname.endsWith('/messages')) {
|
|
550
|
+
pathname = `${pathname}/count_tokens`;
|
|
551
|
+
} else if (pathname.endsWith('/v1')) {
|
|
552
|
+
pathname = `${pathname}/messages/count_tokens`;
|
|
553
|
+
} else {
|
|
554
|
+
pathname = `${pathname}/v1/messages/count_tokens`;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
targetUrl.pathname = pathname;
|
|
558
|
+
targetUrl.searchParams.set('beta', 'true');
|
|
559
|
+
return targetUrl.toString();
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ---------------------------------------------------------------------------
|
|
563
|
+
// Public API
|
|
564
|
+
// ---------------------------------------------------------------------------
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Create a Claude API request from an OpenCode-compatible payload.
|
|
568
|
+
*
|
|
569
|
+
* @param {string} pathname - request path (e.g. /v1/chat/completions, /v1/responses, /v1/messages)
|
|
570
|
+
* @param {object} [payload={}] - OpenCode request body
|
|
571
|
+
* @param {object} [options={}] -
|
|
572
|
+
* apiKey, baseUrl, fallbackModel, stream, sessionUserId, networkHeaders,
|
|
573
|
+
* headers, providerConfig
|
|
574
|
+
* @returns {{ body: object, headers: object, model: string }}
|
|
575
|
+
*/
|
|
576
|
+
function createClaudeRequest(pathname, payload = {}, options = {}) {
|
|
577
|
+
const {
|
|
578
|
+
apiKey = '',
|
|
579
|
+
baseUrl = '',
|
|
580
|
+
fallbackModel = '',
|
|
581
|
+
stream = false,
|
|
582
|
+
sessionUserId = '',
|
|
583
|
+
networkHeaders = true
|
|
584
|
+
} = options;
|
|
585
|
+
|
|
586
|
+
const normalized = normalizeOpenCodeMessages(pathname, payload);
|
|
587
|
+
const maxTokens = Number(payload.max_output_tokens ?? payload.max_tokens);
|
|
588
|
+
const stopSequencesVal = normalizeStopSequences(payload.stop);
|
|
589
|
+
const thinking = normalizeReasoningEffortToClaude(
|
|
590
|
+
payload.reasoning_effort
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
// Deep clone messages to prevent mutating the input
|
|
594
|
+
const clonedMessages = deepClone(
|
|
595
|
+
applyClaudeToolNamePrefixToMessages(normalized.messages)
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
const body = {
|
|
599
|
+
model:
|
|
600
|
+
payload.model || fallbackModel || 'claude-sonnet-4-20250514',
|
|
601
|
+
max_tokens:
|
|
602
|
+
Number.isFinite(maxTokens) && maxTokens > 0
|
|
603
|
+
? Math.round(maxTokens)
|
|
604
|
+
: 4096,
|
|
605
|
+
stream,
|
|
606
|
+
messages: clonedMessages
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
if (
|
|
610
|
+
normalized.systemBlocks &&
|
|
611
|
+
normalized.systemBlocks.length > 0
|
|
612
|
+
) {
|
|
613
|
+
body.system = deepClone(normalized.systemBlocks)
|
|
614
|
+
.map(block => {
|
|
615
|
+
if (!block || typeof block !== 'object') return null;
|
|
616
|
+
if (block.type !== 'text') return block;
|
|
617
|
+
return {
|
|
618
|
+
...block,
|
|
619
|
+
text: transformIdentityTextToClaudeCode(block.text || '')
|
|
620
|
+
};
|
|
621
|
+
})
|
|
622
|
+
.filter(Boolean);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (!Array.isArray(body.system) || body.system.length === 0) {
|
|
626
|
+
body.system = [
|
|
627
|
+
{ type: 'text', text: DEFAULT_CLAUDE_CODE_SYSTEM_PROMPT }
|
|
628
|
+
];
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const tools = normalizeOpenAiToolsToClaude(payload.tools || []);
|
|
632
|
+
if (tools.length > 0) {
|
|
633
|
+
body.tools = tools;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const toolChoice = normalizeToolChoiceToClaude(payload.tool_choice);
|
|
637
|
+
if (toolChoice) {
|
|
638
|
+
body.tool_choice = toolChoice;
|
|
639
|
+
}
|
|
640
|
+
if (stopSequencesVal) {
|
|
641
|
+
body.stop_sequences = stopSequencesVal;
|
|
642
|
+
}
|
|
643
|
+
if (thinking) {
|
|
644
|
+
body.thinking = thinking;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (Number.isFinite(Number(payload.temperature))) {
|
|
648
|
+
body.temperature = Number(payload.temperature);
|
|
649
|
+
}
|
|
650
|
+
if (Number.isFinite(Number(payload.top_p))) {
|
|
651
|
+
body.top_p = Number(payload.top_p);
|
|
652
|
+
}
|
|
653
|
+
if (Number.isFinite(Number(payload.top_k))) {
|
|
654
|
+
body.top_k = Number(payload.top_k);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
body.metadata = normalizeClaudeMetadata(
|
|
658
|
+
payload.metadata,
|
|
659
|
+
sessionUserId
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
applyPromptCachingToClaudePayload(body);
|
|
663
|
+
|
|
664
|
+
const hasTools = Array.isArray(body.tools) && body.tools.length > 0;
|
|
665
|
+
const officialEndpoint = isOfficialAnthropicEndpoint(baseUrl);
|
|
666
|
+
|
|
667
|
+
const headers = buildClaudeRequestHeaders(apiKey, {
|
|
668
|
+
stream,
|
|
669
|
+
hasTools,
|
|
670
|
+
officialEndpoint,
|
|
671
|
+
networkHeaders,
|
|
672
|
+
headers: options.headers,
|
|
673
|
+
providerConfig: options.providerConfig
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
return {
|
|
677
|
+
body,
|
|
678
|
+
headers,
|
|
679
|
+
model: body.model
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// ---------------------------------------------------------------------------
|
|
684
|
+
// Exports
|
|
685
|
+
// ---------------------------------------------------------------------------
|
|
686
|
+
module.exports = {
|
|
687
|
+
createClaudeRequest,
|
|
688
|
+
buildClaudeTargetUrl,
|
|
689
|
+
buildClaudeCountTokensTargetUrl,
|
|
690
|
+
buildClaudeCountTokensHeaders,
|
|
691
|
+
|
|
692
|
+
// Public adapter-required export
|
|
693
|
+
stripClaudeToolNamePrefix
|
|
694
|
+
};
|