condukt 0.25.0 → 0.27.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 (96) hide show
  1. package/dist/runtimes/claude/bridge-cache.d.ts +58 -0
  2. package/dist/runtimes/claude/bridge-cache.d.ts.map +1 -0
  3. package/dist/runtimes/claude/bridge-cache.js +257 -0
  4. package/dist/runtimes/claude/bridge-cache.js.map +1 -0
  5. package/dist/runtimes/claude/claude-runtime.d.ts +30 -0
  6. package/dist/runtimes/claude/claude-runtime.d.ts.map +1 -0
  7. package/dist/runtimes/claude/claude-runtime.js +88 -0
  8. package/dist/runtimes/claude/claude-runtime.js.map +1 -0
  9. package/dist/runtimes/claude/claude-session.d.ts +154 -0
  10. package/dist/runtimes/claude/claude-session.d.ts.map +1 -0
  11. package/dist/runtimes/claude/claude-session.js +851 -0
  12. package/dist/runtimes/claude/claude-session.js.map +1 -0
  13. package/dist/runtimes/claude/config-mapper.d.ts +69 -0
  14. package/dist/runtimes/claude/config-mapper.d.ts.map +1 -0
  15. package/dist/runtimes/claude/config-mapper.js +458 -0
  16. package/dist/runtimes/claude/config-mapper.js.map +1 -0
  17. package/dist/runtimes/claude/epoch-store.d.ts +28 -0
  18. package/dist/runtimes/claude/epoch-store.d.ts.map +1 -0
  19. package/dist/runtimes/claude/epoch-store.js +199 -0
  20. package/dist/runtimes/claude/epoch-store.js.map +1 -0
  21. package/dist/runtimes/claude/fallback-runtime.d.ts +14 -0
  22. package/dist/runtimes/claude/fallback-runtime.d.ts.map +1 -0
  23. package/dist/runtimes/claude/fallback-runtime.js +64 -0
  24. package/dist/runtimes/claude/fallback-runtime.js.map +1 -0
  25. package/dist/runtimes/claude/index.d.ts +24 -0
  26. package/dist/runtimes/claude/index.d.ts.map +1 -0
  27. package/dist/runtimes/claude/index.js +61 -0
  28. package/dist/runtimes/claude/index.js.map +1 -0
  29. package/dist/runtimes/claude/message-mapper.d.ts +135 -0
  30. package/dist/runtimes/claude/message-mapper.d.ts.map +1 -0
  31. package/dist/runtimes/claude/message-mapper.js +504 -0
  32. package/dist/runtimes/claude/message-mapper.js.map +1 -0
  33. package/dist/runtimes/claude/one-shot.d.ts +48 -0
  34. package/dist/runtimes/claude/one-shot.d.ts.map +1 -0
  35. package/dist/runtimes/claude/one-shot.js +186 -0
  36. package/dist/runtimes/claude/one-shot.js.map +1 -0
  37. package/dist/runtimes/claude/protocol-bridge.d.ts +138 -0
  38. package/dist/runtimes/claude/protocol-bridge.d.ts.map +1 -0
  39. package/dist/runtimes/claude/protocol-bridge.js +1170 -0
  40. package/dist/runtimes/claude/protocol-bridge.js.map +1 -0
  41. package/dist/runtimes/claude/protocol-router.d.ts +113 -0
  42. package/dist/runtimes/claude/protocol-router.d.ts.map +1 -0
  43. package/dist/runtimes/claude/protocol-router.js +514 -0
  44. package/dist/runtimes/claude/protocol-router.js.map +1 -0
  45. package/dist/runtimes/claude/session-epoch.d.ts +156 -0
  46. package/dist/runtimes/claude/session-epoch.d.ts.map +1 -0
  47. package/dist/runtimes/claude/session-epoch.js +365 -0
  48. package/dist/runtimes/claude/session-epoch.js.map +1 -0
  49. package/dist/runtimes/claude/session-recovery.d.ts +78 -0
  50. package/dist/runtimes/claude/session-recovery.d.ts.map +1 -0
  51. package/dist/runtimes/claude/session-recovery.js +259 -0
  52. package/dist/runtimes/claude/session-recovery.js.map +1 -0
  53. package/dist/runtimes/claude/sse.d.ts +16 -0
  54. package/dist/runtimes/claude/sse.d.ts.map +1 -0
  55. package/dist/runtimes/claude/sse.js +46 -0
  56. package/dist/runtimes/claude/sse.js.map +1 -0
  57. package/dist/runtimes/claude/translate-chat.d.ts +106 -0
  58. package/dist/runtimes/claude/translate-chat.d.ts.map +1 -0
  59. package/dist/runtimes/claude/translate-chat.js +857 -0
  60. package/dist/runtimes/claude/translate-chat.js.map +1 -0
  61. package/dist/runtimes/claude/translate-responses.d.ts +76 -0
  62. package/dist/runtimes/claude/translate-responses.d.ts.map +1 -0
  63. package/dist/runtimes/claude/translate-responses.js +625 -0
  64. package/dist/runtimes/claude/translate-responses.js.map +1 -0
  65. package/dist/runtimes/claude/usage-ledger.d.ts +108 -0
  66. package/dist/runtimes/claude/usage-ledger.d.ts.map +1 -0
  67. package/dist/runtimes/claude/usage-ledger.js +189 -0
  68. package/dist/runtimes/claude/usage-ledger.js.map +1 -0
  69. package/dist/runtimes/copilot/copilot-backend.d.ts +8 -0
  70. package/dist/runtimes/copilot/copilot-backend.d.ts.map +1 -1
  71. package/dist/runtimes/copilot/sdk-backend.d.ts.map +1 -1
  72. package/dist/runtimes/copilot/sdk-backend.js +314 -135
  73. package/dist/runtimes/copilot/sdk-backend.js.map +1 -1
  74. package/dist/runtimes/mcp-env.d.ts +24 -0
  75. package/dist/runtimes/mcp-env.d.ts.map +1 -0
  76. package/dist/runtimes/mcp-env.js +81 -0
  77. package/dist/runtimes/mcp-env.js.map +1 -0
  78. package/dist/runtimes/mock/mock-runtime.d.ts +2 -2
  79. package/dist/runtimes/mock/mock-runtime.d.ts.map +1 -1
  80. package/dist/runtimes/mock/mock-runtime.js +38 -1
  81. package/dist/runtimes/mock/mock-runtime.js.map +1 -1
  82. package/dist/src/agent.d.ts.map +1 -1
  83. package/dist/src/agent.js +171 -3
  84. package/dist/src/agent.js.map +1 -1
  85. package/dist/src/index.d.ts +1 -1
  86. package/dist/src/index.d.ts.map +1 -1
  87. package/dist/src/index.js.map +1 -1
  88. package/dist/src/session-recovery-policy.d.ts +19 -0
  89. package/dist/src/session-recovery-policy.d.ts.map +1 -0
  90. package/dist/src/session-recovery-policy.js +43 -0
  91. package/dist/src/session-recovery-policy.js.map +1 -0
  92. package/dist/src/types.d.ts +118 -1
  93. package/dist/src/types.d.ts.map +1 -1
  94. package/dist/src/types.js +11 -1
  95. package/dist/src/types.js.map +1 -1
  96. package/package.json +20 -4
@@ -0,0 +1,857 @@
1
+ "use strict";
2
+ /**
3
+ * Anthropic ⇄ OpenAI chat/completions translation (reference-proxy parity).
4
+ *
5
+ * Pure, side-effect-free functions plus a streaming state machine. The
6
+ * bridge owns HTTP concerns; this module owns wire-shape semantics:
7
+ *
8
+ * - anthropicToOpenAI: Claude SDK request → chat/completions body
9
+ * - applyChatExtras: DeepSeek/GLM thinking+effort extras (in place)
10
+ * - openAIToAnthropic: non-streaming response → Anthropic message
11
+ * - ChatStreamTranslator: streaming chunks → Anthropic SSE frames
12
+ * - estimateInputTokens: pre-stream input estimate for message_start
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.extractThinkingType = exports.extractEffort = exports.ChatStreamTranslator = exports.NO_MULTIMODAL = void 0;
16
+ exports.asObject = asObject;
17
+ exports.anthropicToOpenAI = anthropicToOpenAI;
18
+ exports.maxTokensFloorFor = maxTokensFloorFor;
19
+ exports.applyChatExtras = applyChatExtras;
20
+ exports.mapFinishReason = mapFinishReason;
21
+ exports.openAIToAnthropic = openAIToAnthropic;
22
+ exports.parseToolArgs = parseToolArgs;
23
+ exports.estimateInputTokens = estimateInputTokens;
24
+ const node_crypto_1 = require("node:crypto");
25
+ const bridge_cache_1 = require("./bridge-cache");
26
+ const protocol_router_1 = require("./protocol-router");
27
+ Object.defineProperty(exports, "extractEffort", { enumerable: true, get: function () { return protocol_router_1.extractEffort; } });
28
+ Object.defineProperty(exports, "extractThinkingType", { enumerable: true, get: function () { return protocol_router_1.extractThinkingType; } });
29
+ const sse_1 = require("./sse");
30
+ // ── Media support (reference parity) ─────────────────────────────────
31
+ const IMAGE_FORMATS = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif']);
32
+ const DOCUMENT_FORMATS = new Set([
33
+ 'application/pdf',
34
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
35
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
36
+ ]);
37
+ const TEXT_FORMATS = new Set([
38
+ 'text/plain',
39
+ 'text/csv',
40
+ 'text/html',
41
+ 'text/markdown',
42
+ 'application/json',
43
+ 'text/xml',
44
+ ]);
45
+ /** Chat models that only accept string content (media dropped with warning). */
46
+ exports.NO_MULTIMODAL = new Set();
47
+ function dataUrl(mediaType, data) {
48
+ return `data:${mediaType};base64,${data}`;
49
+ }
50
+ /** Convert an Anthropic image/document block to a chat-compatible block. */
51
+ function convertMediaBlock(block) {
52
+ const source = (block['source'] ?? {});
53
+ const mt = String(source['media_type'] ?? '').toLowerCase();
54
+ const sourceType = String(source['type'] ?? '');
55
+ const isUrl = sourceType === 'url';
56
+ const data = typeof source['data'] === 'string' ? source['data'] : '';
57
+ const url = typeof source['url'] === 'string' ? source['url'] : '';
58
+ if (IMAGE_FORMATS.has(mt)) {
59
+ const imageUrl = isUrl ? url : data ? dataUrl(mt, data) : '';
60
+ if (!imageUrl)
61
+ return null;
62
+ return { type: 'image_url', image_url: { url: imageUrl } };
63
+ }
64
+ if (TEXT_FORMATS.has(mt)) {
65
+ if (!isUrl && data) {
66
+ try {
67
+ return { type: 'text', text: Buffer.from(data, 'base64').toString('utf8') };
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
73
+ if (isUrl && url)
74
+ return { type: 'text', text: url };
75
+ return null;
76
+ }
77
+ if (DOCUMENT_FORMATS.has(mt)) {
78
+ const filename = mt === 'application/pdf'
79
+ ? 'document.pdf'
80
+ : mt === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
81
+ ? 'document.docx'
82
+ : 'document.xlsx';
83
+ const fileData = isUrl ? url : data ? dataUrl(mt, data) : '';
84
+ if (!fileData)
85
+ return null;
86
+ return { type: 'file', file: { filename, file_data: fileData } };
87
+ }
88
+ return null;
89
+ }
90
+ function isMediaBlock(block) {
91
+ const t = block['type'];
92
+ return t === 'image' || t === 'document';
93
+ }
94
+ function extractToolResultText(content) {
95
+ if (typeof content === 'string')
96
+ return content;
97
+ if (Array.isArray(content)) {
98
+ return content
99
+ .map((c) => {
100
+ if (typeof c === 'string')
101
+ return c;
102
+ const x = (c ?? {});
103
+ if (typeof x['text'] === 'string')
104
+ return x['text'];
105
+ if (typeof x['content'] === 'string')
106
+ return x['content'];
107
+ try {
108
+ return JSON.stringify(c);
109
+ }
110
+ catch {
111
+ return String(c);
112
+ }
113
+ })
114
+ .join('\n');
115
+ }
116
+ if (content && typeof content === 'object') {
117
+ try {
118
+ return JSON.stringify(content);
119
+ }
120
+ catch {
121
+ return String(content);
122
+ }
123
+ }
124
+ return '';
125
+ }
126
+ // ── Shape narrowing ──────────────────────────────────────────────────
127
+ /**
128
+ * Narrow an untrusted JSON value to an indexable object. Upstream bodies and
129
+ * Anthropic requests are both untrusted, so every nested read goes through a
130
+ * check like this rather than an `any` cast.
131
+ */
132
+ function asObject(value) {
133
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
134
+ ? value
135
+ : null;
136
+ }
137
+ /**
138
+ * Convert an Anthropic messages body to an OpenAI chat/completions body.
139
+ * Ported 1:1 from the reference proxy (system media, thinking replay,
140
+ * tool ordering, tool_choice, thinking budget → max_completion_tokens).
141
+ */
142
+ function anthropicToOpenAI(body, model, opts = {}) {
143
+ const noMulti = opts.noMultimodal ?? exports.NO_MULTIMODAL;
144
+ const thinking = (0, protocol_router_1.thinkingEnabled)(body);
145
+ const messages = [];
146
+ // System prompt.
147
+ const system = body['system'] ?? '';
148
+ if (typeof system === 'string') {
149
+ if (system)
150
+ messages.push({ role: 'system', content: system });
151
+ }
152
+ else if (Array.isArray(system)) {
153
+ const sysBlocks = [];
154
+ for (const block of system) {
155
+ if (!block || typeof block !== 'object')
156
+ continue;
157
+ const b = block;
158
+ const btype = b['type'];
159
+ if (btype === 'text') {
160
+ sysBlocks.push({ type: 'text', text: b['text'] ?? '' });
161
+ }
162
+ else if (isMediaBlock(b)) {
163
+ const converted = convertMediaBlock(b);
164
+ if (converted)
165
+ sysBlocks.push(converted);
166
+ }
167
+ }
168
+ if (sysBlocks.length > 0) {
169
+ const hasMedia = sysBlocks.some((b) => b['type'] !== 'text');
170
+ if (hasMedia && !noMulti.has(model)) {
171
+ messages.push({ role: 'system', content: sysBlocks });
172
+ }
173
+ else {
174
+ const texts = sysBlocks.filter((b) => b['type'] === 'text').map((b) => String(b['text'] ?? ''));
175
+ messages.push({ role: 'system', content: texts.join('\n') });
176
+ }
177
+ }
178
+ }
179
+ const rawMessages = Array.isArray(body['messages']) ? body['messages'] : [];
180
+ for (let mi = 0; mi < rawMessages.length; mi += 1) {
181
+ const msg = rawMessages[mi];
182
+ if (!msg || typeof msg !== 'object')
183
+ continue;
184
+ const role = String(msg['role'] ?? 'user');
185
+ const isAssistant = role === 'assistant';
186
+ const content = msg['content'] ?? '';
187
+ if (typeof content === 'string') {
188
+ const out = { role, content };
189
+ if (thinking && isAssistant)
190
+ out['reasoning_content'] = ' ';
191
+ messages.push(out);
192
+ continue;
193
+ }
194
+ if (!Array.isArray(content))
195
+ continue;
196
+ const textParts = [];
197
+ const contentBlocks = [];
198
+ const toolCalls = [];
199
+ const thinkingParts = [];
200
+ const toolResults = [];
201
+ let hasMedia = false;
202
+ for (const block of content) {
203
+ if (typeof block === 'string') {
204
+ textParts.push(block);
205
+ contentBlocks.push({ type: 'text', text: block });
206
+ continue;
207
+ }
208
+ if (!block || typeof block !== 'object')
209
+ continue;
210
+ const b = block;
211
+ const btype = b['type'];
212
+ if (btype === 'text') {
213
+ const t = String(b['text'] ?? '');
214
+ textParts.push(t);
215
+ contentBlocks.push({ type: 'text', text: t });
216
+ }
217
+ else if (isMediaBlock(b)) {
218
+ const converted = convertMediaBlock(b);
219
+ if (converted) {
220
+ hasMedia = true;
221
+ contentBlocks.push(converted);
222
+ }
223
+ }
224
+ else if (btype === 'thinking') {
225
+ thinkingParts.push(String(b['thinking'] ?? ''));
226
+ }
227
+ else if (btype === 'tool_use') {
228
+ toolCalls.push({
229
+ id: (0, bridge_cache_1.normalizeToolCallId)(b['id'], String(b['name'] ?? ''), `${mi}.${toolCalls.length}`, b['input'] ?? {}),
230
+ type: 'function',
231
+ function: {
232
+ name: b['name'] ?? '',
233
+ arguments: (0, bridge_cache_1.stableJson)(b['input'] ?? {}),
234
+ },
235
+ });
236
+ }
237
+ else if (btype === 'tool_result') {
238
+ toolResults.push({
239
+ role: 'tool',
240
+ tool_call_id: b['tool_use_id'] ?? '',
241
+ content: extractToolResultText(b['content']),
242
+ });
243
+ }
244
+ }
245
+ // Tool results must immediately follow the assistant tool_calls they
246
+ // answer — emit them before this message's own content.
247
+ messages.push(...toolResults);
248
+ const joinedThinking = thinkingParts.join('\n');
249
+ const forceString = noMulti.has(model);
250
+ if (toolCalls.length > 0) {
251
+ // Keep any assistant text that accompanied the calls: dropping it loses
252
+ // the model's preamble ("I'll check the file"), and OpenAI-compatible
253
+ // upstreams accept a string content alongside `tool_calls`.
254
+ const out = {
255
+ role,
256
+ content: textParts.length > 0 ? textParts.join('\n') : null,
257
+ tool_calls: toolCalls,
258
+ };
259
+ if (joinedThinking)
260
+ out['reasoning_content'] = joinedThinking;
261
+ else if (thinking && isAssistant)
262
+ out['reasoning_content'] = ' ';
263
+ messages.push(out);
264
+ }
265
+ else if (hasMedia && !forceString) {
266
+ const out = { role, content: contentBlocks };
267
+ if (joinedThinking)
268
+ out['reasoning_content'] = joinedThinking;
269
+ else if (thinking && isAssistant)
270
+ out['reasoning_content'] = ' ';
271
+ messages.push(out);
272
+ }
273
+ else if (textParts.length > 0 || thinkingParts.length > 0 || (thinking && isAssistant) || hasMedia) {
274
+ const out = { role, content: textParts.length > 0 ? textParts.join('\n') : '' };
275
+ if (joinedThinking)
276
+ out['reasoning_content'] = joinedThinking;
277
+ else if (thinking && isAssistant)
278
+ out['reasoning_content'] = ' ';
279
+ messages.push(out);
280
+ }
281
+ }
282
+ const oai = {
283
+ model,
284
+ messages,
285
+ max_tokens: body['max_tokens'] ?? 16384,
286
+ stream: body['stream'] ?? false,
287
+ };
288
+ const thinkingCfg = asObject(body['thinking']);
289
+ // Only reasoning families (deepseek/glm) accept max_completion_tokens;
290
+ // non-reasoning chat models reject the field when max_tokens is also set.
291
+ if (thinking && thinkingCfg?.['budget_tokens'] && (0, protocol_router_1.chatFamilyOf)(model) !== null) {
292
+ oai['max_completion_tokens'] = thinkingCfg['budget_tokens'];
293
+ }
294
+ for (const [key, oaiKey] of [
295
+ ['temperature', 'temperature'],
296
+ ['top_p', 'top_p'],
297
+ ['stop_sequences', 'stop'],
298
+ ]) {
299
+ if (key in body)
300
+ oai[oaiKey] = body[key];
301
+ }
302
+ if (Array.isArray(body['tools'])) {
303
+ oai['tools'] = body['tools']
304
+ .map((t) => asObject(t))
305
+ .filter((t) => t !== null && typeof t['name'] === 'string' && t['name'] !== '')
306
+ .map((t) => ({
307
+ type: 'function',
308
+ function: {
309
+ name: t['name'],
310
+ description: t['description'] ?? '',
311
+ parameters: t['input_schema'] ?? {},
312
+ },
313
+ }));
314
+ const tc = body['tool_choice'] ?? 'auto';
315
+ const tcObj = asObject(tc);
316
+ if (tcObj !== null) {
317
+ const tcType = tcObj['type'] ?? 'auto';
318
+ if (tcType === 'tool') {
319
+ oai['tool_choice'] = { type: 'function', function: { name: tcObj['name'] ?? '' } };
320
+ }
321
+ else if (tcType === 'any') {
322
+ oai['tool_choice'] = 'required';
323
+ }
324
+ else {
325
+ oai['tool_choice'] = 'auto';
326
+ }
327
+ }
328
+ else {
329
+ oai['tool_choice'] = tc;
330
+ }
331
+ }
332
+ return oai;
333
+ }
334
+ // ── Chat reasoning extras (DeepSeek / GLM) ───────────────────────────
335
+ const MAX_TOKENS_FLOOR = {
336
+ low: 16384,
337
+ high: 65536,
338
+ max: 131072,
339
+ };
340
+ function maxTokensFloorFor(effort) {
341
+ return MAX_TOKENS_FLOOR[effort ?? 'low'] ?? 16384;
342
+ }
343
+ function sanitizeUserId(session) {
344
+ const s = String(session ?? '');
345
+ if (!s)
346
+ return 'default';
347
+ return (0, node_crypto_1.createHash)('sha256').update(s, 'utf8').digest('hex').slice(0, 32);
348
+ }
349
+ /**
350
+ * Family-gated chat extras, in place (reference parity).
351
+ *
352
+ * Emits thinking + reasoning_effort + user_id for deepseek-/glm- families
353
+ * only; sets top_p 0.95 default (DeepSeek, client wins); floors max_tokens
354
+ * by sent effort (never lowers); rejects required/named tool_choice with
355
+ * thinking on (upstream 400s). Returns the wire truth for telemetry.
356
+ */
357
+ function applyChatExtras(oaiBody, modelId, session, clientEffort = null, clientThinking = null) {
358
+ const fam = (0, protocol_router_1.chatFamilyOf)(modelId);
359
+ if (!fam)
360
+ return { effortSent: null, thinkingSent: null };
361
+ const wireEffort = (0, protocol_router_1.mapChatEffort)(modelId, clientEffort);
362
+ let wireThinking = (0, protocol_router_1.mapChatThinking)(modelId, clientThinking);
363
+ // GLM always-on; DeepSeek defaults enabled when unset.
364
+ if (fam === 'glm')
365
+ wireThinking = 'enabled';
366
+ else if (fam === 'deepseek' && wireThinking === null && (clientThinking === null || clientThinking === '' || clientThinking === 'none')) {
367
+ wireThinking = 'enabled';
368
+ }
369
+ // Explicit-off shapes: DeepSeek disable drops effort; GLM cannot
370
+ // disable (thinking stays on; a client 'none' effort just omits it).
371
+ let effortSent = wireEffort;
372
+ if (clientEffort === 'none' || clientThinking === 'disabled') {
373
+ if (fam === 'deepseek') {
374
+ wireThinking = 'disabled';
375
+ effortSent = null;
376
+ }
377
+ }
378
+ if (wireThinking !== null)
379
+ oaiBody['thinking'] = { type: wireThinking };
380
+ if (effortSent !== null)
381
+ oaiBody['reasoning_effort'] = effortSent;
382
+ try {
383
+ oaiBody['user_id'] = sanitizeUserId(session);
384
+ }
385
+ catch {
386
+ /* ignore */
387
+ }
388
+ if (fam === 'deepseek' && !('top_p' in oaiBody))
389
+ oaiBody['top_p'] = 0.95;
390
+ // max_tokens floors by SENT effort (raise only; never coerce odd shapes).
391
+ try {
392
+ const floor = maxTokensFloorFor(effortSent ?? 'low');
393
+ const cur = oaiBody['max_tokens'];
394
+ if (cur === undefined || cur === null) {
395
+ oaiBody['max_tokens'] = floor;
396
+ }
397
+ else if (typeof cur === 'number' && Number.isInteger(cur) && cur < floor) {
398
+ oaiBody['max_tokens'] = floor;
399
+ }
400
+ }
401
+ catch {
402
+ /* ignore */
403
+ }
404
+ // Tool-choice guard: required/named + thinking on → upstream 400.
405
+ try {
406
+ const tc = oaiBody['tool_choice'];
407
+ const thinkingOn = wireThinking === 'enabled';
408
+ if (thinkingOn && tc !== undefined && tc !== null) {
409
+ let bad = false;
410
+ if (tc && typeof tc === 'object') {
411
+ const t = tc['type'];
412
+ if (t === 'required')
413
+ bad = true;
414
+ if (t === 'function') {
415
+ const fn = tc['function'];
416
+ if (fn && typeof fn === 'object' && typeof fn.name === 'string')
417
+ bad = true;
418
+ }
419
+ }
420
+ else if (tc === 'required') {
421
+ bad = true;
422
+ }
423
+ if (bad) {
424
+ throw new Error(`model ${modelId} rejects required/named tool_choice with thinking on (upstream 400); ` +
425
+ 'use auto/none or disable thinking');
426
+ }
427
+ }
428
+ }
429
+ catch (e) {
430
+ if (e instanceof Error && e.message.includes('rejects required/named'))
431
+ throw e;
432
+ // Shape errors are ignored (defensive total function).
433
+ }
434
+ return { effortSent, thinkingSent: wireThinking };
435
+ }
436
+ // ── Response translation (non-streaming) ─────────────────────────────
437
+ const FINISH_TO_STOP = {
438
+ stop: 'end_turn',
439
+ length: 'max_tokens',
440
+ tool_calls: 'tool_use',
441
+ content_filter: 'content_filter',
442
+ };
443
+ function mapFinishReason(finish) {
444
+ return FINISH_TO_STOP[String(finish ?? '')] ?? 'end_turn';
445
+ }
446
+ function oaiUsageToAnthropic(usage) {
447
+ const u = (usage && typeof usage === 'object' ? usage : {});
448
+ const input = Number(u['prompt_tokens'] ?? u['input_tokens'] ?? 0) || 0;
449
+ const output = (0, bridge_cache_1.getOutputTokens)(u);
450
+ return (0, bridge_cache_1.withReasoningSplit)({
451
+ input_tokens: input,
452
+ output_tokens: output,
453
+ cache_creation_input_tokens: (0, bridge_cache_1.extractCacheCreationTokens)(u),
454
+ cache_read_input_tokens: (0, bridge_cache_1.extractCacheReadTokens)(u),
455
+ }, u);
456
+ }
457
+ /**
458
+ * Convert a non-streaming chat/completions response to an Anthropic
459
+ * message. Loud on malformed tool-call arguments.
460
+ */
461
+ function openAIToAnthropic(resp, requested, messageId) {
462
+ if (!resp || typeof resp !== 'object') {
463
+ throw new Error('upstream returned a malformed response envelope');
464
+ }
465
+ const choices = resp['choices'];
466
+ const choice = Array.isArray(choices) && choices.length > 0 && choices[0] && typeof choices[0] === 'object' ? choices[0] : {};
467
+ const msg = choice['message'] && typeof choice['message'] === 'object' ? choice['message'] : {};
468
+ const usage = resp['usage'] && typeof resp['usage'] === 'object' ? resp['usage'] : {};
469
+ const blocks = [];
470
+ const reasoning = msg['reasoning_content'];
471
+ if (typeof reasoning === 'string' && reasoning) {
472
+ blocks.push({ type: 'thinking', thinking: reasoning });
473
+ }
474
+ const content = msg['content'];
475
+ if (content) {
476
+ const text = typeof content === 'string'
477
+ ? content
478
+ : Array.isArray(content)
479
+ ? content.map((p) => String(asObject(p)?.['text'] ?? '')).join('')
480
+ : '';
481
+ if (text)
482
+ blocks.push({ type: 'text', text });
483
+ }
484
+ const toolCalls = Array.isArray(msg['tool_calls']) ? msg['tool_calls'] : [];
485
+ for (let ti = 0; ti < toolCalls.length; ti += 1) {
486
+ const tc = toolCalls[ti];
487
+ if (!tc || typeof tc !== 'object') {
488
+ throw new Error('upstream returned invalid function-call arguments');
489
+ }
490
+ const fn = tc['function'] && typeof tc['function'] === 'object' ? tc['function'] : {};
491
+ let input;
492
+ try {
493
+ input = parseToolArgs(String(fn['arguments'] ?? '{}'));
494
+ }
495
+ catch {
496
+ throw new Error('upstream returned invalid function-call arguments');
497
+ }
498
+ blocks.push({
499
+ type: 'tool_use',
500
+ id: (0, bridge_cache_1.normalizeToolCallId)(tc['id'], String(fn['name'] ?? ''), ti, input),
501
+ name: fn['name'] ?? '',
502
+ input,
503
+ });
504
+ }
505
+ if (blocks.length === 0)
506
+ blocks.push({ type: 'text', text: '' });
507
+ let stop = mapFinishReason(choice['finish_reason']);
508
+ if (toolCalls.length > 0)
509
+ stop = 'tool_use';
510
+ return {
511
+ id: messageId ?? `msg_${Math.random().toString(16).slice(2, 26)}`,
512
+ type: 'message',
513
+ role: 'assistant',
514
+ content: blocks,
515
+ model: requested,
516
+ stop_reason: stop,
517
+ stop_sequence: null,
518
+ usage: oaiUsageToAnthropic(usage),
519
+ };
520
+ }
521
+ function parseToolArgs(raw) {
522
+ if (!raw || !raw.trim())
523
+ return {};
524
+ const obj = JSON.parse(raw);
525
+ if (Array.isArray(obj)) {
526
+ // Some servers stream bare arrays for a single positional argument;
527
+ // fail loudly rather than silently reshaping (reference parity).
528
+ throw new Error('upstream function-call arguments are not a JSON object');
529
+ }
530
+ if (!obj || typeof obj !== 'object') {
531
+ throw new Error('upstream function-call arguments are not a JSON object');
532
+ }
533
+ return obj;
534
+ }
535
+ function countInvalidToolArgs(toolBlocks) {
536
+ let bad = 0;
537
+ for (const b of toolBlocks) {
538
+ try {
539
+ parseToolArgs(b.args);
540
+ }
541
+ catch {
542
+ bad += 1;
543
+ }
544
+ }
545
+ return bad;
546
+ }
547
+ class ChatStreamTranslator {
548
+ requestedModel;
549
+ estimatedInput;
550
+ messageId;
551
+ reader = (0, sse_1.createLineReader)();
552
+ started = false;
553
+ textBlockIdx = null;
554
+ reasoningBlockIdx = null;
555
+ nextBlockIdx = 0;
556
+ openBlocks = new Set();
557
+ toolBlocks = new Map();
558
+ finishReason = null;
559
+ usage = null;
560
+ stopped = false;
561
+ onFinalUsage;
562
+ constructor(opts) {
563
+ this.requestedModel = opts.requestedModel;
564
+ this.estimatedInput = opts.estimatedInput ?? 0;
565
+ this.messageId = opts.messageId ?? `msg_${Math.random().toString(16).slice(2, 26)}`;
566
+ this.onFinalUsage = opts.onFinalUsage;
567
+ }
568
+ get messageStarted() {
569
+ return this.started;
570
+ }
571
+ get isStopped() {
572
+ return this.stopped;
573
+ }
574
+ /** Feed decoded upstream bytes; returns Anthropic SSE frames to forward. */
575
+ push(text) {
576
+ if (this.stopped)
577
+ return [];
578
+ return this.consume(this.reader.push(text));
579
+ }
580
+ /**
581
+ * Parse complete SSE lines into Anthropic frames. `finish()` also
582
+ * drains the reader's unterminated trailing line, so a usage-only
583
+ * event at EOF (no final newline) still reaches `onFinalUsage`.
584
+ */
585
+ consume(lines) {
586
+ const frames = [];
587
+ for (const line of lines) {
588
+ const data = (0, sse_1.parseDataLine)(line);
589
+ if (!data)
590
+ continue;
591
+ let chunk;
592
+ try {
593
+ chunk = JSON.parse(data);
594
+ }
595
+ catch {
596
+ continue;
597
+ }
598
+ frames.push(...this.handleChunk(chunk));
599
+ }
600
+ return frames;
601
+ }
602
+ /** Upstream ended ([DONE] or EOF): emit closing frames. */
603
+ finish() {
604
+ if (this.stopped)
605
+ return [];
606
+ const frames = this.consume(this.reader.flush());
607
+ this.stopped = true;
608
+ if (!this.started) {
609
+ frames.push(...this.startMessage());
610
+ }
611
+ for (const idx of [...this.openBlocks]) {
612
+ frames.push((0, sse_1.sseFrame)('content_block_stop', { type: 'content_block_stop', index: idx }));
613
+ }
614
+ this.openBlocks.clear();
615
+ const hasTools = this.toolBlocks.size > 0;
616
+ let stop = mapFinishReason(this.finishReason);
617
+ if (hasTools) {
618
+ if (this.finishReason === 'length' && countInvalidToolArgs(this.toolBlocks.values()) > 0) {
619
+ const bad = countInvalidToolArgs(this.toolBlocks.values());
620
+ const msg = `upstream cut off ${bad} tool call(s) mid-arguments (length): ` +
621
+ 'raise max_tokens or shorten context';
622
+ frames.push((0, sse_1.sseFrame)('error', { type: 'error', error: { type: 'api_error', message: msg } }));
623
+ return frames; // reference: error close breaks the stream (no delta/stop)
624
+ }
625
+ stop = 'tool_use';
626
+ }
627
+ frames.push(...this.terminalFrames(stop));
628
+ return frames;
629
+ }
630
+ /** Transport/upstream failure: emit the reference error close. */
631
+ fail(message) {
632
+ if (this.stopped)
633
+ return [];
634
+ this.stopped = true;
635
+ const frames = [];
636
+ const payload = { type: 'error', error: { type: 'api_error', message } };
637
+ if (!this.started) {
638
+ frames.push((0, sse_1.sseFrame)('error', payload));
639
+ return frames;
640
+ }
641
+ for (const idx of [...this.openBlocks]) {
642
+ frames.push((0, sse_1.sseFrame)('content_block_stop', { type: 'content_block_stop', index: idx }));
643
+ }
644
+ this.openBlocks.clear();
645
+ frames.push((0, sse_1.sseFrame)('message_delta', {
646
+ type: 'message_delta',
647
+ delta: { stop_reason: 'error' },
648
+ usage: { output_tokens: this.finalOutputTokens() },
649
+ }));
650
+ frames.push((0, sse_1.sseFrame)('message_stop', { type: 'message_stop' }));
651
+ return frames;
652
+ }
653
+ // ── internals ──────────────────────────────────────────────────────
654
+ handleChunk(chunk) {
655
+ const frames = [];
656
+ const u = chunk['usage'];
657
+ if (u && typeof u === 'object') {
658
+ this.usage = { ...(this.usage ?? {}), ...u };
659
+ }
660
+ const choices = chunk['choices'];
661
+ if (!Array.isArray(choices) || choices.length === 0)
662
+ return frames;
663
+ const choice = choices[0] && typeof choices[0] === 'object' ? choices[0] : {};
664
+ if (choice['finish_reason'])
665
+ this.finishReason = String(choice['finish_reason']);
666
+ const delta = choice['delta'] && typeof choice['delta'] === 'object' ? choice['delta'] : {};
667
+ if (!this.started) {
668
+ frames.push(...this.startMessage());
669
+ }
670
+ // Text.
671
+ let text = '';
672
+ const c = delta['content'];
673
+ if (typeof c === 'string')
674
+ text = c;
675
+ else if (Array.isArray(c)) {
676
+ text = c.map((p) => String(asObject(p)?.['text'] ?? '')).join('');
677
+ }
678
+ if (text) {
679
+ if (this.textBlockIdx === null) {
680
+ this.textBlockIdx = this.nextBlockIdx++;
681
+ this.openBlocks.add(this.textBlockIdx);
682
+ frames.push((0, sse_1.sseFrame)('content_block_start', {
683
+ type: 'content_block_start',
684
+ index: this.textBlockIdx,
685
+ content_block: { type: 'text', text: '' },
686
+ }));
687
+ }
688
+ frames.push((0, sse_1.sseFrame)('content_block_delta', {
689
+ type: 'content_block_delta',
690
+ index: this.textBlockIdx,
691
+ delta: { type: 'text_delta', text },
692
+ }));
693
+ }
694
+ // Reasoning.
695
+ const reasoning = delta['reasoning_content'];
696
+ if (typeof reasoning === 'string' && reasoning) {
697
+ if (this.reasoningBlockIdx === null) {
698
+ this.reasoningBlockIdx = this.nextBlockIdx++;
699
+ this.openBlocks.add(this.reasoningBlockIdx);
700
+ frames.push((0, sse_1.sseFrame)('content_block_start', {
701
+ type: 'content_block_start',
702
+ index: this.reasoningBlockIdx,
703
+ content_block: { type: 'thinking', thinking: '' },
704
+ }));
705
+ }
706
+ frames.push((0, sse_1.sseFrame)('content_block_delta', {
707
+ type: 'content_block_delta',
708
+ index: this.reasoningBlockIdx,
709
+ delta: { type: 'thinking_delta', thinking: reasoning },
710
+ }));
711
+ }
712
+ // Tool calls.
713
+ const toolCalls = delta['tool_calls'];
714
+ if (Array.isArray(toolCalls)) {
715
+ for (const tc of toolCalls) {
716
+ if (!tc || typeof tc !== 'object')
717
+ continue;
718
+ const apiIdx = typeof tc['index'] === 'number' ? tc['index'] : 0;
719
+ const fn = tc['function'] && typeof tc['function'] === 'object' ? tc['function'] : {};
720
+ let state = this.toolBlocks.get(apiIdx);
721
+ if (!state) {
722
+ const blockIdx = this.nextBlockIdx++;
723
+ const providedId = typeof tc['id'] === 'string' && tc['id'] ? tc['id'] : undefined;
724
+ const name = typeof fn['name'] === 'string' ? fn['name'] : '';
725
+ const id = providedId ?? `toolu_${(0, bridge_cache_1.normalizeToolCallId)(undefined, name || 'tool', apiIdx, '').slice(7)}`;
726
+ state = { blockIdx, id, name, args: '' };
727
+ this.toolBlocks.set(apiIdx, state);
728
+ this.openBlocks.add(blockIdx);
729
+ frames.push((0, sse_1.sseFrame)('content_block_start', {
730
+ type: 'content_block_start',
731
+ index: blockIdx,
732
+ content_block: { type: 'tool_use', id, name, input: {} },
733
+ }));
734
+ }
735
+ if (typeof fn['name'] === 'string' && fn['name'] && !state.name)
736
+ state.name = fn['name'];
737
+ if (typeof fn['arguments'] === 'string' && fn['arguments']) {
738
+ state.args += fn['arguments'];
739
+ frames.push((0, sse_1.sseFrame)('content_block_delta', {
740
+ type: 'content_block_delta',
741
+ index: state.blockIdx,
742
+ delta: { type: 'input_json_delta', partial_json: fn['arguments'] },
743
+ }));
744
+ }
745
+ }
746
+ }
747
+ return frames;
748
+ }
749
+ startMessage() {
750
+ this.started = true;
751
+ return [
752
+ (0, sse_1.sseFrame)('message_start', {
753
+ type: 'message_start',
754
+ message: {
755
+ id: this.messageId,
756
+ type: 'message',
757
+ role: 'assistant',
758
+ content: [],
759
+ model: this.requestedModel,
760
+ stop_reason: null,
761
+ stop_sequence: null,
762
+ usage: {
763
+ input_tokens: this.estimatedInput,
764
+ output_tokens: 0,
765
+ cache_creation_input_tokens: 0,
766
+ cache_read_input_tokens: 0,
767
+ },
768
+ },
769
+ }),
770
+ ];
771
+ }
772
+ finalOutputTokens() {
773
+ if (this.usage) {
774
+ const n = this.usage['completion_tokens'] ?? this.usage['output_tokens'];
775
+ if (typeof n === 'number' && Number.isFinite(n))
776
+ return n;
777
+ }
778
+ return 0;
779
+ }
780
+ terminalFrames(stop) {
781
+ try {
782
+ this.onFinalUsage?.((0, bridge_cache_1.upstreamUsageFromOpenAI)(this.usage));
783
+ }
784
+ catch {
785
+ /* observability only: never fail the stream on a usage callback */
786
+ }
787
+ const u = this.usage ?? {};
788
+ const finalIn = Number(u['prompt_tokens'] ?? u['input_tokens'] ?? this.estimatedInput) || this.estimatedInput;
789
+ const finalOut = this.finalOutputTokens();
790
+ // Report reads and writes under their own Anthropic fields. Folding the
791
+ // write count into `cache_read_input_tokens` (as the reference does) makes
792
+ // an assistant client see cache hits that never happened.
793
+ const usage = (0, bridge_cache_1.withReasoningSplit)({
794
+ output_tokens: finalOut,
795
+ input_tokens: finalIn,
796
+ cache_read_input_tokens: (0, bridge_cache_1.extractCacheReadTokens)(u),
797
+ cache_creation_input_tokens: (0, bridge_cache_1.extractCacheCreationTokens)(u),
798
+ }, u);
799
+ return [
800
+ (0, sse_1.sseFrame)('message_delta', { type: 'message_delta', delta: { stop_reason: stop }, usage }),
801
+ (0, sse_1.sseFrame)('message_stop', { type: 'message_stop' }),
802
+ ];
803
+ }
804
+ }
805
+ exports.ChatStreamTranslator = ChatStreamTranslator;
806
+ // ── Input estimation ─────────────────────────────────────────────────
807
+ /** Rough pre-stream input estimate (reference `_estimate_input_tokens`). */
808
+ function estimateInputTokens(body) {
809
+ const chunks = [];
810
+ const system = body?.['system'] ?? '';
811
+ if (typeof system === 'string')
812
+ chunks.push(system);
813
+ else if (Array.isArray(system)) {
814
+ for (const s of system) {
815
+ if (typeof s === 'string')
816
+ chunks.push(s);
817
+ else if (s && typeof s === 'object') {
818
+ const src = (s['source'] ?? {});
819
+ chunks.push(String(src['data'] ?? src['url'] ?? s['text'] ?? ''));
820
+ }
821
+ }
822
+ }
823
+ for (const t of Array.isArray(body?.['tools']) ? body['tools'] : []) {
824
+ if (!t || typeof t !== 'object')
825
+ continue;
826
+ chunks.push(String(t['name'] ?? ''), String(t['description'] ?? ''), (0, bridge_cache_1.stableJson)(t['input_schema'] ?? {}));
827
+ }
828
+ for (const msg of Array.isArray(body?.['messages']) ? body['messages'] : []) {
829
+ const content = msg?.['content'] ?? '';
830
+ if (typeof content === 'string')
831
+ chunks.push(content);
832
+ else if (Array.isArray(content)) {
833
+ for (const block of content) {
834
+ if (typeof block === 'string')
835
+ chunks.push(block);
836
+ else if (block && typeof block === 'object') {
837
+ const btype = block['type'];
838
+ if (btype === 'tool_result')
839
+ chunks.push(extractToolResultText(block['content']));
840
+ else if (btype === 'thinking')
841
+ chunks.push(String(block['thinking'] ?? ''));
842
+ else if (btype === 'image' || btype === 'document') {
843
+ const src = (block['source'] ?? {});
844
+ chunks.push(String(src['data'] ?? src['url'] ?? ''));
845
+ }
846
+ else {
847
+ chunks.push(String(block['text'] ?? ''));
848
+ chunks.push((0, bridge_cache_1.stableJson)(block['input'] ?? ''));
849
+ }
850
+ }
851
+ }
852
+ }
853
+ }
854
+ const combined = chunks.join('\n');
855
+ return Math.max(1, Math.floor(combined.length / 3));
856
+ }
857
+ //# sourceMappingURL=translate-chat.js.map