@hileeon/mcc 0.1.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 (138) hide show
  1. package/.claude/CLAUDE.md +204 -0
  2. package/.claude/agents/.gitkeep +0 -0
  3. package/.claude/settings.json +9 -0
  4. package/.claude/skills/.gitkeep +0 -0
  5. package/README.md +127 -0
  6. package/dist/accounts/instance-manager.d.ts +11 -0
  7. package/dist/accounts/instance-manager.d.ts.map +1 -0
  8. package/dist/accounts/instance-manager.js +89 -0
  9. package/dist/accounts/instance-manager.js.map +1 -0
  10. package/dist/accounts/shared-manager.d.ts +25 -0
  11. package/dist/accounts/shared-manager.d.ts.map +1 -0
  12. package/dist/accounts/shared-manager.js +186 -0
  13. package/dist/accounts/shared-manager.js.map +1 -0
  14. package/dist/accounts/store.d.ts +30 -0
  15. package/dist/accounts/store.d.ts.map +1 -0
  16. package/dist/accounts/store.js +128 -0
  17. package/dist/accounts/store.js.map +1 -0
  18. package/dist/core/model-router.d.ts +30 -0
  19. package/dist/core/model-router.d.ts.map +1 -0
  20. package/dist/core/model-router.js +64 -0
  21. package/dist/core/model-router.js.map +1 -0
  22. package/dist/dashboard-server.d.ts +5 -0
  23. package/dist/dashboard-server.d.ts.map +1 -0
  24. package/dist/dashboard-server.js +387 -0
  25. package/dist/dashboard-server.js.map +1 -0
  26. package/dist/mcc.d.ts +8 -0
  27. package/dist/mcc.d.ts.map +1 -0
  28. package/dist/mcc.js +474 -0
  29. package/dist/mcc.js.map +1 -0
  30. package/dist/mcp/external-registry.d.ts +24 -0
  31. package/dist/mcp/external-registry.d.ts.map +1 -0
  32. package/dist/mcp/external-registry.js +99 -0
  33. package/dist/mcp/external-registry.js.map +1 -0
  34. package/dist/mcp/installer.d.ts +31 -0
  35. package/dist/mcp/installer.d.ts.map +1 -0
  36. package/dist/mcp/installer.js +273 -0
  37. package/dist/mcp/installer.js.map +1 -0
  38. package/dist/mcp/mcp-config.d.ts +86 -0
  39. package/dist/mcp/mcp-config.d.ts.map +1 -0
  40. package/dist/mcp/mcp-config.js +178 -0
  41. package/dist/mcp/mcp-config.js.map +1 -0
  42. package/dist/mcp/registry.d.ts +23 -0
  43. package/dist/mcp/registry.d.ts.map +1 -0
  44. package/dist/mcp/registry.js +100 -0
  45. package/dist/mcp/registry.js.map +1 -0
  46. package/dist/proxy/proxy-daemon.d.ts +27 -0
  47. package/dist/proxy/proxy-daemon.d.ts.map +1 -0
  48. package/dist/proxy/proxy-daemon.js +192 -0
  49. package/dist/proxy/proxy-daemon.js.map +1 -0
  50. package/dist/proxy/proxy-entry.d.ts +11 -0
  51. package/dist/proxy/proxy-entry.d.ts.map +1 -0
  52. package/dist/proxy/proxy-entry.js +74 -0
  53. package/dist/proxy/proxy-entry.js.map +1 -0
  54. package/dist/proxy/proxy-paths.d.ts +27 -0
  55. package/dist/proxy/proxy-paths.d.ts.map +1 -0
  56. package/dist/proxy/proxy-paths.js +125 -0
  57. package/dist/proxy/proxy-paths.js.map +1 -0
  58. package/dist/proxy/proxy-server.d.ts +20 -0
  59. package/dist/proxy/proxy-server.d.ts.map +1 -0
  60. package/dist/proxy/proxy-server.js +280 -0
  61. package/dist/proxy/proxy-server.js.map +1 -0
  62. package/dist/proxy/upstream-url.d.ts +7 -0
  63. package/dist/proxy/upstream-url.d.ts.map +1 -0
  64. package/dist/proxy/upstream-url.js +38 -0
  65. package/dist/proxy/upstream-url.js.map +1 -0
  66. package/dist/shared/logger.d.ts +23 -0
  67. package/dist/shared/logger.d.ts.map +1 -0
  68. package/dist/shared/logger.js +184 -0
  69. package/dist/shared/logger.js.map +1 -0
  70. package/dist/shared/provider-preset-catalog.d.ts +41 -0
  71. package/dist/shared/provider-preset-catalog.d.ts.map +1 -0
  72. package/dist/shared/provider-preset-catalog.js +299 -0
  73. package/dist/shared/provider-preset-catalog.js.map +1 -0
  74. package/docs/decisions.md +33 -0
  75. package/docs/lessons.md +8 -0
  76. package/docs/product.md +37 -0
  77. package/lib/mcp/mcc-image-analysis-server.cjs +454 -0
  78. package/lib/mcp/mcc-websearch-server.cjs +339 -0
  79. package/lib/mcp-hooks/image-analysis-runtime.cjs +510 -0
  80. package/lib/mcp-hooks/image-analyzer-transformer.cjs +526 -0
  81. package/lib/mcp-hooks/websearch-transformer.cjs +1421 -0
  82. package/lib/proxy/config/config-loader-facade.js +24 -0
  83. package/lib/proxy/glmt/delta-accumulator.js +363 -0
  84. package/lib/proxy/glmt/glmt-transformer.js +204 -0
  85. package/lib/proxy/glmt/index.js +41 -0
  86. package/lib/proxy/glmt/locale-enforcer.js +69 -0
  87. package/lib/proxy/glmt/pipeline/content-transformer.js +162 -0
  88. package/lib/proxy/glmt/pipeline/index.js +20 -0
  89. package/lib/proxy/glmt/pipeline/request-transformer.js +116 -0
  90. package/lib/proxy/glmt/pipeline/response-builder.js +205 -0
  91. package/lib/proxy/glmt/pipeline/stream-parser.js +234 -0
  92. package/lib/proxy/glmt/pipeline/tool-call-handler.js +78 -0
  93. package/lib/proxy/glmt/pipeline/types.js +6 -0
  94. package/lib/proxy/glmt/reasoning-enforcer.js +151 -0
  95. package/lib/proxy/glmt/sse-parser.js +102 -0
  96. package/lib/proxy/services/logging.js +13 -0
  97. package/lib/proxy/transformers/request-transformer.js +452 -0
  98. package/lib/proxy/transformers/sse-stream-transformer.js +199 -0
  99. package/lib/shared/logger.cjs +138 -0
  100. package/package.json +35 -0
  101. package/src/accounts/instance-manager.ts +58 -0
  102. package/src/accounts/shared-manager.ts +154 -0
  103. package/src/accounts/store.ts +111 -0
  104. package/src/core/model-router.ts +82 -0
  105. package/src/dashboard-server.ts +407 -0
  106. package/src/mcc.ts +474 -0
  107. package/src/mcp/external-registry.ts +73 -0
  108. package/src/mcp/installer.ts +258 -0
  109. package/src/mcp/mcp-config.ts +168 -0
  110. package/src/mcp/registry.ts +89 -0
  111. package/src/proxy/proxy-daemon.ts +184 -0
  112. package/src/proxy/proxy-entry.ts +63 -0
  113. package/src/proxy/proxy-paths.ts +97 -0
  114. package/src/proxy/proxy-server.ts +278 -0
  115. package/src/proxy/upstream-url.ts +38 -0
  116. package/src/shared/logger.ts +140 -0
  117. package/src/shared/provider-preset-catalog.ts +340 -0
  118. package/tsconfig.json +33 -0
  119. package/ui/.prettierrc +9 -0
  120. package/ui/index.html +12 -0
  121. package/ui/package.json +33 -0
  122. package/ui/postcss.config.js +6 -0
  123. package/ui/src/App.tsx +753 -0
  124. package/ui/src/components/ui/button.tsx +48 -0
  125. package/ui/src/components/ui/card.tsx +50 -0
  126. package/ui/src/components/ui/input.tsx +21 -0
  127. package/ui/src/components/ui/label.tsx +20 -0
  128. package/ui/src/components/ui/select.tsx +80 -0
  129. package/ui/src/components/ui/switch.tsx +26 -0
  130. package/ui/src/components/ui/tabs.tsx +52 -0
  131. package/ui/src/index.css +33 -0
  132. package/ui/src/lib/api.ts +185 -0
  133. package/ui/src/lib/utils.ts +6 -0
  134. package/ui/src/main.tsx +10 -0
  135. package/ui/src/vite-env.d.ts +1 -0
  136. package/ui/tailwind.config.js +49 -0
  137. package/ui/tsconfig.json +25 -0
  138. package/ui/vite.config.ts +20 -0
@@ -0,0 +1,452 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProxyRequestTransformer = void 0;
4
+ const TOOL_USE_ARGUMENTS_FALLBACK = '{}';
5
+ function assertObject(value, label) {
6
+ if (typeof value !== 'object' || value === null) {
7
+ throw new Error(`${label} must be an object`);
8
+ }
9
+ return value;
10
+ }
11
+ function asNumber(value) {
12
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
13
+ }
14
+ function asStringArray(value) {
15
+ if (!Array.isArray(value)) {
16
+ return undefined;
17
+ }
18
+ const result = value.filter((entry) => typeof entry === 'string' && entry.length > 0);
19
+ return result.length > 0 ? result : undefined;
20
+ }
21
+ function asMetadata(value) {
22
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
23
+ ? value
24
+ : undefined;
25
+ }
26
+ function safeJsonStringify(value, fallback) {
27
+ try {
28
+ const serialized = JSON.stringify(value);
29
+ return typeof serialized === 'string' ? serialized : fallback;
30
+ }
31
+ catch {
32
+ return fallback;
33
+ }
34
+ }
35
+ function flattenTextContent(content, label) {
36
+ if (typeof content === 'string') {
37
+ return content;
38
+ }
39
+ if (!Array.isArray(content)) {
40
+ throw new Error(`${label} must be a string or content block array`);
41
+ }
42
+ return content
43
+ .map((block, index) => {
44
+ const parsed = assertObject(block, `${label}[${index}]`);
45
+ if (parsed.type !== 'text') {
46
+ throw new Error(`${label}[${index}].type "${String(parsed.type)}" is not supported`);
47
+ }
48
+ return typeof parsed.text === 'string' ? parsed.text : '';
49
+ })
50
+ .join('\n');
51
+ }
52
+ /**
53
+ * Convert tool_result content to OpenAI-compatible format.
54
+ * Handles strings, arrays with text/image blocks, and error prefixing.
55
+ * Ported from openclaude's convertToolResultContent.
56
+ */
57
+ function convertToolResultContent(content, isError, label) {
58
+ if (content === undefined) {
59
+ return '';
60
+ }
61
+ if (typeof content === 'string') {
62
+ return isError ? `Error: ${content}` : content;
63
+ }
64
+ if (!Array.isArray(content)) {
65
+ const text = safeJsonStringify(content, '[unserializable content]');
66
+ return isError ? `Error: ${text}` : text;
67
+ }
68
+ const parts = [];
69
+ for (const [index, block] of content.entries()) {
70
+ const parsed = assertObject(block, `${label}[${index}]`);
71
+ if (parsed.type === 'text' && typeof parsed.text === 'string') {
72
+ parts.push(parsed.text);
73
+ continue;
74
+ }
75
+ if (parsed.type === 'image') {
76
+ const source = typeof parsed.source === 'object' && parsed.source !== null
77
+ ? parsed.source
78
+ : undefined;
79
+ const description = source?.type === 'url'
80
+ ? 'url image payload'
81
+ : source?.type === 'base64' && typeof source.media_type === 'string'
82
+ ? `${source.media_type} base64 payload`
83
+ : 'unsupported image payload';
84
+ parts.push(`[tool_result image omitted: ${description}]`);
85
+ continue;
86
+ }
87
+ if (typeof parsed.text === 'string') {
88
+ parts.push(parsed.text);
89
+ continue;
90
+ }
91
+ throw new Error(`${label}[${index}].type "${String(parsed.type)}" is not supported`);
92
+ }
93
+ const text = parts.join('\n');
94
+ if (!text) {
95
+ return isError ? 'Error:' : '';
96
+ }
97
+ return isError ? `Error: ${text}` : text;
98
+ }
99
+ function createFallbackToolId(messageIndex, blockIndex) {
100
+ return `toolu_proxy_fallback_${messageIndex}_${blockIndex}`;
101
+ }
102
+ function toImagePart(block, label) {
103
+ const source = block.source;
104
+ if (!source) {
105
+ throw new Error(`${label}.source is missing`);
106
+ }
107
+ if (source.type === 'url' && source.url) {
108
+ return {
109
+ type: 'image_url',
110
+ image_url: { url: source.url },
111
+ };
112
+ }
113
+ if (source.type === 'base64' && source.media_type && source.data) {
114
+ return {
115
+ type: 'image_url',
116
+ image_url: {
117
+ url: `data:${source.media_type};base64,${source.data}`,
118
+ },
119
+ };
120
+ }
121
+ throw new Error(`${label}.source must be a base64 or url image payload`);
122
+ }
123
+ function isImageBlock(block) {
124
+ return block.type === 'image';
125
+ }
126
+ function isToolUseBlock(block) {
127
+ return block.type === 'tool_use';
128
+ }
129
+ function isToolResultBlock(block) {
130
+ return block.type === 'tool_result';
131
+ }
132
+ function flushUserContent(messages, parts) {
133
+ if (parts.length === 0) {
134
+ return;
135
+ }
136
+ const onlyText = parts.every((part) => part.type === 'text');
137
+ messages.push({
138
+ role: 'user',
139
+ content: onlyText ? parts.map((part) => part.text).join('\n') : [...parts],
140
+ });
141
+ parts.length = 0;
142
+ }
143
+ function transformTools(value) {
144
+ if (!Array.isArray(value)) {
145
+ return undefined;
146
+ }
147
+ const tools = value
148
+ .filter((entry) => typeof entry === 'object' && entry !== null)
149
+ .map((entry) => {
150
+ const rawSchema = typeof entry.input_schema === 'object' && entry.input_schema !== null
151
+ ? entry.input_schema
152
+ : { type: 'object', properties: {} };
153
+ return {
154
+ type: 'function',
155
+ function: {
156
+ name: typeof entry.name === 'string' ? entry.name : 'tool',
157
+ ...(typeof entry.description === 'string' ? { description: entry.description } : {}),
158
+ parameters: rawSchema,
159
+ },
160
+ };
161
+ });
162
+ return tools.length > 0 ? tools : undefined;
163
+ }
164
+ function transformToolChoice(value, hasTools) {
165
+ if (!value) {
166
+ return hasTools ? { tool_choice: 'auto' } : {};
167
+ }
168
+ if (!hasTools) {
169
+ throw new Error('tool_choice requires tools');
170
+ }
171
+ const parallelToolCalls = value.disable_parallel_tool_use === true ? { parallel_tool_calls: false } : {};
172
+ switch (value.type) {
173
+ case undefined:
174
+ case 'auto':
175
+ return { tool_choice: 'auto', ...parallelToolCalls };
176
+ case 'none':
177
+ return { tool_choice: 'none' };
178
+ case 'any':
179
+ return { tool_choice: 'required', ...parallelToolCalls };
180
+ case 'tool':
181
+ if (typeof value.name !== 'string' || value.name.trim().length === 0) {
182
+ throw new Error('tool_choice.name must be a non-empty string when type is "tool"');
183
+ }
184
+ return {
185
+ tool_choice: {
186
+ type: 'function',
187
+ function: { name: value.name.trim() },
188
+ },
189
+ ...parallelToolCalls,
190
+ };
191
+ default:
192
+ throw new Error('tool_choice.type must be "auto", "any", "tool", or "none"');
193
+ }
194
+ }
195
+ function mapThinkingToReasoning(thinking, outputConfig) {
196
+ if (!thinking || thinking.type === 'disabled') {
197
+ return {};
198
+ }
199
+ if (thinking.type === 'adaptive') {
200
+ const effort = toOpenAIEffort(resolveOutputConfigEffort(outputConfig) ?? 'high');
201
+ return {
202
+ reasoning_effort: effort,
203
+ };
204
+ }
205
+ if (thinking.type !== 'enabled') {
206
+ throw new Error('thinking.type must be "enabled", "adaptive", or "disabled"');
207
+ }
208
+ const effort = typeof thinking.budget_tokens === 'number' && thinking.budget_tokens >= 8192
209
+ ? 'high'
210
+ : 'medium';
211
+ return {
212
+ reasoning_effort: effort,
213
+ };
214
+ }
215
+ const VALID_EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
216
+ function resolveOutputConfigEffort(outputConfig) {
217
+ if (!outputConfig || typeof outputConfig.effort !== 'string') {
218
+ return undefined;
219
+ }
220
+ const normalized = outputConfig.effort.trim().toLowerCase();
221
+ return VALID_EFFORT_LEVELS.has(normalized) ? normalized : undefined;
222
+ }
223
+ /**
224
+ * Map Anthropic effort levels to OpenAI-compatible reasoning_effort.
225
+ * Anthropic's `max` has no standard OpenAI equivalent — most providers
226
+ * only accept low/medium/high and reject unknown values with a 400.
227
+ * Ported from openclaude's standardEffortToOpenAI() which maps max -> xhigh
228
+ * for Codex; for generic OpenAI-compat providers we clamp to high.
229
+ */
230
+ function toOpenAIEffort(effort) {
231
+ return effort === 'max' || effort === 'xhigh' ? 'high' : effort;
232
+ }
233
+ function transformMessages(messagesValue) {
234
+ if (!Array.isArray(messagesValue)) {
235
+ throw new Error('messages must be an array');
236
+ }
237
+ const translatedMessages = [];
238
+ let pendingToolUseIds = null;
239
+ let hasPendingToolUseIds = false;
240
+ messagesValue.forEach((message, messageIndex) => {
241
+ const parsedMessage = assertObject(message, `messages[${messageIndex}]`);
242
+ const role = parsedMessage.role;
243
+ if (role !== 'user' && role !== 'assistant') {
244
+ throw new Error(`messages[${messageIndex}].role must be "user" or "assistant"`);
245
+ }
246
+ if (pendingToolUseIds && pendingToolUseIds.size > 0 && role !== 'user') {
247
+ throw new Error(`messages[${messageIndex}].role must be "user" with tool_result blocks after assistant tool_use`);
248
+ }
249
+ const content = parsedMessage.content;
250
+ if (typeof content === 'string') {
251
+ if (pendingToolUseIds && pendingToolUseIds.size > 0) {
252
+ throw new Error(`messages[${messageIndex}].content must start with tool_result blocks for pending tool_use ids`);
253
+ }
254
+ translatedMessages.push({ role, content });
255
+ return;
256
+ }
257
+ if (!Array.isArray(content)) {
258
+ throw new Error(`messages[${messageIndex}].content must be a string or array`);
259
+ }
260
+ if (role === 'user') {
261
+ const userParts = [];
262
+ const followUpParts = [];
263
+ const resolvedToolUseIds = new Set();
264
+ const handleUserPart = (part, blockIndex, kind) => {
265
+ if (!pendingToolUseIds || pendingToolUseIds.size === 0) {
266
+ userParts.push(part);
267
+ return;
268
+ }
269
+ if (resolvedToolUseIds.size === 0) {
270
+ throw new Error(`messages[${messageIndex}].content[${blockIndex}] ${kind} is not allowed before tool_result blocks for pending tool_use ids`);
271
+ }
272
+ if (resolvedToolUseIds.size !== pendingToolUseIds.size) {
273
+ throw new Error(`messages[${messageIndex}].content[${blockIndex}] ${kind} is not allowed between tool_result blocks for pending tool_use ids`);
274
+ }
275
+ followUpParts.push(part);
276
+ };
277
+ content.forEach((block, blockIndex) => {
278
+ const parsed = assertObject(block, `messages[${messageIndex}].content[${blockIndex}]`);
279
+ if (parsed.type === 'thinking' || parsed.type === 'redacted_thinking') {
280
+ return;
281
+ }
282
+ if (parsed.type === 'text') {
283
+ const text = typeof parsed.text === 'string' ? parsed.text : '';
284
+ handleUserPart({ type: 'text', text }, blockIndex, 'text');
285
+ return;
286
+ }
287
+ if (isImageBlock(parsed)) {
288
+ handleUserPart(toImagePart(parsed, `messages[${messageIndex}].content[${blockIndex}]`), blockIndex, 'image');
289
+ return;
290
+ }
291
+ if (isToolResultBlock(parsed)) {
292
+ if (!pendingToolUseIds || pendingToolUseIds.size === 0) {
293
+ throw new Error(`messages[${messageIndex}].content[${blockIndex}] tool_result requires a preceding assistant tool_use`);
294
+ }
295
+ if (typeof parsed.tool_use_id !== 'string' || parsed.tool_use_id.trim().length === 0) {
296
+ throw new Error(`messages[${messageIndex}].content[${blockIndex}].tool_use_id must be a non-empty string`);
297
+ }
298
+ if (!pendingToolUseIds.has(parsed.tool_use_id)) {
299
+ throw new Error(`messages[${messageIndex}].content[${blockIndex}].tool_use_id "${parsed.tool_use_id}" does not match a pending tool_use`);
300
+ }
301
+ if (resolvedToolUseIds.has(parsed.tool_use_id)) {
302
+ throw new Error(`messages[${messageIndex}].content[${blockIndex}].tool_use_id "${parsed.tool_use_id}" is duplicated`);
303
+ }
304
+ resolvedToolUseIds.add(parsed.tool_use_id);
305
+ translatedMessages.push({
306
+ role: 'tool',
307
+ tool_call_id: parsed.tool_use_id,
308
+ content: convertToolResultContent(parsed.content, parsed.is_error === true, `messages[${messageIndex}].content[${blockIndex}].content`),
309
+ });
310
+ return;
311
+ }
312
+ if (isToolUseBlock(parsed)) {
313
+ throw new Error(`messages[${messageIndex}].content[${blockIndex}] tool_use requires assistant role`);
314
+ }
315
+ throw new Error(`messages[${messageIndex}].content[${blockIndex}].type "${String(parsed.type)}" is not supported`);
316
+ });
317
+ if (resolvedToolUseIds.size > 0) {
318
+ if (resolvedToolUseIds.size !== pendingToolUseIds?.size) {
319
+ throw new Error(`messages[${messageIndex}].content must provide tool_result blocks for all pending tool_use ids`);
320
+ }
321
+ pendingToolUseIds = null;
322
+ hasPendingToolUseIds = false;
323
+ }
324
+ if (pendingToolUseIds && pendingToolUseIds.size > 0) {
325
+ throw new Error(`messages[${messageIndex}].content must include tool_result blocks for pending tool_use ids`);
326
+ }
327
+ if (userParts.length > 0) {
328
+ flushUserContent(translatedMessages, userParts);
329
+ }
330
+ if (followUpParts.length > 0) {
331
+ flushUserContent(translatedMessages, followUpParts);
332
+ }
333
+ return;
334
+ }
335
+ // Assistant role
336
+ const assistantTextParts = [];
337
+ const toolCalls = [];
338
+ content.forEach((block, blockIndex) => {
339
+ const parsed = assertObject(block, `messages[${messageIndex}].content[${blockIndex}]`);
340
+ if (parsed.type === 'thinking' || parsed.type === 'redacted_thinking') {
341
+ return;
342
+ }
343
+ if (parsed.type === 'text') {
344
+ const text = typeof parsed.text === 'string' ? parsed.text : '';
345
+ assistantTextParts.push(text);
346
+ return;
347
+ }
348
+ if (isToolUseBlock(parsed)) {
349
+ toolCalls.push({
350
+ id: typeof parsed.id === 'string' && parsed.id.length > 0
351
+ ? parsed.id
352
+ : createFallbackToolId(messageIndex, blockIndex),
353
+ type: 'function',
354
+ function: {
355
+ name: typeof parsed.name === 'string' ? parsed.name : 'tool',
356
+ arguments: safeJsonStringify(parsed.input ?? {}, TOOL_USE_ARGUMENTS_FALLBACK),
357
+ },
358
+ });
359
+ return;
360
+ }
361
+ if (isImageBlock(parsed)) {
362
+ throw new Error(`messages[${messageIndex}].content[${blockIndex}] image requires user role`);
363
+ }
364
+ if (isToolResultBlock(parsed)) {
365
+ throw new Error(`messages[${messageIndex}].content[${blockIndex}] tool_result requires user role`);
366
+ }
367
+ throw new Error(`messages[${messageIndex}].content[${blockIndex}].type "${String(parsed.type)}" is not supported`);
368
+ });
369
+ if (assistantTextParts.length === 0 && toolCalls.length === 0) {
370
+ return;
371
+ }
372
+ pendingToolUseIds =
373
+ toolCalls.length > 0 ? new Set(toolCalls.map((toolCall) => toolCall.id)) : null;
374
+ hasPendingToolUseIds = toolCalls.length > 0;
375
+ translatedMessages.push({
376
+ role: 'assistant',
377
+ content: assistantTextParts.join('\n'),
378
+ tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
379
+ });
380
+ });
381
+ if (hasPendingToolUseIds) {
382
+ throw new Error('messages must provide tool_result blocks for the latest assistant tool_use');
383
+ }
384
+ return translatedMessages;
385
+ }
386
+ /**
387
+ * Coalesce consecutive messages of the same role.
388
+ * OpenAI/vLLM/Ollama/Mistral require strict user<->assistant alternation.
389
+ * Multiple consecutive tool messages are allowed (assistant -> tool* -> user).
390
+ * Ported from openclaude's coalescing pass.
391
+ */
392
+ function coalesceMessages(messages) {
393
+ const coalesced = [];
394
+ for (const msg of messages) {
395
+ const prev = coalesced[coalesced.length - 1];
396
+ if (prev && prev.role === msg.role && msg.role !== 'tool' && msg.role !== 'system') {
397
+ const prevContent = prev.content;
398
+ const curContent = msg.content;
399
+ if (typeof prevContent === 'string' && typeof curContent === 'string') {
400
+ prev.content = prevContent + (prevContent && curContent ? '\n' : '') + curContent;
401
+ }
402
+ else {
403
+ const toArray = (c) => {
404
+ if (!c)
405
+ return [];
406
+ if (typeof c === 'string')
407
+ return c ? [{ type: 'text', text: c }] : [];
408
+ return c;
409
+ };
410
+ prev.content = [...toArray(prevContent), ...toArray(curContent)];
411
+ }
412
+ if (msg.tool_calls?.length) {
413
+ prev.tool_calls = [...(prev.tool_calls ?? []), ...msg.tool_calls];
414
+ }
415
+ }
416
+ else {
417
+ coalesced.push({ ...msg });
418
+ }
419
+ }
420
+ return coalesced;
421
+ }
422
+ class ProxyRequestTransformer {
423
+ transform(raw) {
424
+ const source = assertObject(raw || {}, 'request');
425
+ const tools = transformTools(source.tools);
426
+ const messages = transformMessages(source.messages);
427
+ const system = source.system;
428
+ const allMessages = system !== undefined
429
+ ? [
430
+ { role: 'system', content: flattenTextContent(system, 'system') },
431
+ ...messages,
432
+ ]
433
+ : messages;
434
+ return {
435
+ model: typeof source.model === 'string' && source.model.trim().length > 0
436
+ ? source.model.trim()
437
+ : undefined,
438
+ stream: source.stream === true,
439
+ messages: coalesceMessages(allMessages),
440
+ max_tokens: asNumber(source.max_tokens),
441
+ temperature: asNumber(source.temperature),
442
+ top_p: asNumber(source.top_p),
443
+ stop: asStringArray(source.stop_sequences),
444
+ metadata: asMetadata(source.metadata),
445
+ tools,
446
+ ...transformToolChoice(source.tool_choice, tools !== undefined),
447
+ ...mapThinkingToReasoning(source.thinking, source.output_config),
448
+ };
449
+ }
450
+ }
451
+ exports.ProxyRequestTransformer = ProxyRequestTransformer;
452
+ //# sourceMappingURL=request-transformer.js.map
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProxySseStreamTransformer = exports.createAnthropicProxyResponse = exports.createAnthropicErrorResponse = void 0;
4
+ const delta_accumulator_1 = require("../glmt/delta-accumulator");
5
+ const glmt_transformer_1 = require("../glmt/glmt-transformer");
6
+ const sse_parser_1 = require("../glmt/sse-parser");
7
+ const JSON_TRANSLATION_ERROR_MESSAGE = 'Failed to translate OpenAI-compatible JSON response';
8
+ const STREAM_TRANSLATION_ERROR_MESSAGE = 'Failed to translate OpenAI-compatible SSE response';
9
+ function createAnthropicErrorPayload(type, message) {
10
+ return {
11
+ type: 'error',
12
+ error: {
13
+ type,
14
+ message,
15
+ },
16
+ };
17
+ }
18
+ function formatErrorForLog(error) {
19
+ if (error instanceof Error) {
20
+ return error.message;
21
+ }
22
+ try {
23
+ return JSON.stringify(error);
24
+ }
25
+ catch {
26
+ return String(error);
27
+ }
28
+ }
29
+ function logTranslationError(context, error) {
30
+ console.error(`[proxy-sse-transformer] ${context}: ${formatErrorForLog(error)}`);
31
+ }
32
+ function createAnthropicErrorResponse(status, type, message, headers) {
33
+ const responseHeaders = new Headers(headers);
34
+ responseHeaders.set('Content-Type', 'application/json');
35
+ responseHeaders.delete('Content-Length');
36
+ return new Response(JSON.stringify(createAnthropicErrorPayload(type, message)), {
37
+ status,
38
+ headers: responseHeaders,
39
+ });
40
+ }
41
+ exports.createAnthropicErrorResponse = createAnthropicErrorResponse;
42
+ function formatSseEvent(event, data) {
43
+ return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
44
+ }
45
+ function hasTranslatableChoices(value) {
46
+ if (typeof value !== 'object' || value === null) {
47
+ return false;
48
+ }
49
+ const { choices } = value;
50
+ if (!Array.isArray(choices) || choices.length === 0) {
51
+ return false;
52
+ }
53
+ const firstChoice = choices[0];
54
+ if (typeof firstChoice !== 'object' || firstChoice === null) {
55
+ return false;
56
+ }
57
+ const message = firstChoice.message;
58
+ return typeof message === 'object' && message !== null;
59
+ }
60
+ function isSyntheticTransformationFallback(value) {
61
+ return (typeof value === 'object' &&
62
+ value !== null &&
63
+ typeof value.id === 'string' &&
64
+ value.id.startsWith('msg_error_'));
65
+ }
66
+ async function createAnthropicErrorProxyResponse(response) {
67
+ const headers = new Headers(response.headers);
68
+ headers.delete('Content-Type');
69
+ headers.delete('Content-Length');
70
+ let type = response.status === 401
71
+ ? 'authentication_error'
72
+ : response.status === 429
73
+ ? 'rate_limit_error'
74
+ : response.status >= 400 && response.status < 500
75
+ ? 'invalid_request_error'
76
+ : 'api_error';
77
+ let message = `Upstream request failed with status ${response.status}`;
78
+ try {
79
+ const contentType = (response.headers.get('content-type') || '').toLowerCase();
80
+ if (contentType.includes('application/json')) {
81
+ const payload = (await response.json());
82
+ if (typeof payload?.error?.type === 'string' && payload.error.type.trim().length > 0) {
83
+ type = payload.error.type;
84
+ }
85
+ if (typeof payload?.error?.message === 'string' && payload.error.message.trim().length > 0) {
86
+ message = payload.error.message;
87
+ }
88
+ else if (typeof payload?.message === 'string' && payload.message.trim().length > 0) {
89
+ message = payload.message;
90
+ }
91
+ }
92
+ else {
93
+ const text = (await response.text()).trim();
94
+ if (text.length > 0) {
95
+ message = text;
96
+ }
97
+ }
98
+ }
99
+ catch (error) {
100
+ logTranslationError('Failed to parse upstream error response', error);
101
+ }
102
+ return createAnthropicErrorResponse(response.status, type, message, headers);
103
+ }
104
+ async function createAnthropicJsonResponse(response) {
105
+ try {
106
+ const openAIResponse = await response.json();
107
+ if (!hasTranslatableChoices(openAIResponse)) {
108
+ return createAnthropicErrorResponse(502, 'api_error', JSON_TRANSLATION_ERROR_MESSAGE);
109
+ }
110
+ const anthropicResponse = new glmt_transformer_1.GlmtTransformer().transformResponse(openAIResponse);
111
+ if (isSyntheticTransformationFallback(anthropicResponse)) {
112
+ logTranslationError('OpenAI-compatible JSON translation produced synthetic fallback response', anthropicResponse);
113
+ return createAnthropicErrorResponse(502, 'api_error', JSON_TRANSLATION_ERROR_MESSAGE);
114
+ }
115
+ return new Response(JSON.stringify(anthropicResponse), {
116
+ status: response.status,
117
+ headers: { 'Content-Type': 'application/json' },
118
+ });
119
+ }
120
+ catch (error) {
121
+ logTranslationError('OpenAI-compatible JSON translation failed', error);
122
+ return createAnthropicErrorResponse(502, 'api_error', JSON_TRANSLATION_ERROR_MESSAGE);
123
+ }
124
+ }
125
+ function createAnthropicStreamingResponse(response) {
126
+ const body = response.body;
127
+ if (!body) {
128
+ return createAnthropicErrorResponse(502, 'api_error', 'Upstream stream ended before a response body was available');
129
+ }
130
+ const parser = new sse_parser_1.SSEParser({ throwOnMalformedJson: true });
131
+ const transformer = new glmt_transformer_1.GlmtTransformer();
132
+ const accumulator = new delta_accumulator_1.DeltaAccumulator({});
133
+ const encoder = new TextEncoder();
134
+ const readable = new ReadableStream({
135
+ async start(controller) {
136
+ const reader = body.getReader();
137
+ try {
138
+ while (true) {
139
+ const { done, value } = await reader.read();
140
+ if (done) {
141
+ break;
142
+ }
143
+ if (!value) {
144
+ continue;
145
+ }
146
+ const events = parser.parse(Buffer.from(value));
147
+ for (const event of events) {
148
+ const anthropicEvents = transformer.transformDelta(event, accumulator);
149
+ for (const anthropicEvent of anthropicEvents) {
150
+ controller.enqueue(encoder.encode(formatSseEvent(anthropicEvent.event, anthropicEvent.data)));
151
+ }
152
+ }
153
+ }
154
+ if (!accumulator.isFinalized() && accumulator.isMessageStarted()) {
155
+ for (const anthropicEvent of transformer.finalizeDelta(accumulator)) {
156
+ controller.enqueue(encoder.encode(formatSseEvent(anthropicEvent.event, anthropicEvent.data)));
157
+ }
158
+ }
159
+ }
160
+ catch (error) {
161
+ logTranslationError('OpenAI-compatible SSE translation failed', error);
162
+ controller.enqueue(encoder.encode(formatSseEvent('error', createAnthropicErrorPayload('api_error', STREAM_TRANSLATION_ERROR_MESSAGE))));
163
+ }
164
+ finally {
165
+ reader.releaseLock();
166
+ controller.close();
167
+ }
168
+ },
169
+ });
170
+ return new Response(readable, {
171
+ status: response.status,
172
+ headers: {
173
+ 'Content-Type': 'text/event-stream',
174
+ 'Cache-Control': 'no-cache',
175
+ Connection: 'keep-alive',
176
+ },
177
+ });
178
+ }
179
+ async function createAnthropicProxyResponse(response) {
180
+ if (!response.ok) {
181
+ return createAnthropicErrorProxyResponse(response);
182
+ }
183
+ const contentType = (response.headers.get('content-type') || '').toLowerCase();
184
+ const isEventStream = contentType === 'text/event-stream' || contentType.startsWith('text/event-stream;');
185
+ return isEventStream
186
+ ? createAnthropicStreamingResponse(response)
187
+ : createAnthropicJsonResponse(response);
188
+ }
189
+ exports.createAnthropicProxyResponse = createAnthropicProxyResponse;
190
+ class ProxySseStreamTransformer {
191
+ async transform(response) {
192
+ return createAnthropicProxyResponse(response);
193
+ }
194
+ error(status, type, message) {
195
+ return createAnthropicErrorResponse(status, type, message);
196
+ }
197
+ }
198
+ exports.ProxySseStreamTransformer = ProxySseStreamTransformer;
199
+ //# sourceMappingURL=sse-stream-transformer.js.map