coding-tool-x 3.8.1 → 3.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/dist/web/assets/{Analytics-8dRv1aYW.js → Analytics-COEUXmd4.js} +1 -1
  3. package/dist/web/assets/{ConfigTemplates-BQRXSICO.js → ConfigTemplates-DLLkS53H.js} +1 -1
  4. package/dist/web/assets/{Home-BtzWhKIa.js → Home-DCEyLzwX.js} +1 -1
  5. package/dist/web/assets/{PluginManager-BTa-Ygf6.js → PluginManager-Oe1ZHoMO.js} +1 -1
  6. package/dist/web/assets/{ProjectList-F_cB8ldA.js → ProjectList-CWjuAFof.js} +1 -1
  7. package/dist/web/assets/{SessionList-LOrbPQvu.js → SessionList-v3ehGg9U.js} +1 -1
  8. package/dist/web/assets/{SkillManager-BzI_t-yV.js → SkillManager-DUF_qP5S.js} +1 -1
  9. package/dist/web/assets/{WorkspaceManager-Db9RGiz7.js → WorkspaceManager-CjAWBosO.js} +1 -1
  10. package/dist/web/assets/index-D5hyHPj7.js +2 -0
  11. package/dist/web/index.html +1 -1
  12. package/docs/superpowers/plans/2026-08-10-codex-conversion-compatibility.md +251 -0
  13. package/docs/superpowers/plans/2026-08-11-windows-command-window-hiding.md +90 -0
  14. package/docs/superpowers/plans/2026-08-13-omp-restart-recovery.md +48 -0
  15. package/docs/superpowers/specs/2026-08-10-codex-conversion-compatibility-design.md +61 -0
  16. package/docs/superpowers/specs/2026-08-11-windows-command-window-hiding-design.md +44 -0
  17. package/docs/superpowers/specs/2026-08-13-omp-restart-recovery-design.md +22 -0
  18. package/package.json +1 -1
  19. package/src/commands/daemon.js +3 -12
  20. package/src/commands/update.js +1 -1
  21. package/src/index.js +8 -1
  22. package/src/server/omp-proxy-server.js +9 -6
  23. package/src/server/opencode-proxy-server.js +55 -1037
  24. package/src/server/services/claude-wire.js +694 -0
  25. package/src/server/services/codex-wire.js +219 -0
  26. package/src/server/services/gemini-wire.js +759 -0
  27. package/src/server/services/model-detector.js +49 -131
  28. package/src/server/services/omp-channels.js +23 -4
  29. package/src/server/services/omp-gateway.js +29 -1
  30. package/src/server/services/omp-settings-manager.js +27 -3
  31. package/src/server/services/opencode-gateway-adapters.js +22 -1147
  32. package/src/server/services/opencode-normalization.js +546 -0
  33. package/src/server/services/session-history-index.js +1 -1
  34. package/src/server/services/session-history-worker.js +2 -1
  35. package/src/server/services/speed-test.js +85 -303
  36. 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
+ };