@llumiverse/drivers 1.1.1-dev.20260505.151157Z → 1.3.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 (49) hide show
  1. package/lib/cjs/anthropic/index.js +64 -0
  2. package/lib/cjs/anthropic/index.js.map +1 -0
  3. package/lib/cjs/index.js +1 -0
  4. package/lib/cjs/index.js.map +1 -1
  5. package/lib/cjs/openai/index.js +12 -6
  6. package/lib/cjs/openai/index.js.map +1 -1
  7. package/lib/cjs/shared/claude-messages.js +737 -0
  8. package/lib/cjs/shared/claude-messages.js.map +1 -0
  9. package/lib/cjs/vertexai/index.js.map +1 -1
  10. package/lib/cjs/vertexai/models/claude.js +27 -872
  11. package/lib/cjs/vertexai/models/claude.js.map +1 -1
  12. package/lib/cjs/vertexai/models/gemini.js +18 -12
  13. package/lib/cjs/vertexai/models/gemini.js.map +1 -1
  14. package/lib/esm/anthropic/index.js +57 -0
  15. package/lib/esm/anthropic/index.js.map +1 -0
  16. package/lib/esm/index.js +1 -0
  17. package/lib/esm/index.js.map +1 -1
  18. package/lib/esm/openai/index.js +12 -7
  19. package/lib/esm/openai/index.js.map +1 -1
  20. package/lib/esm/shared/claude-messages.js +716 -0
  21. package/lib/esm/shared/claude-messages.js.map +1 -0
  22. package/lib/esm/vertexai/index.js.map +1 -1
  23. package/lib/esm/vertexai/models/claude.js +27 -865
  24. package/lib/esm/vertexai/models/claude.js.map +1 -1
  25. package/lib/esm/vertexai/models/gemini.js +18 -12
  26. package/lib/esm/vertexai/models/gemini.js.map +1 -1
  27. package/lib/types/anthropic/index.d.ts +21 -0
  28. package/lib/types/anthropic/index.d.ts.map +1 -0
  29. package/lib/types/index.d.ts +1 -0
  30. package/lib/types/index.d.ts.map +1 -1
  31. package/lib/types/openai/index.d.ts +1 -0
  32. package/lib/types/openai/index.d.ts.map +1 -1
  33. package/lib/types/shared/claude-messages.d.ts +75 -0
  34. package/lib/types/shared/claude-messages.d.ts.map +1 -0
  35. package/lib/types/vertexai/index.d.ts +4 -4
  36. package/lib/types/vertexai/index.d.ts.map +1 -1
  37. package/lib/types/vertexai/models/claude.d.ts +3 -106
  38. package/lib/types/vertexai/models/claude.d.ts.map +1 -1
  39. package/lib/types/vertexai/models/gemini.d.ts +1 -1
  40. package/lib/types/vertexai/models/gemini.d.ts.map +1 -1
  41. package/package.json +7 -6
  42. package/src/anthropic/index.ts +104 -0
  43. package/src/index.ts +1 -0
  44. package/src/openai/index.ts +13 -8
  45. package/src/shared/claude-messages.ts +879 -0
  46. package/src/vertexai/index.ts +18 -19
  47. package/src/vertexai/models/claude-error-handling.test.ts +3 -3
  48. package/src/vertexai/models/claude.ts +44 -1016
  49. package/src/vertexai/models/gemini.ts +27 -14
@@ -0,0 +1,737 @@
1
+ "use strict";
2
+ /**
3
+ * Shared utilities for Anthropic SDK-based drivers.
4
+ *
5
+ * Used by both the native AnthropicDriver (drivers/src/anthropic/) and the
6
+ * VertexAI Claude pathway (drivers/src/vertexai/models/claude.ts). Both use
7
+ * the same Anthropic Messages API surface — the only difference is the client
8
+ * (Anthropic vs AnthropicVertex) and how auth is wired up.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.anthropicUsageToTokenUsage = anthropicUsageToTokenUsage;
12
+ exports.claudeFinishReason = claudeFinishReason;
13
+ exports.collectClaudeTools = collectClaudeTools;
14
+ exports.collectAllTextContent = collectAllTextContent;
15
+ exports.claudeMaxTokens = claudeMaxTokens;
16
+ exports.formatClaudePrompt = formatClaudePrompt;
17
+ exports.createPromptFromResponse = createPromptFromResponse;
18
+ exports.mergeConsecutiveUserMessages = mergeConsecutiveUserMessages;
19
+ exports.sanitizeMessages = sanitizeMessages;
20
+ exports.fixOrphanedToolUse = fixOrphanedToolUse;
21
+ exports.updateClaudeConversation = updateClaudeConversation;
22
+ exports.claudeMessagesContainToolBlocks = claudeMessagesContainToolBlocks;
23
+ exports.convertClaudeToolBlocksToText = convertClaudeToolBlocksToText;
24
+ exports.getClaudePayload = getClaudePayload;
25
+ exports.buildClaudeStreamingConversation = buildClaudeStreamingConversation;
26
+ exports.executeClaudeCompletion = executeClaudeCompletion;
27
+ exports.streamClaudeCompletion = streamClaudeCompletion;
28
+ exports.formatAnthropicLlumiverseError = formatAnthropicLlumiverseError;
29
+ exports.isClaudeErrorRetryable = isClaudeErrorRetryable;
30
+ const error_1 = require("@anthropic-ai/sdk/error");
31
+ const common_1 = require("@llumiverse/common");
32
+ const core_1 = require("@llumiverse/core");
33
+ const async_1 = require("@llumiverse/core/async");
34
+ const claude_thinking_js_1 = require("./claude-thinking.js");
35
+ // ============================================================================
36
+ // Token usage
37
+ // ============================================================================
38
+ function anthropicUsageToTokenUsage(usage) {
39
+ const cacheRead = usage.cache_read_input_tokens ?? 0;
40
+ const cacheWrite = usage.cache_creation_input_tokens ?? 0;
41
+ return {
42
+ prompt_new: usage.input_tokens,
43
+ prompt: usage.input_tokens + cacheRead + cacheWrite,
44
+ result: usage.output_tokens,
45
+ total: usage.input_tokens + usage.output_tokens + cacheRead + cacheWrite,
46
+ prompt_cached: usage.cache_read_input_tokens ?? undefined,
47
+ prompt_cache_write: usage.cache_creation_input_tokens ?? undefined,
48
+ };
49
+ }
50
+ // ============================================================================
51
+ // Finish reason
52
+ // ============================================================================
53
+ function claudeFinishReason(reason) {
54
+ if (!reason)
55
+ return undefined;
56
+ switch (reason) {
57
+ case 'end_turn': return 'stop';
58
+ case 'max_tokens': return 'length';
59
+ default: return reason; // stop_sequence, tool_use
60
+ }
61
+ }
62
+ // ============================================================================
63
+ // Content extraction
64
+ // ============================================================================
65
+ function collectClaudeTools(content) {
66
+ const out = [];
67
+ for (const block of content) {
68
+ if (block.type === 'tool_use') {
69
+ out.push({
70
+ id: block.id,
71
+ tool_name: block.name,
72
+ tool_input: block.input,
73
+ });
74
+ }
75
+ }
76
+ return out.length > 0 ? out : undefined;
77
+ }
78
+ function collectAllTextContent(content, includeThoughts = false) {
79
+ const textParts = [];
80
+ if (includeThoughts) {
81
+ for (const block of content) {
82
+ if (block.type === 'thinking' && block.thinking) {
83
+ textParts.push(block.thinking);
84
+ }
85
+ else if (block.type === 'redacted_thinking' && block.data) {
86
+ textParts.push(`[Redacted thinking: ${block.data}]`);
87
+ }
88
+ }
89
+ if (textParts.length > 0) {
90
+ textParts.push('');
91
+ }
92
+ }
93
+ for (const block of content) {
94
+ if (block.type === 'text' && block.text) {
95
+ textParts.push(block.text);
96
+ }
97
+ }
98
+ return textParts.join('\n');
99
+ }
100
+ // ============================================================================
101
+ // Max tokens
102
+ // ============================================================================
103
+ function claudeMaxTokens(option) {
104
+ const modelOptions = option.model_options;
105
+ if (modelOptions && typeof modelOptions.max_tokens === 'number') {
106
+ return modelOptions.max_tokens;
107
+ }
108
+ let maxSupportedTokens = (0, common_1.getClaudeMaxTokensLimit)(option.model);
109
+ // Claude 3.7 supports up to 128k with a beta header; default to 64k when no budget is set.
110
+ if (option.model.includes('claude-3-7-sonnet') && (modelOptions?.thinking_budget_tokens ?? 0) < 48000) {
111
+ maxSupportedTokens = 64000;
112
+ }
113
+ return maxSupportedTokens;
114
+ }
115
+ async function collectFileBlocks(segment, restrictedTypes = false) {
116
+ const contentBlocks = [];
117
+ for (const file of segment.files || []) {
118
+ if (file.mime_type?.startsWith('image/')) {
119
+ const allowedTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
120
+ if (!allowedTypes.includes(file.mime_type)) {
121
+ throw new Error(`Unsupported image type: ${file.mime_type}`);
122
+ }
123
+ const mimeType = String(file.mime_type);
124
+ contentBlocks.push({
125
+ type: 'image',
126
+ source: {
127
+ type: 'base64',
128
+ data: await (0, core_1.readStreamAsBase64)(await file.getStream()),
129
+ media_type: mimeType,
130
+ },
131
+ });
132
+ }
133
+ else if (!restrictedTypes) {
134
+ if (file.mime_type === 'application/pdf') {
135
+ contentBlocks.push({
136
+ title: file.name,
137
+ type: 'document',
138
+ source: {
139
+ type: 'base64',
140
+ data: await (0, core_1.readStreamAsBase64)(await file.getStream()),
141
+ media_type: 'application/pdf',
142
+ },
143
+ });
144
+ }
145
+ else if (file.mime_type?.startsWith('text/')) {
146
+ contentBlocks.push({
147
+ title: file.name,
148
+ type: 'document',
149
+ source: {
150
+ type: 'text',
151
+ data: await (0, core_1.readStreamAsString)(await file.getStream()),
152
+ media_type: 'text/plain',
153
+ },
154
+ });
155
+ }
156
+ }
157
+ }
158
+ return contentBlocks;
159
+ }
160
+ // ============================================================================
161
+ // Prompt formatting (PromptSegment[] → ClaudePrompt)
162
+ // ============================================================================
163
+ async function formatClaudePrompt(segments, options) {
164
+ let system = segments
165
+ .filter((s) => s.role === core_1.PromptRole.system)
166
+ .map((s) => ({ text: s.content, type: 'text' }));
167
+ if (options.result_schema) {
168
+ const schemaText = options.tools && options.tools.length > 0
169
+ ? 'When not calling tools, the answer must be a JSON object using the following JSON Schema:\n' + JSON.stringify(options.result_schema)
170
+ : 'The answer must be a JSON object using the following JSON Schema:\n' + JSON.stringify(options.result_schema);
171
+ system.push({ text: schemaText, type: 'text' });
172
+ }
173
+ let messages = [];
174
+ const safetyMessages = [];
175
+ for (const segment of segments) {
176
+ if (segment.role === core_1.PromptRole.system)
177
+ continue;
178
+ if (segment.role === core_1.PromptRole.tool) {
179
+ if (!segment.tool_use_id) {
180
+ throw new Error('Tool prompt segment must have a tool use ID');
181
+ }
182
+ const contentBlocks = [];
183
+ if (segment.content) {
184
+ contentBlocks.push({ type: 'text', text: segment.content });
185
+ }
186
+ contentBlocks.push(...(await collectFileBlocks(segment, true)));
187
+ messages.push({
188
+ role: 'user',
189
+ content: [{
190
+ type: 'tool_result',
191
+ tool_use_id: segment.tool_use_id,
192
+ content: contentBlocks,
193
+ }],
194
+ });
195
+ }
196
+ else {
197
+ const contentBlocks = [];
198
+ if (segment.content) {
199
+ contentBlocks.push({ type: 'text', text: segment.content });
200
+ }
201
+ contentBlocks.push(...(await collectFileBlocks(segment, false)));
202
+ if (contentBlocks.length === 0)
203
+ continue;
204
+ const messageParam = {
205
+ role: segment.role === core_1.PromptRole.assistant ? 'assistant' : 'user',
206
+ content: contentBlocks,
207
+ };
208
+ if (segment.role === core_1.PromptRole.safety) {
209
+ safetyMessages.push(messageParam);
210
+ }
211
+ else {
212
+ messages.push(messageParam);
213
+ }
214
+ }
215
+ }
216
+ messages = messages.concat(safetyMessages);
217
+ if (system && system.length === 0)
218
+ system = undefined;
219
+ return { messages, system };
220
+ }
221
+ // ============================================================================
222
+ // Conversation management
223
+ // ============================================================================
224
+ function createPromptFromResponse(response) {
225
+ return {
226
+ messages: [{ role: response.role, content: response.content }],
227
+ system: undefined,
228
+ };
229
+ }
230
+ function mergeConsecutiveUserMessages(messages) {
231
+ if (messages.length === 0)
232
+ return [];
233
+ const needsMerging = messages.some((msg, i) => i < messages.length - 1 && msg.role === 'user' && messages[i + 1].role === 'user');
234
+ if (!needsMerging)
235
+ return messages;
236
+ const result = [];
237
+ let i = 0;
238
+ while (i < messages.length) {
239
+ const current = messages[i];
240
+ if (current.role === 'user') {
241
+ const mergedContent = [];
242
+ while (i < messages.length && messages[i].role === 'user') {
243
+ const userMsg = messages[i];
244
+ if (Array.isArray(userMsg.content)) {
245
+ mergedContent.push(...userMsg.content);
246
+ }
247
+ else if (typeof userMsg.content === 'string') {
248
+ mergedContent.push({ type: 'text', text: userMsg.content });
249
+ }
250
+ i++;
251
+ }
252
+ result.push({ role: 'user', content: mergedContent });
253
+ }
254
+ else {
255
+ result.push(current);
256
+ i++;
257
+ }
258
+ }
259
+ return result;
260
+ }
261
+ function sanitizeMessages(messages) {
262
+ const result = [];
263
+ for (const message of messages) {
264
+ if (typeof message.content === 'string') {
265
+ if (message.content.trim())
266
+ result.push(message);
267
+ continue;
268
+ }
269
+ const filteredContent = message.content.filter((block) => {
270
+ if (block.type === 'text')
271
+ return block.text && block.text.trim().length > 0;
272
+ return true;
273
+ });
274
+ if (filteredContent.length > 0) {
275
+ result.push({ ...message, content: filteredContent });
276
+ }
277
+ }
278
+ return result;
279
+ }
280
+ function fixOrphanedToolUse(messages) {
281
+ if (messages.length < 2)
282
+ return messages;
283
+ const result = [];
284
+ for (let i = 0; i < messages.length; i++) {
285
+ const current = messages[i];
286
+ result.push(current);
287
+ if (current.role === 'assistant' && Array.isArray(current.content)) {
288
+ const toolUseBlocks = current.content.filter((block) => block.type === 'tool_use');
289
+ if (toolUseBlocks.length > 0) {
290
+ const nextMessage = messages[i + 1];
291
+ if (nextMessage && nextMessage.role === 'user' && Array.isArray(nextMessage.content)) {
292
+ const toolResultIds = new Set(nextMessage.content
293
+ .filter((block) => block.type === 'tool_result')
294
+ .map((block) => block.tool_use_id));
295
+ const orphaned = toolUseBlocks.filter((block) => !toolResultIds.has(block.id));
296
+ if (orphaned.length > 0) {
297
+ const syntheticResults = orphaned.map((block) => ({
298
+ type: 'tool_result',
299
+ tool_use_id: block.id,
300
+ content: `[Tool interrupted: The user stopped the operation before "${block.name}" could execute.]`,
301
+ }));
302
+ messages[i + 1] = { ...nextMessage, content: [...syntheticResults, ...nextMessage.content] };
303
+ }
304
+ }
305
+ else if (nextMessage && nextMessage.role === 'user') {
306
+ const syntheticResults = toolUseBlocks.map((block) => ({
307
+ type: 'tool_result',
308
+ tool_use_id: block.id,
309
+ content: `[Tool interrupted: The user stopped the operation before "${block.name}" could execute.]`,
310
+ }));
311
+ const textContent = typeof nextMessage.content === 'string'
312
+ ? { type: 'text', text: nextMessage.content }
313
+ : { type: 'text', text: '' };
314
+ messages[i + 1] = { role: 'user', content: [...syntheticResults, textContent] };
315
+ }
316
+ }
317
+ }
318
+ }
319
+ return result;
320
+ }
321
+ function updateClaudeConversation(conversation, prompt) {
322
+ const baseSystemMessages = conversation?.system || [];
323
+ const baseMessages = conversation?.messages || [];
324
+ const system = baseSystemMessages.concat(prompt.system || []);
325
+ const combined = sanitizeMessages(baseMessages.concat(prompt.messages || []));
326
+ const mergedMessages = mergeConsecutiveUserMessages(combined);
327
+ return {
328
+ messages: mergedMessages,
329
+ system: system.length > 0 ? system : undefined,
330
+ };
331
+ }
332
+ function claudeMessagesContainToolBlocks(messages) {
333
+ for (const msg of messages) {
334
+ if (!Array.isArray(msg.content))
335
+ continue;
336
+ for (const block of msg.content) {
337
+ if (typeof block === 'object' && block !== null && 'type' in block) {
338
+ if (block.type === 'tool_use' || block.type === 'tool_result')
339
+ return true;
340
+ }
341
+ }
342
+ }
343
+ return false;
344
+ }
345
+ function convertClaudeToolBlocksToText(messages) {
346
+ return messages.map((msg) => {
347
+ if (!Array.isArray(msg.content))
348
+ return msg;
349
+ let hasToolBlocks = false;
350
+ for (const block of msg.content) {
351
+ if (typeof block === 'object' && block !== null && 'type' in block &&
352
+ (block.type === 'tool_use' || block.type === 'tool_result')) {
353
+ hasToolBlocks = true;
354
+ break;
355
+ }
356
+ }
357
+ if (!hasToolBlocks)
358
+ return msg;
359
+ const newContent = [];
360
+ for (const block of msg.content) {
361
+ if (typeof block === 'string') {
362
+ newContent.push(block);
363
+ continue;
364
+ }
365
+ if (block.type === 'tool_use') {
366
+ const inputStr = block.input ? JSON.stringify(block.input) : '';
367
+ const truncated = inputStr.length > 500 ? inputStr.substring(0, 500) + '...' : inputStr;
368
+ newContent.push({ type: 'text', text: `[Tool call: ${block.name}(${truncated})]` });
369
+ }
370
+ else if (block.type === 'tool_result') {
371
+ let resultStr = 'No content';
372
+ if (typeof block.content === 'string') {
373
+ resultStr = block.content.length > 500 ? block.content.substring(0, 500) + '...' : block.content;
374
+ }
375
+ else if (Array.isArray(block.content)) {
376
+ const texts = block.content
377
+ .filter((c) => c.type === 'text')
378
+ .map((c) => (c.text.length > 500 ? c.text.substring(0, 500) + '...' : c.text));
379
+ resultStr = texts.join('\n') || 'No text content';
380
+ }
381
+ newContent.push({ type: 'text', text: `[Tool result: ${resultStr}]` });
382
+ }
383
+ else {
384
+ newContent.push(block);
385
+ }
386
+ }
387
+ return { ...msg, content: newContent };
388
+ });
389
+ }
390
+ // ============================================================================
391
+ // Cache control stripping
392
+ // ============================================================================
393
+ function stripClaudeCacheControlFromBlock(block) {
394
+ if (typeof block === 'object' && block !== null && 'cache_control' in block) {
395
+ const { cache_control: _cc, ...rest } = block;
396
+ return rest;
397
+ }
398
+ return block;
399
+ }
400
+ function stripClaudeCacheControlFromMessages(messages) {
401
+ return messages.map((msg) => {
402
+ if (!Array.isArray(msg.content))
403
+ return msg;
404
+ return { ...msg, content: msg.content.map(stripClaudeCacheControlFromBlock) };
405
+ });
406
+ }
407
+ function stripClaudeCacheControlFromSystem(system) {
408
+ if (!system)
409
+ return undefined;
410
+ return system.map(stripClaudeCacheControlFromBlock);
411
+ }
412
+ function stripClaudeCacheControlFromTools(tools) {
413
+ if (!tools)
414
+ return undefined;
415
+ return tools.map((tool) => {
416
+ if ('cache_control' in tool) {
417
+ const { cache_control: _cc, ...rest } = tool;
418
+ return rest;
419
+ }
420
+ return tool;
421
+ });
422
+ }
423
+ // ============================================================================
424
+ // Payload builder
425
+ // ============================================================================
426
+ function getClaudePayload(options, prompt) {
427
+ const modelName = options.model;
428
+ const model_options = options.model_options;
429
+ let requestOptions;
430
+ if (modelName.includes('claude-3-7-sonnet') &&
431
+ ((model_options?.max_tokens ?? 0) > 64000 || (model_options?.thinking_budget_tokens ?? 0) > 64000)) {
432
+ requestOptions = { headers: { 'anthropic-beta': 'output-128k-2025-02-19' } };
433
+ }
434
+ const fixedMessages = fixOrphanedToolUse(prompt.messages);
435
+ let sanitizedMessages = sanitizeMessages(fixedMessages);
436
+ if (options.tools) {
437
+ for (const tool of options.tools) {
438
+ if (tool.input_schema.type !== 'object') {
439
+ throw new Error(`Tool "${tool.name}" has invalid input_schema.type: expected "object", got "${tool.input_schema.type}"`);
440
+ }
441
+ }
442
+ }
443
+ const hasTools = options.tools && options.tools.length > 0;
444
+ if (!hasTools && claudeMessagesContainToolBlocks(sanitizedMessages)) {
445
+ sanitizedMessages = convertClaudeToolBlocksToText(sanitizedMessages);
446
+ }
447
+ sanitizedMessages = stripClaudeCacheControlFromMessages(sanitizedMessages);
448
+ const sanitizedSystem = stripClaudeCacheControlFromSystem(prompt.system);
449
+ const sanitizedTools = hasTools
450
+ ? stripClaudeCacheControlFromTools(options.tools)
451
+ : undefined;
452
+ const cacheEnabled = model_options?.cache_enabled === true;
453
+ if (cacheEnabled) {
454
+ const cacheTtl = model_options?.cache_ttl;
455
+ const cacheControl = { type: 'ephemeral', ...(cacheTtl && { ttl: cacheTtl }) };
456
+ if (sanitizedSystem && sanitizedSystem.length > 0) {
457
+ const lastBlock = sanitizedSystem[sanitizedSystem.length - 1];
458
+ lastBlock.cache_control = cacheControl;
459
+ }
460
+ if (sanitizedTools && sanitizedTools.length > 0) {
461
+ const lastTool = sanitizedTools[sanitizedTools.length - 1];
462
+ lastTool.cache_control = cacheControl;
463
+ }
464
+ if (sanitizedMessages.length >= 4) {
465
+ const pivotMsg = sanitizedMessages[sanitizedMessages.length - 2];
466
+ if (Array.isArray(pivotMsg.content) && pivotMsg.content.length > 0) {
467
+ const lastBlock = pivotMsg.content[pivotMsg.content.length - 1];
468
+ if (typeof lastBlock === 'object' && lastBlock !== null && 'type' in lastBlock &&
469
+ lastBlock.type !== 'thinking' && lastBlock.type !== 'redacted_thinking') {
470
+ lastBlock.cache_control = cacheControl;
471
+ }
472
+ }
473
+ }
474
+ }
475
+ const { thinking, outputConfig, hasSamplingRestriction } = (0, claude_thinking_js_1.resolveClaudeThinking)(modelName, model_options);
476
+ const payload = {
477
+ messages: sanitizedMessages,
478
+ system: sanitizedSystem,
479
+ tools: sanitizedTools,
480
+ temperature: hasSamplingRestriction ? undefined : model_options?.temperature,
481
+ model: modelName,
482
+ max_tokens: claudeMaxTokens(options),
483
+ top_p: hasSamplingRestriction ? undefined : (model_options?.temperature != null ? undefined : model_options?.top_p),
484
+ top_k: hasSamplingRestriction ? undefined : model_options?.top_k,
485
+ stop_sequences: model_options?.stop_sequence,
486
+ thinking,
487
+ stream: true,
488
+ ...(outputConfig && { output_config: outputConfig }),
489
+ };
490
+ return { payload, requestOptions };
491
+ }
492
+ // ============================================================================
493
+ // Streaming conversation builder (called after stream completes)
494
+ // ============================================================================
495
+ function buildClaudeStreamingConversation(prompt, result, toolUse, options) {
496
+ const completionResults = result;
497
+ const text = completionResults
498
+ .filter((r) => r.type === 'text')
499
+ .map((r) => r.value)
500
+ .join('');
501
+ let conversation = updateClaudeConversation(options.conversation, prompt);
502
+ if (text) {
503
+ const assistantMsg = { role: 'assistant', content: text };
504
+ conversation = updateClaudeConversation(conversation, { messages: [assistantMsg] });
505
+ }
506
+ if (toolUse && toolUse.length > 0) {
507
+ const toolBlocks = toolUse.map((t) => ({
508
+ type: 'tool_use',
509
+ id: t.id,
510
+ name: t.tool_name,
511
+ input: t.tool_input ?? {},
512
+ }));
513
+ const assistantToolMsg = { role: 'assistant', content: toolBlocks };
514
+ conversation = updateClaudeConversation(conversation, { messages: [assistantToolMsg] });
515
+ }
516
+ conversation = (0, core_1.incrementConversationTurn)(conversation);
517
+ const currentTurn = (0, core_1.getConversationMeta)(conversation).turnNumber;
518
+ const stripOptions = {
519
+ keepForTurns: options.stripImagesAfterTurns ?? Infinity,
520
+ currentTurn,
521
+ textMaxTokens: options.stripTextMaxTokens,
522
+ };
523
+ let processed = (0, core_1.stripBase64ImagesFromConversation)(conversation, stripOptions);
524
+ processed = (0, core_1.truncateLargeTextInConversation)(processed, stripOptions);
525
+ processed = (0, core_1.stripHeartbeatsFromConversation)(processed, {
526
+ keepForTurns: options.stripHeartbeatsAfterTurns ?? 1,
527
+ currentTurn,
528
+ });
529
+ return processed;
530
+ }
531
+ // ============================================================================
532
+ // Execution helpers (standalone, take a client parameter)
533
+ // ============================================================================
534
+ /**
535
+ * Execute a non-streaming Claude completion.
536
+ * Works with any Anthropic-compatible client (Anthropic or AnthropicVertex).
537
+ */
538
+ async function executeClaudeCompletion(client, prompt, options) {
539
+ const model_options = options.model_options;
540
+ let conversation = updateClaudeConversation(options.conversation, prompt);
541
+ const { payload, requestOptions } = getClaudePayload(options, conversation);
542
+ const result = await client.messages.stream(payload, requestOptions).finalMessage();
543
+ const includeThoughts = model_options?.include_thoughts ?? false;
544
+ const text = collectAllTextContent(result.content, includeThoughts);
545
+ const tool_use = collectClaudeTools(result.content);
546
+ conversation = updateClaudeConversation(conversation, createPromptFromResponse(result));
547
+ conversation = (0, core_1.incrementConversationTurn)(conversation);
548
+ const currentTurn = (0, core_1.getConversationMeta)(conversation).turnNumber;
549
+ const stripOpts = {
550
+ keepForTurns: options.stripImagesAfterTurns ?? Infinity,
551
+ currentTurn,
552
+ textMaxTokens: options.stripTextMaxTokens,
553
+ };
554
+ let processedConversation = (0, core_1.stripBase64ImagesFromConversation)(conversation, stripOpts);
555
+ processedConversation = (0, core_1.truncateLargeTextInConversation)(processedConversation, stripOpts);
556
+ processedConversation = (0, core_1.stripHeartbeatsFromConversation)(processedConversation, {
557
+ keepForTurns: options.stripHeartbeatsAfterTurns ?? 1,
558
+ currentTurn,
559
+ });
560
+ return {
561
+ result: text ? [{ type: 'text', value: text }] : [{ type: 'text', value: '' }],
562
+ tool_use,
563
+ token_usage: anthropicUsageToTokenUsage(result.usage),
564
+ finish_reason: tool_use ? 'tool_use' : claudeFinishReason(result?.stop_reason ?? ''),
565
+ conversation: processedConversation,
566
+ };
567
+ }
568
+ /**
569
+ * Execute a streaming Claude completion.
570
+ * Works with any Anthropic-compatible client (Anthropic or AnthropicVertex).
571
+ */
572
+ async function streamClaudeCompletion(client, prompt, options) {
573
+ const model_options = options.model_options;
574
+ const conversation = updateClaudeConversation(options.conversation, prompt);
575
+ const { payload, requestOptions } = getClaudePayload(options, conversation);
576
+ const streamingPayload = { ...payload, stream: true };
577
+ const response_stream = await client.messages.stream(streamingPayload, requestOptions);
578
+ let currentToolUse = null;
579
+ let pendingSpacing = false;
580
+ const stream = (0, async_1.asyncMap)(response_stream, async (streamEvent) => {
581
+ switch (streamEvent.type) {
582
+ case 'message_start':
583
+ return {
584
+ result: [{ type: 'text', value: '' }],
585
+ token_usage: anthropicUsageToTokenUsage(streamEvent.message.usage),
586
+ };
587
+ case 'message_delta':
588
+ return {
589
+ result: [{ type: 'text', value: '' }],
590
+ token_usage: { result: streamEvent.usage.output_tokens },
591
+ finish_reason: claudeFinishReason(streamEvent.delta.stop_reason ?? undefined),
592
+ };
593
+ case 'content_block_start':
594
+ if (streamEvent.content_block.type === 'tool_use') {
595
+ currentToolUse = { id: streamEvent.content_block.id, name: streamEvent.content_block.name, inputJson: '' };
596
+ return {
597
+ result: [],
598
+ tool_use: [{
599
+ id: streamEvent.content_block.id,
600
+ tool_name: streamEvent.content_block.name,
601
+ tool_input: '',
602
+ }],
603
+ };
604
+ }
605
+ if (streamEvent.content_block.type === 'redacted_thinking' && model_options?.include_thoughts) {
606
+ return {
607
+ result: [{ type: 'text', value: `[Redacted thinking: ${streamEvent.content_block.data}]` }],
608
+ };
609
+ }
610
+ break;
611
+ case 'content_block_delta':
612
+ switch (streamEvent.delta.type) {
613
+ case 'text_delta': {
614
+ const prefix = pendingSpacing ? '\n\n' : '';
615
+ pendingSpacing = false;
616
+ return {
617
+ result: streamEvent.delta.text ? [{ type: 'text', value: prefix + streamEvent.delta.text }] : [],
618
+ };
619
+ }
620
+ case 'input_json_delta':
621
+ if (currentToolUse && streamEvent.delta.partial_json) {
622
+ return {
623
+ result: [],
624
+ tool_use: [{
625
+ id: currentToolUse.id,
626
+ tool_name: '',
627
+ tool_input: streamEvent.delta.partial_json,
628
+ }],
629
+ };
630
+ }
631
+ break;
632
+ case 'thinking_delta':
633
+ if (model_options?.include_thoughts) {
634
+ return {
635
+ result: streamEvent.delta.thinking ? [{ type: 'text', value: streamEvent.delta.thinking }] : [],
636
+ };
637
+ }
638
+ break;
639
+ case 'signature_delta':
640
+ if (model_options?.include_thoughts) {
641
+ pendingSpacing = true;
642
+ }
643
+ break;
644
+ }
645
+ break;
646
+ case 'content_block_stop':
647
+ if (currentToolUse) {
648
+ currentToolUse = null;
649
+ pendingSpacing = false;
650
+ }
651
+ break;
652
+ }
653
+ return { result: [] };
654
+ });
655
+ return stream;
656
+ }
657
+ // ============================================================================
658
+ // Error handling
659
+ // ============================================================================
660
+ function formatAnthropicLlumiverseError(error, context) {
661
+ if (error instanceof error_1.AnthropicError && !(error instanceof error_1.APIError)) {
662
+ // Client-side SDK error (e.g. "Streaming is required for operations that may take longer than 10 minutes").
663
+ // These are structural/configuration errors — retrying will never succeed.
664
+ const errorName = error.constructor?.name || 'AnthropicError';
665
+ return new core_1.LlumiverseError(`[${context.provider}] ${error.message}`, false, context, error, undefined, errorName);
666
+ }
667
+ if (!(error instanceof error_1.APIError)) {
668
+ // Not an Anthropic error — rethrow for default handling
669
+ throw error;
670
+ }
671
+ const apiError = error;
672
+ const httpStatusCode = apiError.status;
673
+ let message = apiError.message || String(error);
674
+ let errorType;
675
+ if (apiError.error && typeof apiError.error === 'object') {
676
+ const nested = apiError.error;
677
+ if (nested['error'] && typeof nested['error'] === 'object') {
678
+ const innerError = nested['error'];
679
+ errorType = innerError['type'];
680
+ if (typeof innerError['message'] === 'string') {
681
+ message = innerError['message'];
682
+ }
683
+ }
684
+ }
685
+ let userMessage = message;
686
+ if (httpStatusCode)
687
+ userMessage = `[${httpStatusCode}] ${userMessage}`;
688
+ if (errorType && errorType !== 'error')
689
+ userMessage = `${errorType}: ${userMessage}`;
690
+ if (apiError.requestID)
691
+ userMessage += ` (Request ID: ${apiError.requestID})`;
692
+ const retryable = isClaudeErrorRetryable(error, httpStatusCode, errorType, apiError.headers ?? undefined);
693
+ const errorName = error.constructor?.name || 'AnthropicError';
694
+ return new core_1.LlumiverseError(`[${context.provider}] ${userMessage}`, retryable, context, error, httpStatusCode, errorName);
695
+ }
696
+ function isClaudeErrorRetryable(error, httpStatusCode, errorType, headers) {
697
+ // Honour the server's explicit retry directive first (mirrors SDK shouldRetry logic).
698
+ const shouldRetryHeader = headers?.get('x-should-retry');
699
+ if (shouldRetryHeader === 'true')
700
+ return true;
701
+ if (shouldRetryHeader === 'false')
702
+ return false;
703
+ if (error instanceof error_1.APIUserAbortError)
704
+ return false;
705
+ if (error instanceof error_1.RateLimitError)
706
+ return true;
707
+ if (error instanceof error_1.InternalServerError)
708
+ return true;
709
+ if (error instanceof error_1.APIConnectionTimeoutError)
710
+ return true;
711
+ if (error instanceof error_1.BadRequestError)
712
+ return false;
713
+ if (error instanceof error_1.AuthenticationError)
714
+ return false;
715
+ if (error instanceof error_1.PermissionDeniedError)
716
+ return false;
717
+ if (error instanceof error_1.NotFoundError)
718
+ return false;
719
+ if (error instanceof error_1.ConflictError)
720
+ return true; // SDK retries 409 (lock timeouts)
721
+ if (error instanceof error_1.UnprocessableEntityError)
722
+ return false;
723
+ if (errorType === 'invalid_request_error')
724
+ return false;
725
+ if (httpStatusCode !== undefined) {
726
+ if (httpStatusCode === 429 || httpStatusCode === 408 || httpStatusCode === 529)
727
+ return true;
728
+ if (httpStatusCode >= 500 && httpStatusCode < 600)
729
+ return true;
730
+ if (httpStatusCode >= 400 && httpStatusCode < 500)
731
+ return false;
732
+ }
733
+ if (error instanceof error_1.APIConnectionError && !(error instanceof error_1.APIConnectionTimeoutError))
734
+ return true;
735
+ return undefined;
736
+ }
737
+ //# sourceMappingURL=claude-messages.js.map