aicodeswitch 4.0.4 → 5.0.0

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 (77) hide show
  1. package/README.md +6 -5
  2. package/UPGRADE.md +5 -6
  3. package/dist/server/coding-plan.js +94 -0
  4. package/dist/server/config-managed-fields.js +1 -0
  5. package/dist/server/conversions/compact.js +613 -0
  6. package/dist/server/conversions/detector.js +70 -0
  7. package/dist/server/conversions/index.js +285 -0
  8. package/dist/server/conversions/pairs/claude-completions/request.js +167 -0
  9. package/dist/server/conversions/pairs/claude-completions/response.js +56 -0
  10. package/dist/server/conversions/pairs/claude-completions/streaming.js +259 -0
  11. package/dist/server/conversions/pairs/claude-gemini/request.js +130 -0
  12. package/dist/server/conversions/pairs/claude-gemini/response.js +65 -0
  13. package/dist/server/conversions/pairs/claude-gemini/streaming.js +199 -0
  14. package/dist/server/conversions/pairs/claude-responses/request.js +190 -0
  15. package/dist/server/conversions/pairs/claude-responses/response.js +89 -0
  16. package/dist/server/conversions/pairs/claude-responses/streaming.js +266 -0
  17. package/dist/server/conversions/pairs/completions-claude/request.js +111 -0
  18. package/dist/server/conversions/pairs/completions-claude/response.js +67 -0
  19. package/dist/server/conversions/pairs/completions-claude/streaming.js +165 -0
  20. package/dist/server/conversions/pairs/completions-gemini/request.js +169 -0
  21. package/dist/server/conversions/pairs/completions-gemini/response.js +70 -0
  22. package/dist/server/conversions/pairs/completions-gemini/streaming.js +132 -0
  23. package/dist/server/conversions/pairs/completions-responses/request.js +149 -0
  24. package/dist/server/conversions/pairs/completions-responses/response.js +74 -0
  25. package/dist/server/conversions/pairs/completions-responses/streaming.js +189 -0
  26. package/dist/server/conversions/pairs/gemini-claude/request.js +118 -0
  27. package/dist/server/conversions/pairs/gemini-claude/response.js +45 -0
  28. package/dist/server/conversions/pairs/gemini-claude/streaming.js +146 -0
  29. package/dist/server/conversions/pairs/gemini-completions/request.js +151 -0
  30. package/dist/server/conversions/pairs/gemini-completions/response.js +54 -0
  31. package/dist/server/conversions/pairs/gemini-completions/streaming.js +108 -0
  32. package/dist/server/conversions/pairs/gemini-responses/request.js +18 -0
  33. package/dist/server/conversions/pairs/gemini-responses/response.js +18 -0
  34. package/dist/server/conversions/pairs/gemini-responses/streaming.js +43 -0
  35. package/dist/server/conversions/pairs/responses-claude/request.js +155 -0
  36. package/dist/server/conversions/pairs/responses-claude/response.js +70 -0
  37. package/dist/server/conversions/pairs/responses-claude/streaming.js +345 -0
  38. package/dist/server/conversions/pairs/responses-completions/request.js +207 -0
  39. package/dist/server/conversions/pairs/responses-completions/response.js +96 -0
  40. package/dist/server/conversions/pairs/responses-completions/streaming.js +344 -0
  41. package/dist/server/conversions/pairs/responses-gemini/request.js +18 -0
  42. package/dist/server/conversions/pairs/responses-gemini/response.js +18 -0
  43. package/dist/server/conversions/pairs/responses-gemini/streaming.js +43 -0
  44. package/dist/server/conversions/pairs/responses-responses/request.js +115 -0
  45. package/dist/server/conversions/pipeline.js +296 -0
  46. package/dist/server/conversions/stream-converter-adapter.js +49 -0
  47. package/dist/server/conversions/thinking/effort.js +61 -0
  48. package/dist/server/conversions/thinking/mapper.js +59 -0
  49. package/dist/server/conversions/thinking/providers.js +76 -0
  50. package/dist/server/conversions/types.js +5 -0
  51. package/dist/server/conversions/url-normalizer.js +58 -0
  52. package/dist/server/conversions/utils/format-mappers.js +57 -0
  53. package/dist/server/conversions/utils/id.js +33 -0
  54. package/dist/server/conversions/utils/stop-reasons.js +95 -0
  55. package/dist/server/conversions/utils/streaming-helpers.js +59 -0
  56. package/dist/server/conversions/utils/tool-schema.js +169 -0
  57. package/dist/server/conversions/utils/usage.js +82 -0
  58. package/dist/server/fs-database.js +465 -135
  59. package/dist/server/main.js +93 -33
  60. package/dist/server/original-config-reader.js +1 -1
  61. package/dist/server/proxy-server.js +887 -633
  62. package/dist/server/transformers/chunk-collector.js +5 -1
  63. package/dist/server/transformers/streaming.js +6 -3235
  64. package/dist/server/type-migration.js +2 -3
  65. package/dist/server/utils.js +5 -0
  66. package/dist/ui/assets/{index-C7G0whng.css → index-BHR12ImE.css} +1 -1
  67. package/dist/ui/assets/index-DjdBW1yu.js +517 -0
  68. package/dist/ui/index.html +2 -2
  69. package/package.json +1 -1
  70. package/dist/server/transformers/transformers.js +0 -1767
  71. package/dist/ui/assets/index-Dl-B9pXM.js +0 -514
  72. package/schema/claude.schema.md +0 -946
  73. package/schema/deepseek-chat.schema.md +0 -799
  74. package/schema/gemini.schema.md +0 -1408
  75. package/schema/openai-chat-completions.schema.md +0 -1088
  76. package/schema/openai-responses.schema.md +0 -226196
  77. package/schema/stream.md +0 -2592
@@ -0,0 +1,613 @@
1
+ "use strict";
2
+ /**
3
+ * Conversation Compaction for the Responses API.
4
+ *
5
+ * Provides the core logic for `/v1/responses/compact`:
6
+ * - Extract conversation text from Responses API `input` array
7
+ * - Build compaction prompt
8
+ * - Build upstream requests via the conversion system (transformRequest)
9
+ * - Extract summary from upstream responses (transformResponse)
10
+ * - Build the final Responses API compaction response
11
+ *
12
+ * Two usage levels:
13
+ * - **High-level**: `prepareCompactRequest` + `processCompactResponse` — unified API
14
+ * - **Low-level**: Individual functions for fine-grained control
15
+ */
16
+ var __importDefault = (this && this.__importDefault) || function (mod) {
17
+ return (mod && mod.__esModule) ? mod : { "default": mod };
18
+ };
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.COMPACTION_SYSTEM_PROMPT = void 0;
21
+ exports.extractConversationText = extractConversationText;
22
+ exports.extractMessageContent = extractMessageContent;
23
+ exports.isClaudeCompactRequest = isClaudeCompactRequest;
24
+ exports.isLastClaudeMessageCompact = isLastClaudeMessageCompact;
25
+ exports.isCodexCompactRequest = isCodexCompactRequest;
26
+ exports.sanitizeClaudeMessagesForCompact = sanitizeClaudeMessagesForCompact;
27
+ exports.flattenClaudeToolBlocksForCompact = flattenClaudeToolBlocksForCompact;
28
+ exports.normalizeClaudeCompactRequestBody = normalizeClaudeCompactRequestBody;
29
+ exports.stripClaudeCompactResponseContent = stripClaudeCompactResponseContent;
30
+ exports.countUnpairedClaudeToolUses = countUnpairedClaudeToolUses;
31
+ exports.summarizeClaudeMessagesForDebug = summarizeClaudeMessagesForDebug;
32
+ exports.collectCompactPayloadDebugInfo = collectCompactPayloadDebugInfo;
33
+ exports.collectClaudeToolUseDiagnostics = collectClaudeToolUseDiagnostics;
34
+ exports.buildCompactionPrompt = buildCompactionPrompt;
35
+ exports.buildCompactUpstreamRequest = buildCompactUpstreamRequest;
36
+ exports.extractSummaryFromResponse = extractSummaryFromResponse;
37
+ exports.buildCompactedResponse = buildCompactedResponse;
38
+ exports.prepareCompactRequest = prepareCompactRequest;
39
+ exports.processCompactResponse = processCompactResponse;
40
+ const crypto_1 = __importDefault(require("crypto"));
41
+ const index_js_1 = require("./index.js");
42
+ // ============================================================
43
+ // Conversation Text Extraction
44
+ // ============================================================
45
+ /**
46
+ * Extract conversation text from a Responses API `input` array.
47
+ * Converts each item (message, function_call, function_call_output) into
48
+ * readable text suitable for a compaction prompt.
49
+ * Compaction items are skipped (encrypted_content is opaque).
50
+ */
51
+ function extractConversationText(input) {
52
+ const parts = [];
53
+ for (const item of input) {
54
+ if (!item || typeof item !== 'object') {
55
+ if (typeof item === 'string')
56
+ parts.push(item);
57
+ continue;
58
+ }
59
+ // Skip compaction items — their encrypted_content is not readable
60
+ if (item.type === 'compaction')
61
+ continue;
62
+ // Skip item references
63
+ if (item.type === 'item_reference')
64
+ continue;
65
+ // Message items (EasyInputMessage, ResponseOutputMessage, etc.)
66
+ if (item.type === 'message' || item.role) {
67
+ const role = item.role || 'unknown';
68
+ const content = extractMessageContent(item.content);
69
+ if (content) {
70
+ parts.push(`[${role}]: ${content}`);
71
+ }
72
+ }
73
+ // Function call items
74
+ if (item.type === 'function_call') {
75
+ const name = item.name || 'unknown';
76
+ const args = item.arguments || item.input || '';
77
+ const callId = item.call_id || '';
78
+ parts.push(`[function_call${callId ? ` (${callId})` : ''} -> ${name}]: ${typeof args === 'string' ? args : JSON.stringify(args)}`);
79
+ }
80
+ // Function call output items
81
+ if (item.type === 'function_call_output') {
82
+ const callId = item.call_id || '';
83
+ const output = item.output || '';
84
+ parts.push(`[function_call_output${callId ? ` (${callId})` : ''}]: ${typeof output === 'string' ? output : JSON.stringify(output)}`);
85
+ }
86
+ }
87
+ return parts.join('\n\n');
88
+ }
89
+ /**
90
+ * Extract text content from a Responses API message's content field.
91
+ * Handles string content, content arrays (input_text, output_text, text, etc.),
92
+ * and nested structures.
93
+ */
94
+ function extractMessageContent(content) {
95
+ if (!content)
96
+ return '';
97
+ if (typeof content === 'string')
98
+ return content;
99
+ if (Array.isArray(content)) {
100
+ return content
101
+ .map((block) => {
102
+ if (!block || typeof block !== 'object')
103
+ return String(block || '');
104
+ if (typeof block === 'string')
105
+ return block;
106
+ // text, input_text, output_text
107
+ if (block.text)
108
+ return block.text;
109
+ // input_image, input_file — skip
110
+ if (block.type === 'input_image' || block.type === 'input_file')
111
+ return '[media content]';
112
+ return '';
113
+ })
114
+ .filter(Boolean)
115
+ .join('\n');
116
+ }
117
+ return String(content);
118
+ }
119
+ // ============================================================
120
+ // Compact Request Detection
121
+ // ============================================================
122
+ /**
123
+ * 检测消息是否为 Claude Code 的 compact 命令请求。
124
+ *
125
+ * Compact 命令触发时,Claude Code 会在 messages 末尾插入一条特殊指令:
126
+ * - role 为 "user"
127
+ * - content 为数组,包含一个 text 块
128
+ * - text 内容以 "CRITICAL: Respond with TEXT ONLY" 开头
129
+ * - 包含对话摘要生成指令,要求输出 <analysis> 和 <summary> 结构
130
+ */
131
+ function isClaudeCompactRequest(message) {
132
+ if (!message || message.role !== 'user') {
133
+ return false;
134
+ }
135
+ const content = message.content;
136
+ if (!Array.isArray(content)) {
137
+ return false;
138
+ }
139
+ for (const block of content) {
140
+ if ((block === null || block === void 0 ? void 0 : block.type) === 'text' && typeof block.text === 'string') {
141
+ const text = block.text;
142
+ if (text.includes('CRITICAL: Respond with TEXT ONLY') &&
143
+ text.includes('create a detailed summary of the conversation') &&
144
+ text.includes('<analysis>') &&
145
+ text.includes('<summary>')) {
146
+ return true;
147
+ }
148
+ }
149
+ }
150
+ return false;
151
+ }
152
+ /**
153
+ * 检测消息列表中的最后一条消息是否为 Claude Code compact 请求。
154
+ */
155
+ function isLastClaudeMessageCompact(messages) {
156
+ if (!Array.isArray(messages) || messages.length === 0) {
157
+ return false;
158
+ }
159
+ return isClaudeCompactRequest(messages[messages.length - 1]);
160
+ }
161
+ /**
162
+ * 检测请求是否为 Codex 的 compact(压缩)请求。
163
+ *
164
+ * Codex 基于 OpenAI Responses API,compact 操作走独立端点:
165
+ * - POST /v1/responses/compact
166
+ */
167
+ function isCodexCompactRequest(path) {
168
+ if (!path || typeof path !== 'string') {
169
+ return false;
170
+ }
171
+ const normalizedPath = path.split('?')[0];
172
+ return /\/v1\/responses\/compact\/?$/.test(normalizedPath);
173
+ }
174
+ // ============================================================
175
+ // Claude Messages Sanitization
176
+ // ============================================================
177
+ /**
178
+ * 判断内容块是否为工具调用类型(包括 tool_use 和 server_tool_use)。
179
+ * Claude Code 的内置工具(如 webReader)使用 server_tool_use 类型,
180
+ * 上游 Claude 兼容 API 同样要求其紧邻的下一条 user 消息包含对应的 tool_result。
181
+ */
182
+ function isToolUseBlock(block) {
183
+ return ((block === null || block === void 0 ? void 0 : block.type) === 'tool_use' || (block === null || block === void 0 ? void 0 : block.type) === 'server_tool_use') && typeof block.id === 'string' && block.id;
184
+ }
185
+ /**
186
+ * 清理 Claude Messages API 格式的 messages,确保所有 tool_use/server_tool_use 都有对应的 tool_result。
187
+ * 对于没有对应 tool_result 的工具调用,添加合成的 tool_result 块。
188
+ *
189
+ * 这解决了 Claude Code compact 请求中 assistant 消息末尾有 tool_use/server_tool_use 但
190
+ * 下一条 user 消息(compact 指令)不含对应 tool_result 导致上游 400 错误的问题。
191
+ */
192
+ function sanitizeClaudeMessagesForCompact(messages) {
193
+ if (!Array.isArray(messages) || messages.length === 0)
194
+ return messages;
195
+ const result = [...messages];
196
+ for (let i = 0; i < result.length; i++) {
197
+ const msg = result[i];
198
+ if ((msg === null || msg === void 0 ? void 0 : msg.role) !== 'assistant' || !Array.isArray(msg.content))
199
+ continue;
200
+ // 收集当前 assistant 消息中所有 tool_use/server_tool_use 的 id
201
+ const toolUseIds = new Set();
202
+ for (const block of msg.content) {
203
+ if (isToolUseBlock(block)) {
204
+ toolUseIds.add(block.id);
205
+ }
206
+ }
207
+ if (toolUseIds.size === 0)
208
+ continue;
209
+ // 检查下一条 user 消息中是否有对应的 tool_result
210
+ const nextMsg = result[i + 1];
211
+ if (!nextMsg || nextMsg.role !== 'user') {
212
+ // 若下一条不是 user,直接插入一条合成 user(tool_result) 消息,
213
+ // 强制满足 Claude 要求的“紧邻下一条消息必须含 tool_result”约束。
214
+ result.splice(i + 1, 0, {
215
+ role: 'user',
216
+ content: [...toolUseIds].map(id => ({
217
+ type: 'tool_result',
218
+ tool_use_id: id,
219
+ content: '[Result omitted for compaction]',
220
+ })),
221
+ });
222
+ i += 1;
223
+ continue;
224
+ }
225
+ const userContent = Array.isArray(nextMsg.content)
226
+ ? [...nextMsg.content]
227
+ : (typeof nextMsg.content === 'string' && nextMsg.content.trim())
228
+ ? [{ type: 'text', text: nextMsg.content }]
229
+ : [];
230
+ const toolResultBlocks = userContent.filter((block) => (block === null || block === void 0 ? void 0 : block.type) === 'tool_result' && typeof block.tool_use_id === 'string' && block.tool_use_id);
231
+ const providedResultIds = new Set(toolResultBlocks.map((block) => block.tool_use_id));
232
+ const nonToolResultBlocks = userContent.filter((block) => (block === null || block === void 0 ? void 0 : block.type) !== 'tool_result');
233
+ // 找到未配对的 tool_use id
234
+ const missingIds = [...toolUseIds].filter(id => !providedResultIds.has(id));
235
+ // 为未配对的 tool_use 补充合成的 tool_result
236
+ const syntheticResults = missingIds.map(id => ({
237
+ type: 'tool_result',
238
+ tool_use_id: id,
239
+ content: '[Result omitted for compaction]',
240
+ }));
241
+ const orderedToolResults = [...toolResultBlocks, ...syntheticResults];
242
+ // 某些 Claude 兼容实现要求 tool_result 独占“下一条消息”,
243
+ // 不能与后续 compact 指令文本混在同一个 user message 中。
244
+ if (nonToolResultBlocks.length > 0) {
245
+ result.splice(i + 1, 1, {
246
+ role: 'user',
247
+ content: orderedToolResults,
248
+ }, Object.assign(Object.assign({}, nextMsg), { content: nonToolResultBlocks }));
249
+ i += 1;
250
+ continue;
251
+ }
252
+ result[i + 1] = Object.assign(Object.assign({}, nextMsg), { content: orderedToolResults });
253
+ }
254
+ return result;
255
+ }
256
+ /**
257
+ * 将 Claude 历史中的 tool_use / tool_result 块降级为普通文本块。
258
+ *
259
+ * compact 请求的目标只是生成摘要,不需要保留严格的工具调用协议语义。
260
+ * 对某些 Claude 兼容实现(如第三方 Claude 标准接口)而言,历史中出现
261
+ * tool_use/tool_result 往往会触发比 Anthropic 官方更严格的校验。
262
+ * 因此在 compact 场景下,将工具块平铺为文本是更稳妥的上游输入形式。
263
+ */
264
+ function flattenClaudeToolBlocksForCompact(messages) {
265
+ if (!Array.isArray(messages) || messages.length === 0)
266
+ return messages;
267
+ return messages.map((message) => {
268
+ if (!Array.isArray(message === null || message === void 0 ? void 0 : message.content)) {
269
+ return message;
270
+ }
271
+ const flattenedContent = message.content.map((block) => {
272
+ if ((block === null || block === void 0 ? void 0 : block.type) === 'tool_use' || (block === null || block === void 0 ? void 0 : block.type) === 'server_tool_use') {
273
+ const toolName = typeof block.name === 'string' && block.name ? block.name : 'tool';
274
+ const toolInput = block.input ? JSON.stringify(block.input) : '';
275
+ return {
276
+ type: 'text',
277
+ text: `[Tool use: ${toolName}${toolInput ? ` ${toolInput}` : ''}]`,
278
+ };
279
+ }
280
+ if ((block === null || block === void 0 ? void 0 : block.type) === 'tool_result') {
281
+ let resultText = '';
282
+ if (typeof block.content === 'string') {
283
+ resultText = block.content;
284
+ }
285
+ else if (Array.isArray(block.content)) {
286
+ resultText = block.content
287
+ .map((item) => {
288
+ if (typeof item === 'string')
289
+ return item;
290
+ if (typeof (item === null || item === void 0 ? void 0 : item.text) === 'string')
291
+ return item.text;
292
+ return '';
293
+ })
294
+ .filter(Boolean)
295
+ .join('\n');
296
+ }
297
+ return {
298
+ type: 'text',
299
+ text: `[Tool result${resultText ? `: ${resultText}` : ''}]`,
300
+ };
301
+ }
302
+ return block;
303
+ });
304
+ return Object.assign(Object.assign({}, message), { content: flattenedContent });
305
+ });
306
+ }
307
+ /**
308
+ * Normalize Claude compact request payload so the upstream model can only
309
+ * produce a plain-text summary instead of reasoning/tool output.
310
+ */
311
+ function normalizeClaudeCompactRequestBody(body) {
312
+ if (!body || typeof body !== 'object') {
313
+ return body;
314
+ }
315
+ const normalized = Object.assign({}, body);
316
+ delete normalized.thinking;
317
+ delete normalized.tools;
318
+ delete normalized.tool_choice;
319
+ delete normalized.mcp_servers;
320
+ return normalized;
321
+ }
322
+ /**
323
+ * Strip non-text assistant content from Claude compact responses.
324
+ * Claude Code compact expects a plain text summary, so thinking/tool blocks
325
+ * should not be sent back downstream even if the upstream model produced them.
326
+ */
327
+ function stripClaudeCompactResponseContent(response) {
328
+ if (!response || typeof response !== 'object' || !Array.isArray(response.content)) {
329
+ return response;
330
+ }
331
+ const filteredContent = response.content.filter((block) => (block === null || block === void 0 ? void 0 : block.type) === 'text');
332
+ return Object.assign(Object.assign({}, response), { content: filteredContent, stop_reason: response.stop_reason === 'tool_use' ? 'end_turn' : response.stop_reason });
333
+ }
334
+ function countUnpairedClaudeToolUses(messages) {
335
+ if (!Array.isArray(messages) || messages.length === 0)
336
+ return 0;
337
+ let unpairedCount = 0;
338
+ for (let i = 0; i < messages.length; i++) {
339
+ const msg = messages[i];
340
+ if ((msg === null || msg === void 0 ? void 0 : msg.role) !== 'assistant' || !Array.isArray(msg.content))
341
+ continue;
342
+ const toolUseIds = msg.content
343
+ .filter((block) => isToolUseBlock(block))
344
+ .map((block) => block.id);
345
+ if (toolUseIds.length === 0)
346
+ continue;
347
+ const nextMsg = messages[i + 1];
348
+ if (!nextMsg || nextMsg.role !== 'user') {
349
+ unpairedCount += toolUseIds.length;
350
+ continue;
351
+ }
352
+ const nextContent = Array.isArray(nextMsg.content) ? nextMsg.content : [];
353
+ const isPureToolResultMessage = nextContent.length > 0 && nextContent.every((block) => (block === null || block === void 0 ? void 0 : block.type) === 'tool_result' && typeof block.tool_use_id === 'string' && block.tool_use_id);
354
+ if (!isPureToolResultMessage) {
355
+ unpairedCount += toolUseIds.length;
356
+ continue;
357
+ }
358
+ const resultIds = new Set(nextContent.map((block) => block.tool_use_id));
359
+ unpairedCount += toolUseIds.filter((id) => !resultIds.has(id)).length;
360
+ }
361
+ return unpairedCount;
362
+ }
363
+ function summarizeClaudeMessagesForDebug(messages, startIndex, endIndex) {
364
+ if (!Array.isArray(messages) || messages.length === 0)
365
+ return [];
366
+ const start = Math.max(0, startIndex);
367
+ const end = Math.min(messages.length - 1, endIndex);
368
+ const summary = [];
369
+ for (let i = start; i <= end; i++) {
370
+ const msg = messages[i];
371
+ const content = Array.isArray(msg === null || msg === void 0 ? void 0 : msg.content)
372
+ ? msg.content.map((block, blockIndex) => ({
373
+ blockIndex,
374
+ type: block === null || block === void 0 ? void 0 : block.type,
375
+ id: typeof (block === null || block === void 0 ? void 0 : block.id) === 'string' ? block.id : undefined,
376
+ tool_use_id: typeof (block === null || block === void 0 ? void 0 : block.tool_use_id) === 'string' ? block.tool_use_id : undefined,
377
+ name: typeof (block === null || block === void 0 ? void 0 : block.name) === 'string' ? block.name : undefined,
378
+ text: typeof (block === null || block === void 0 ? void 0 : block.text) === 'string' ? block.text.slice(0, 120) : undefined,
379
+ content: typeof (block === null || block === void 0 ? void 0 : block.content) === 'string' ? block.content.slice(0, 120) : undefined,
380
+ }))
381
+ : msg === null || msg === void 0 ? void 0 : msg.content;
382
+ summary.push({
383
+ index: i,
384
+ role: msg === null || msg === void 0 ? void 0 : msg.role,
385
+ content,
386
+ });
387
+ }
388
+ return summary;
389
+ }
390
+ function collectCompactPayloadDebugInfo(body, targetCallId) {
391
+ const messages = Array.isArray(body === null || body === void 0 ? void 0 : body.messages) ? body.messages : [];
392
+ const serialized = (() => {
393
+ try {
394
+ return JSON.stringify(body);
395
+ }
396
+ catch (_a) {
397
+ return '';
398
+ }
399
+ })();
400
+ return {
401
+ messageCount: messages.length,
402
+ hasToolUseLiteral: serialized.includes('"tool_use"'),
403
+ hasToolResultLiteral: serialized.includes('"tool_result"'),
404
+ hasTargetCallId: targetCallId ? serialized.includes(targetCallId) : false,
405
+ window47to49: summarizeClaudeMessagesForDebug(messages, 47, 49),
406
+ };
407
+ }
408
+ function summarizeClaudeMessageContent(content) {
409
+ if (!Array.isArray(content))
410
+ return typeof content;
411
+ return content.map((block, index) => {
412
+ const type = (block === null || block === void 0 ? void 0 : block.type) || 'unknown';
413
+ const id = (block === null || block === void 0 ? void 0 : block.id) ? `#${block.id}` : '';
414
+ const toolUseId = (block === null || block === void 0 ? void 0 : block.tool_use_id) ? `->${block.tool_use_id}` : '';
415
+ const text = typeof (block === null || block === void 0 ? void 0 : block.text) === 'string'
416
+ ? `:${block.text.slice(0, 40).replace(/\s+/g, ' ')}`
417
+ : '';
418
+ return `${index}:${type}${id}${toolUseId}${text}`;
419
+ }).join(' | ');
420
+ }
421
+ function collectClaudeToolUseDiagnostics(messages) {
422
+ if (!Array.isArray(messages) || messages.length === 0)
423
+ return [];
424
+ const diagnostics = [];
425
+ for (let i = 0; i < messages.length; i++) {
426
+ const msg = messages[i];
427
+ if ((msg === null || msg === void 0 ? void 0 : msg.role) !== 'assistant' || !Array.isArray(msg.content))
428
+ continue;
429
+ const toolUseIds = msg.content
430
+ .filter((block) => isToolUseBlock(block))
431
+ .map((block) => block.id);
432
+ if (toolUseIds.length === 0)
433
+ continue;
434
+ const nextMsg = messages[i + 1];
435
+ diagnostics.push(`[${i}] assistant tool_use_ids=${toolUseIds.join(', ')} blocks=${summarizeClaudeMessageContent(msg.content)}`);
436
+ diagnostics.push(`[${i + 1}] ${(nextMsg === null || nextMsg === void 0 ? void 0 : nextMsg.role) || 'missing'} blocks=${summarizeClaudeMessageContent(nextMsg === null || nextMsg === void 0 ? void 0 : nextMsg.content)}`);
437
+ }
438
+ return diagnostics;
439
+ }
440
+ // ============================================================
441
+ // Compaction Prompt
442
+ // ============================================================
443
+ /**
444
+ * The system prompt used to instruct the upstream model to compact a conversation.
445
+ */
446
+ exports.COMPACTION_SYSTEM_PROMPT = `You are a conversation compaction assistant. Your task is to create a comprehensive but concise summary of the following programming conversation, preserving all essential context needed to continue the conversation without any loss of critical information.
447
+
448
+ The summary MUST include:
449
+ 1. **Current Task/Goal**: What the user is trying to accomplish
450
+ 2. **Decisions Made**: Key decisions and their rationale
451
+ 3. **Code State**: Current state of relevant files, including file paths, function names, and recent changes
452
+ 4. **Errors Encountered**: Any errors or issues and their resolution status
453
+ 5. **Pending Items**: Tasks or actions that are still in progress or yet to be done
454
+ 6. **Context Details**: Important variables, configurations, or technical details discussed
455
+
456
+ Output a well-structured summary that can fully replace the original conversation. Be thorough but concise. Use markdown formatting for clarity.`;
457
+ /**
458
+ * Build the full compaction system prompt text.
459
+ */
460
+ function buildCompactionPrompt(instructions) {
461
+ let prompt = exports.COMPACTION_SYSTEM_PROMPT;
462
+ if (instructions) {
463
+ prompt += `\n\nAdditional instructions from the user:\n${instructions}`;
464
+ }
465
+ return prompt;
466
+ }
467
+ // ============================================================
468
+ // Upstream Request / Response
469
+ // ============================================================
470
+ /**
471
+ * Build an upstream request for compaction using the conversion system.
472
+ *
473
+ * Instead of manually constructing requests for each format, this builds a
474
+ * standard Responses API request body and uses `transformRequest()` to convert
475
+ * it to the target upstream format.
476
+ */
477
+ function buildCompactUpstreamRequest(conversationText, instructions, toFormat, model) {
478
+ const systemPrompt = buildCompactionPrompt(instructions);
479
+ // Build a standard Responses API request that represents the compact task
480
+ const responsesBody = {
481
+ model,
482
+ instructions: systemPrompt,
483
+ input: [
484
+ {
485
+ type: 'message',
486
+ role: 'user',
487
+ content: conversationText,
488
+ },
489
+ ],
490
+ max_output_tokens: 8192,
491
+ stream: false,
492
+ };
493
+ const result = (0, index_js_1.transformRequest)({
494
+ fromFormat: 'responses',
495
+ toFormat,
496
+ body: responsesBody,
497
+ });
498
+ return { body: result.body };
499
+ }
500
+ /**
501
+ * Extract the summary text from an upstream response.
502
+ *
503
+ * Uses `transformResponse()` to convert the upstream response back to
504
+ * Responses API format, then extracts the text content.
505
+ */
506
+ function extractSummaryFromResponse(response, fromFormat) {
507
+ if (!response)
508
+ return '';
509
+ // If already in responses format, extract directly
510
+ if (fromFormat === 'responses') {
511
+ return extractSummaryFromResponsesFormat(response);
512
+ }
513
+ // Convert upstream response to responses format, then extract
514
+ const responsesResponse = (0, index_js_1.transformResponse)({
515
+ fromFormat,
516
+ toFormat: 'responses',
517
+ response,
518
+ });
519
+ return extractSummaryFromResponsesFormat(responsesResponse);
520
+ }
521
+ /**
522
+ * Extract summary text from a Responses API format response.
523
+ */
524
+ function extractSummaryFromResponsesFormat(response) {
525
+ if (!response)
526
+ return '';
527
+ // Responses API: output array with message items containing text
528
+ if (Array.isArray(response.output)) {
529
+ return response.output
530
+ .flatMap((item) => {
531
+ if (item.type === 'message' && Array.isArray(item.content)) {
532
+ return item.content
533
+ .filter((block) => block.type === 'output_text' || block.type === 'text')
534
+ .map((block) => block.text || '');
535
+ }
536
+ return [];
537
+ })
538
+ .filter(Boolean)
539
+ .join('\n');
540
+ }
541
+ // Fallback for raw string
542
+ if (typeof response === 'string')
543
+ return response;
544
+ return '';
545
+ }
546
+ // ============================================================
547
+ // Compacted Response Builder
548
+ // ============================================================
549
+ /**
550
+ * Build a CompactedResponse object in the OpenAI Responses API format.
551
+ */
552
+ function buildCompactedResponse(summary, _model, inputTokens = 0, outputTokens = 0) {
553
+ const randHex = () => crypto_1.default.randomUUID().replace(/-/g, '').substring(0, 24);
554
+ return {
555
+ id: `resp_compact_${randHex()}`,
556
+ created_at: Math.floor(Date.now() / 1000),
557
+ object: 'response.compaction',
558
+ output: [
559
+ {
560
+ type: 'message',
561
+ id: `msg_${randHex()}`,
562
+ role: 'assistant',
563
+ content: [
564
+ {
565
+ type: 'output_text',
566
+ text: summary,
567
+ },
568
+ ],
569
+ status: 'completed',
570
+ },
571
+ ],
572
+ usage: {
573
+ input_tokens: inputTokens,
574
+ input_tokens_details: { cached_tokens: 0 },
575
+ output_tokens: outputTokens,
576
+ output_tokens_details: { reasoning_tokens: 0 },
577
+ total_tokens: inputTokens + outputTokens,
578
+ },
579
+ };
580
+ }
581
+ /**
582
+ * 构建 compact 上游请求(统一入口)。
583
+ *
584
+ * - 目标格式是 `responses` → 直接转发原始请求体(passthrough)
585
+ * - 目标格式是其他格式 → 提取对话文本 → 构造压缩提示词 → 转换为目标格式
586
+ *
587
+ * 外部只需传入目标格式和模型名称即可,无需关心内部处理逻辑。
588
+ */
589
+ function prepareCompactRequest(options) {
590
+ const { body, toFormat, model } = options;
591
+ // 目标格式是 responses:直接转发原始请求
592
+ if (toFormat === 'responses') {
593
+ return { body, isPassthrough: true };
594
+ }
595
+ // 其他格式:提取对话文本 → 构造压缩提示词 → 转换
596
+ const conversationText = extractConversationText(body.input);
597
+ const { body: transformedBody } = buildCompactUpstreamRequest(conversationText, body.instructions, toFormat, model);
598
+ return { body: transformedBody, isPassthrough: false };
599
+ }
600
+ /**
601
+ * 处理 compact 上游响应(统一入口)。
602
+ *
603
+ * - `isPassthrough: true` → 直接返回原始响应(上游已经是 Responses compact 格式)
604
+ * - `isPassthrough: false` → 提取摘要 → 构建 Responses API compact 响应
605
+ */
606
+ function processCompactResponse(response, fromFormat, model, isPassthrough, usage) {
607
+ // Passthrough:上游已经是 Responses compact 格式,直接返回
608
+ if (isPassthrough)
609
+ return response;
610
+ // 非 passthrough:提取摘要 → 构建 compact 响应
611
+ const summary = extractSummaryFromResponse(response, fromFormat);
612
+ return buildCompactedResponse(summary, model, usage === null || usage === void 0 ? void 0 : usage.inputTokens, usage === null || usage === void 0 ? void 0 : usage.outputTokens);
613
+ }
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectRequestFormat = detectRequestFormat;
4
+ exports.sourceTypeToFormat = sourceTypeToFormat;
5
+ /**
6
+ * Detect the format of the incoming request based on path and body structure.
7
+ * Enhanced version that distinguishes between Chat Completions and Responses API.
8
+ */
9
+ function detectRequestFormat(path, body) {
10
+ // Path-based detection (highest priority)
11
+ if (path.includes('/v1/messages') || path.includes('/messages')) {
12
+ return 'claude';
13
+ }
14
+ if (path.includes('/v1/chat/completions') || path.includes('/chat/completions')) {
15
+ return 'completions';
16
+ }
17
+ // Responses API: /v1/responses but NOT /v1/responses/compact
18
+ if (path.includes('/v1/responses') || path.includes('/responses')) {
19
+ if (!path.includes('/responses/compact') && !path.includes('/responses/compact')) {
20
+ return 'responses';
21
+ }
22
+ }
23
+ // Body-based detection
24
+ if (body) {
25
+ // Claude Messages format: messages array with content blocks
26
+ if (body.messages && Array.isArray(body.messages)) {
27
+ const firstMsg = body.messages[0];
28
+ if (firstMsg && typeof firstMsg.content === 'object' && Array.isArray(firstMsg.content)) {
29
+ return 'claude';
30
+ }
31
+ }
32
+ // Responses API: has 'input' field (string or array) and optionally 'instructions'
33
+ if (body.input !== undefined && body.model) {
34
+ // Has 'input' field → Responses API
35
+ if (typeof body.input === 'string' || Array.isArray(body.input)) {
36
+ return 'responses';
37
+ }
38
+ }
39
+ // OpenAI Chat Completions: has 'messages' + 'model'
40
+ if (body.messages && body.model) {
41
+ return 'completions';
42
+ }
43
+ // Fallback: if has 'input' without 'messages', assume responses
44
+ if (body.input !== undefined && !body.messages) {
45
+ return 'responses';
46
+ }
47
+ }
48
+ // Default to completions
49
+ return 'completions';
50
+ }
51
+ /**
52
+ * Determine the upstream format from a SourceType string.
53
+ * Maps the legacy SourceType values to the new Format type.
54
+ */
55
+ function sourceTypeToFormat(sourceType) {
56
+ switch (sourceType) {
57
+ case 'claude':
58
+ case 'claude-chat':
59
+ return 'claude';
60
+ case 'openai':
61
+ return 'responses';
62
+ case 'openai-chat':
63
+ return 'completions';
64
+ case 'gemini':
65
+ case 'gemini-chat':
66
+ return 'gemini';
67
+ default:
68
+ return 'completions';
69
+ }
70
+ }