@llumiverse/drivers 1.0.0-dev.20260224.234313Z → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/lib/cjs/bedrock/converse.js +86 -12
  2. package/lib/cjs/bedrock/converse.js.map +1 -1
  3. package/lib/cjs/bedrock/index.js +208 -1
  4. package/lib/cjs/bedrock/index.js.map +1 -1
  5. package/lib/cjs/groq/index.js +7 -4
  6. package/lib/cjs/groq/index.js.map +1 -1
  7. package/lib/cjs/openai/index.js +457 -26
  8. package/lib/cjs/openai/index.js.map +1 -1
  9. package/lib/cjs/openai/openai_compatible.js +1 -0
  10. package/lib/cjs/openai/openai_compatible.js.map +1 -1
  11. package/lib/cjs/vertexai/index.js +42 -0
  12. package/lib/cjs/vertexai/index.js.map +1 -1
  13. package/lib/cjs/vertexai/models/claude.js +230 -2
  14. package/lib/cjs/vertexai/models/claude.js.map +1 -1
  15. package/lib/cjs/vertexai/models/gemini.js +261 -41
  16. package/lib/cjs/vertexai/models/gemini.js.map +1 -1
  17. package/lib/cjs/vertexai/models.js +1 -1
  18. package/lib/cjs/vertexai/models.js.map +1 -1
  19. package/lib/esm/bedrock/converse.js +80 -6
  20. package/lib/esm/bedrock/converse.js.map +1 -1
  21. package/lib/esm/bedrock/index.js +207 -2
  22. package/lib/esm/bedrock/index.js.map +1 -1
  23. package/lib/esm/groq/index.js +7 -4
  24. package/lib/esm/groq/index.js.map +1 -1
  25. package/lib/esm/openai/index.js +456 -27
  26. package/lib/esm/openai/index.js.map +1 -1
  27. package/lib/esm/openai/openai_compatible.js +1 -0
  28. package/lib/esm/openai/openai_compatible.js.map +1 -1
  29. package/lib/esm/vertexai/index.js +43 -1
  30. package/lib/esm/vertexai/index.js.map +1 -1
  31. package/lib/esm/vertexai/models/claude.js +229 -3
  32. package/lib/esm/vertexai/models/claude.js.map +1 -1
  33. package/lib/esm/vertexai/models/gemini.js +262 -43
  34. package/lib/esm/vertexai/models/gemini.js.map +1 -1
  35. package/lib/esm/vertexai/models.js +1 -1
  36. package/lib/esm/vertexai/models.js.map +1 -1
  37. package/lib/types/bedrock/converse.d.ts +1 -2
  38. package/lib/types/bedrock/converse.d.ts.map +1 -1
  39. package/lib/types/bedrock/index.d.ts +53 -1
  40. package/lib/types/bedrock/index.d.ts.map +1 -1
  41. package/lib/types/openai/index.d.ts +96 -1
  42. package/lib/types/openai/index.d.ts.map +1 -1
  43. package/lib/types/openai/openai_compatible.d.ts +5 -0
  44. package/lib/types/openai/openai_compatible.d.ts.map +1 -1
  45. package/lib/types/openai/openai_format.d.ts +1 -1
  46. package/lib/types/vertexai/index.d.ts +11 -1
  47. package/lib/types/vertexai/index.d.ts.map +1 -1
  48. package/lib/types/vertexai/models/claude.d.ts +64 -1
  49. package/lib/types/vertexai/models/claude.d.ts.map +1 -1
  50. package/lib/types/vertexai/models/gemini.d.ts +61 -1
  51. package/lib/types/vertexai/models/gemini.d.ts.map +1 -1
  52. package/lib/types/vertexai/models.d.ts +6 -1
  53. package/lib/types/vertexai/models.d.ts.map +1 -1
  54. package/package.json +9 -9
  55. package/src/bedrock/converse.ts +85 -10
  56. package/src/bedrock/error-handling.test.ts +352 -0
  57. package/src/bedrock/index.ts +225 -1
  58. package/src/groq/index.ts +9 -4
  59. package/src/openai/error-handling.test.ts +567 -0
  60. package/src/openai/index.ts +505 -29
  61. package/src/openai/openai_compatible.ts +7 -0
  62. package/src/openai/openai_format.ts +1 -1
  63. package/src/vertexai/index.ts +56 -5
  64. package/src/vertexai/models/claude-error-handling.test.ts +432 -0
  65. package/src/vertexai/models/claude.ts +273 -7
  66. package/src/vertexai/models/gemini-error-handling.test.ts +353 -0
  67. package/src/vertexai/models/gemini.ts +304 -48
  68. package/src/vertexai/models.ts +7 -2
@@ -1,3 +1,4 @@
1
+ import type { ClientOptions as AnthropicVertexClientOptions } from "@anthropic-ai/vertex-sdk";
1
2
  import { AnthropicVertex } from "@anthropic-ai/vertex-sdk";
2
3
  import { PredictionServiceClient, v1beta1 } from "@google-cloud/aiplatform";
3
4
  import { Content, GoogleGenAI, Model } from "@google/genai";
@@ -11,18 +12,20 @@ import {
11
12
  EmbeddingsOptions,
12
13
  EmbeddingsResult,
13
14
  ExecutionOptions,
15
+ LlumiverseError,
16
+ LlumiverseErrorContext,
14
17
  ModelSearchPayload,
15
18
  PromptSegment,
19
+ getConversationMeta,
16
20
  getModelCapabilities,
21
+ incrementConversationTurn,
17
22
  modelModalitiesToArray,
18
23
  stripBase64ImagesFromConversation,
24
+ stripHeartbeatsFromConversation,
19
25
  truncateLargeTextInConversation,
20
- getConversationMeta,
21
- incrementConversationTurn,
22
26
  } from "@llumiverse/core";
23
27
  import { FetchClient } from "@vertesia/api-fetch-client";
24
- import { GoogleAuth, GoogleAuthOptions, AuthClient } from "google-auth-library";
25
- import type { ClientOptions as AnthropicVertexClientOptions } from "@anthropic-ai/vertex-sdk";
28
+ import { AuthClient, GoogleAuth, GoogleAuthOptions } from "google-auth-library";
26
29
  import { getEmbeddingsForImages } from "./embeddings/embeddings-image.js";
27
30
  import { TextEmbeddingsOptions, getEmbeddingsForText } from "./embeddings/embeddings-text.js";
28
31
  import { getModelDefinition } from "./models.js";
@@ -347,8 +350,22 @@ export class VertexAIDriver extends AbstractDriver<VertexAIDriverOptions, Vertex
347
350
  };
348
351
  let processedConversation = stripBase64ImagesFromConversation(conversation, stripOptions);
349
352
  processedConversation = truncateLargeTextInConversation(processedConversation, stripOptions);
353
+ processedConversation = stripHeartbeatsFromConversation(processedConversation, {
354
+ keepForTurns: options.stripHeartbeatsAfterTurns ?? 1,
355
+ currentTurn,
356
+ });
357
+
358
+ // Preserve system instruction in conversation for Gemini multi-turn support.
359
+ // The Gemini API takes system as a separate parameter (not in contents),
360
+ // so we must store it in the conversation wrapper to survive serialization.
361
+ const geminiPrompt = prompt as GenerateContentPrompt;
362
+ if (geminiPrompt.system) {
363
+ if (typeof processedConversation === 'object' && processedConversation !== null) {
364
+ processedConversation = { ...processedConversation as object, _llumiverse_system: geminiPrompt.system };
365
+ }
366
+ }
350
367
 
351
- return processedConversation as Content[];
368
+ return processedConversation;
352
369
  }
353
370
 
354
371
  /**
@@ -440,6 +457,10 @@ export class VertexAIDriver extends AbstractDriver<VertexAIDriverOptions, Vertex
440
457
  };
441
458
  let processedConversation = stripBase64ImagesFromConversation(withTurn, stripOptions);
442
459
  processedConversation = truncateLargeTextInConversation(processedConversation, stripOptions);
460
+ processedConversation = stripHeartbeatsFromConversation(processedConversation, {
461
+ keepForTurns: options.stripHeartbeatsAfterTurns ?? 1,
462
+ currentTurn,
463
+ });
443
464
 
444
465
  return processedConversation;
445
466
  }
@@ -698,6 +719,36 @@ export class VertexAIDriver extends AbstractDriver<VertexAIDriverOptions, Vertex
698
719
  this.modelGarden?.close();
699
720
  this.imagenClient?.close();
700
721
  }
722
+
723
+ /**
724
+ * Format VertexAI errors by routing to model-specific error handlers.
725
+ * Each model definition (Gemini, Claude, Llama) can provide custom error parsing
726
+ * based on their specific SDK error structures.
727
+ *
728
+ * @param error - The error from the VertexAI/model SDK
729
+ * @param context - Context about where the error occurred
730
+ * @returns A standardized LlumiverseError
731
+ */
732
+ public formatLlumiverseError(
733
+ error: unknown,
734
+ context: LlumiverseErrorContext
735
+ ): LlumiverseError {
736
+ // Get the model definition for this request
737
+ const modelDef = getModelDefinition(context.model);
738
+
739
+ // If the model definition provides custom error handling, use it
740
+ if (modelDef.formatLlumiverseError) {
741
+ try {
742
+ return modelDef.formatLlumiverseError(this, error, context);
743
+ } catch (formattingError) {
744
+ // If model-specific handler throws, fall through to default handling
745
+ // This allows model handlers to explicitly opt out for certain errors
746
+ }
747
+ }
748
+
749
+ // Fall back to default AbstractDriver error handling
750
+ return super.formatLlumiverseError(error, context);
751
+ }
701
752
  }
702
753
 
703
754
  //'us-central1-aiplatform.googleapis.com',
@@ -0,0 +1,432 @@
1
+ import {
2
+ APIConnectionError,
3
+ APIConnectionTimeoutError,
4
+ APIError,
5
+ AuthenticationError,
6
+ BadRequestError,
7
+ ConflictError,
8
+ InternalServerError,
9
+ NotFoundError,
10
+ PermissionDeniedError,
11
+ RateLimitError,
12
+ UnprocessableEntityError,
13
+ } from '@anthropic-ai/sdk/error';
14
+ import { LlumiverseError } from '@llumiverse/core';
15
+ import { beforeEach, describe, expect, it } from 'vitest';
16
+ import { VertexAIDriver } from '../index.js';
17
+ import { ClaudeModelDefinition } from './claude.js';
18
+
19
+ describe('ClaudeModelDefinition Error Handling', () => {
20
+ let modelDef: ClaudeModelDefinition;
21
+ let driver: VertexAIDriver;
22
+
23
+ beforeEach(() => {
24
+ modelDef = new ClaudeModelDefinition('claude-haiku-4-5');
25
+ driver = {
26
+ provider: 'vertexai',
27
+ logger: { warn: () => { }, info: () => { }, error: () => { } },
28
+ } as any;
29
+ });
30
+
31
+ describe('formatLlumiverseError', () => {
32
+ it('should handle BadRequestError with status code in message', () => {
33
+ const headers = new Headers();
34
+ headers.set('request-id', 'req_test_123');
35
+
36
+ const anthropicError = new BadRequestError(
37
+ 400,
38
+ {
39
+ type: 'error',
40
+ error: {
41
+ type: 'invalid_request_error',
42
+ message: 'temperature: range: 0..1'
43
+ }
44
+ },
45
+ '400 {"type":"error","error":{"type":"invalid_request_error","message":"temperature: range: 0..1"},"request_id":"req_test_123"}',
46
+ headers
47
+ );
48
+
49
+ const error = modelDef.formatLlumiverseError(driver, anthropicError, {
50
+ provider: 'vertexai',
51
+ model: 'claude-haiku-4-5',
52
+ operation: 'execute',
53
+ });
54
+
55
+ expect(error).toBeInstanceOf(LlumiverseError);
56
+ expect(error.code).toBe(400);
57
+ expect(error.message).toContain('[400]');
58
+ expect(error.message).toContain('temperature: range: 0..1');
59
+ expect(error.message).toContain('invalid_request_error');
60
+ expect(error.message).toContain('req_test_123');
61
+ expect(error.name).toBe('BadRequestError');
62
+ expect(error.retryable).toBe(false);
63
+ });
64
+
65
+ it('should handle RateLimitError as retryable', () => {
66
+ const anthropicError = new RateLimitError(
67
+ 429,
68
+ { type: 'error', error: { type: 'rate_limit_error', message: 'Rate limit exceeded' } },
69
+ 'Rate limit exceeded',
70
+ new Headers()
71
+ );
72
+
73
+ const error = modelDef.formatLlumiverseError(driver, anthropicError, {
74
+ provider: 'vertexai',
75
+ model: 'claude-haiku-4-5',
76
+ operation: 'execute',
77
+ });
78
+
79
+ expect(error.code).toBe(429);
80
+ expect(error.retryable).toBe(true);
81
+ expect(error.name).toBe('RateLimitError');
82
+ });
83
+
84
+ it('should handle InternalServerError as retryable', () => {
85
+ const anthropicError = new InternalServerError(
86
+ 500,
87
+ { type: 'error', error: { type: 'internal_error', message: 'Internal server error' } },
88
+ 'Internal server error',
89
+ new Headers()
90
+ );
91
+
92
+ const error = modelDef.formatLlumiverseError(driver, anthropicError, {
93
+ provider: 'vertexai',
94
+ model: 'claude-haiku-4-5',
95
+ operation: 'execute',
96
+ });
97
+
98
+ expect(error.code).toBe(500);
99
+ expect(error.retryable).toBe(true);
100
+ expect(error.name).toBe('InternalServerError');
101
+ });
102
+
103
+ it('should handle AuthenticationError as not retryable', () => {
104
+ const anthropicError = new AuthenticationError(
105
+ 401,
106
+ { type: 'error', error: { type: 'authentication_error', message: 'Invalid API key' } },
107
+ 'Invalid API key',
108
+ new Headers()
109
+ );
110
+
111
+ const error = modelDef.formatLlumiverseError(driver, anthropicError, {
112
+ provider: 'vertexai',
113
+ model: 'claude-haiku-4-5',
114
+ operation: 'execute',
115
+ });
116
+
117
+ expect(error.code).toBe(401);
118
+ expect(error.retryable).toBe(false);
119
+ expect(error.name).toBe('AuthenticationError');
120
+ });
121
+
122
+ it('should handle PermissionDeniedError as not retryable', () => {
123
+ const anthropicError = new PermissionDeniedError(
124
+ 403,
125
+ { type: 'error', error: { type: 'permission_error', message: 'Insufficient permissions' } },
126
+ 'Insufficient permissions',
127
+ new Headers()
128
+ );
129
+
130
+ const error = modelDef.formatLlumiverseError(driver, anthropicError, {
131
+ provider: 'vertexai',
132
+ model: 'claude-haiku-4-5',
133
+ operation: 'execute',
134
+ });
135
+
136
+ expect(error.code).toBe(403);
137
+ expect(error.retryable).toBe(false);
138
+ expect(error.name).toBe('PermissionDeniedError');
139
+ });
140
+
141
+ it('should handle NotFoundError as not retryable', () => {
142
+ const anthropicError = new NotFoundError(
143
+ 404,
144
+ { type: 'error', error: { type: 'not_found_error', message: 'Model not found' } },
145
+ 'Model not found',
146
+ new Headers()
147
+ );
148
+
149
+ const error = modelDef.formatLlumiverseError(driver, anthropicError, {
150
+ provider: 'vertexai',
151
+ model: 'claude-haiku-4-5',
152
+ operation: 'execute',
153
+ });
154
+
155
+ expect(error.code).toBe(404);
156
+ expect(error.retryable).toBe(false);
157
+ expect(error.name).toBe('NotFoundError');
158
+ });
159
+
160
+ it('should handle ConflictError as not retryable', () => {
161
+ const anthropicError = new ConflictError(
162
+ 409,
163
+ { type: 'error', error: { type: 'conflict_error', message: 'Resource conflict' } },
164
+ 'Resource conflict',
165
+ new Headers()
166
+ );
167
+
168
+ const error = modelDef.formatLlumiverseError(driver, anthropicError, {
169
+ provider: 'vertexai',
170
+ model: 'claude-haiku-4-5',
171
+ operation: 'execute',
172
+ });
173
+
174
+ expect(error.code).toBe(409);
175
+ expect(error.retryable).toBe(false);
176
+ expect(error.name).toBe('ConflictError');
177
+ });
178
+
179
+ it('should handle UnprocessableEntityError as not retryable', () => {
180
+ const anthropicError = new UnprocessableEntityError(
181
+ 422,
182
+ { type: 'error', error: { type: 'validation_error', message: 'Validation failed' } },
183
+ 'Validation failed',
184
+ new Headers()
185
+ );
186
+
187
+ const error = modelDef.formatLlumiverseError(driver, anthropicError, {
188
+ provider: 'vertexai',
189
+ model: 'claude-haiku-4-5',
190
+ operation: 'execute',
191
+ });
192
+
193
+ expect(error.code).toBe(422);
194
+ expect(error.retryable).toBe(false);
195
+ expect(error.name).toBe('UnprocessableEntityError');
196
+ });
197
+
198
+ it('should handle APIConnectionTimeoutError as retryable', () => {
199
+ const anthropicError = new APIConnectionTimeoutError({ message: 'Request timed out' });
200
+
201
+ const error = modelDef.formatLlumiverseError(driver, anthropicError, {
202
+ provider: 'vertexai',
203
+ model: 'claude-haiku-4-5',
204
+ operation: 'execute',
205
+ });
206
+
207
+ expect(error.code).toBeUndefined();
208
+ expect(error.retryable).toBe(true);
209
+ expect(error.name).toBe('APIConnectionTimeoutError');
210
+ });
211
+
212
+ it('should handle APIConnectionError as retryable', () => {
213
+ const anthropicError = new APIConnectionError({ message: 'Connection failed' });
214
+
215
+ const error = modelDef.formatLlumiverseError(driver, anthropicError, {
216
+ provider: 'vertexai',
217
+ model: 'claude-haiku-4-5',
218
+ operation: 'execute',
219
+ });
220
+
221
+ expect(error.code).toBeUndefined();
222
+ expect(error.retryable).toBe(true);
223
+ expect(error.name).toBe('APIConnectionError');
224
+ });
225
+
226
+ it('should extract error type from nested error object', () => {
227
+ const anthropicError = new BadRequestError(
228
+ 400,
229
+ {
230
+ type: 'error',
231
+ error: {
232
+ type: 'invalid_request_error',
233
+ message: 'Missing required field'
234
+ }
235
+ },
236
+ 'Missing required field',
237
+ new Headers()
238
+ );
239
+
240
+ const error = modelDef.formatLlumiverseError(driver, anthropicError, {
241
+ provider: 'vertexai',
242
+ model: 'claude-haiku-4-5',
243
+ operation: 'execute',
244
+ });
245
+
246
+ expect(error.message).toContain('invalid_request_error');
247
+ expect(error.message).toContain('Missing required field');
248
+ });
249
+
250
+ it('should include request ID in message when available', () => {
251
+ const headers = new Headers();
252
+ headers.set('request-id', 'req_vrtx_test123');
253
+
254
+ const anthropicError = new BadRequestError(
255
+ 400,
256
+ { type: 'error', error: { type: 'invalid_request_error', message: 'Bad request' } },
257
+ 'Bad request',
258
+ headers
259
+ );
260
+
261
+ const error = modelDef.formatLlumiverseError(driver, anthropicError, {
262
+ provider: 'vertexai',
263
+ model: 'claude-haiku-4-5',
264
+ operation: 'execute',
265
+ });
266
+
267
+ expect(error.message).toContain('req_vrtx_test123');
268
+ });
269
+
270
+ it('should throw for non-Anthropic errors', () => {
271
+ const regularError = new Error('Regular error');
272
+
273
+ expect(() => {
274
+ modelDef.formatLlumiverseError(driver, regularError, {
275
+ provider: 'vertexai',
276
+ model: 'claude-haiku-4-5',
277
+ operation: 'execute',
278
+ });
279
+ }).toThrow('Regular error');
280
+ });
281
+
282
+ it('should preserve original error for debugging', () => {
283
+ const anthropicError = new BadRequestError(
284
+ 400,
285
+ { type: 'error', error: { type: 'invalid_request_error', message: 'Test error' } },
286
+ 'Test error',
287
+ new Headers()
288
+ );
289
+
290
+ const error = modelDef.formatLlumiverseError(driver, anthropicError, {
291
+ provider: 'vertexai',
292
+ model: 'claude-haiku-4-5',
293
+ operation: 'execute',
294
+ });
295
+
296
+ expect(error.originalError).toBe(anthropicError);
297
+ });
298
+ });
299
+
300
+ describe('isClaudeErrorRetryable', () => {
301
+ it('should classify retryable error types correctly', () => {
302
+ const retryableErrors = [
303
+ new RateLimitError(429, {}, 'Rate limit', new Headers()),
304
+ new InternalServerError(500, {}, 'Server error', new Headers()),
305
+ new APIConnectionTimeoutError({ message: 'Timeout' }),
306
+ ];
307
+
308
+ for (const error of retryableErrors) {
309
+ const result = (modelDef as any).isClaudeErrorRetryable(error, error.status, undefined);
310
+ expect(result, `${error.constructor.name} should be retryable`).toBe(true);
311
+ }
312
+ });
313
+
314
+ it('should classify non-retryable error types correctly', () => {
315
+ const nonRetryableErrors = [
316
+ new BadRequestError(400, {}, 'Bad request', new Headers()),
317
+ new AuthenticationError(401, {}, 'Auth error', new Headers()),
318
+ new PermissionDeniedError(403, {}, 'Permission denied', new Headers()),
319
+ new NotFoundError(404, {}, 'Not found', new Headers()),
320
+ new ConflictError(409, {}, 'Conflict', new Headers()),
321
+ new UnprocessableEntityError(422, {}, 'Validation error', new Headers()),
322
+ ];
323
+
324
+ for (const error of nonRetryableErrors) {
325
+ const result = (modelDef as any).isClaudeErrorRetryable(error, error.status, undefined);
326
+ expect(result, `${error.constructor.name} should not be retryable`).toBe(false);
327
+ }
328
+ });
329
+
330
+ it('should use HTTP status codes when available', () => {
331
+ const apiError = new APIError(429, {}, 'Too many requests', new Headers());
332
+ expect((modelDef as any).isClaudeErrorRetryable(apiError, 429, undefined)).toBe(true);
333
+
334
+ const apiError2 = new APIError(408, {}, 'Request timeout', new Headers());
335
+ expect((modelDef as any).isClaudeErrorRetryable(apiError2, 408, undefined)).toBe(true);
336
+
337
+ const apiError3 = new APIError(529, {}, 'Overloaded', new Headers());
338
+ expect((modelDef as any).isClaudeErrorRetryable(apiError3, 529, undefined)).toBe(true);
339
+
340
+ const apiError4 = new APIError(503, {}, 'Service unavailable', new Headers());
341
+ expect((modelDef as any).isClaudeErrorRetryable(apiError4, 503, undefined)).toBe(true);
342
+ });
343
+
344
+ it('should classify 4xx as non-retryable', () => {
345
+ const apiError = new APIError(400, {}, 'Bad request', new Headers());
346
+ expect((modelDef as any).isClaudeErrorRetryable(apiError, 400, undefined)).toBe(false);
347
+
348
+ const apiError2 = new APIError(403, {}, 'Forbidden', new Headers());
349
+ expect((modelDef as any).isClaudeErrorRetryable(apiError2, 403, undefined)).toBe(false);
350
+ });
351
+
352
+ it('should classify 5xx as retryable', () => {
353
+ const apiError = new APIError(500, {}, 'Internal error', new Headers());
354
+ expect((modelDef as any).isClaudeErrorRetryable(apiError, 500, undefined)).toBe(true);
355
+
356
+ const apiError2 = new APIError(502, {}, 'Bad gateway', new Headers());
357
+ expect((modelDef as any).isClaudeErrorRetryable(apiError2, 502, undefined)).toBe(true);
358
+ });
359
+
360
+ it('should classify invalid_request_error as non-retryable', () => {
361
+ const apiError = new APIError(400, {}, 'Invalid request', new Headers());
362
+ expect((modelDef as any).isClaudeErrorRetryable(apiError, 400, 'invalid_request_error')).toBe(false);
363
+ });
364
+
365
+ it('should classify APIConnectionError (non-timeout) as retryable', () => {
366
+ const connectionError = new APIConnectionError({ message: 'Network failure' });
367
+ expect((modelDef as any).isClaudeErrorRetryable(connectionError, undefined, undefined)).toBe(true);
368
+ });
369
+
370
+ it('should return undefined for unknown errors', () => {
371
+ const apiError = new APIError(undefined, {}, 'Unknown error', undefined as any);
372
+ expect((modelDef as any).isClaudeErrorRetryable(apiError, undefined, undefined)).toBeUndefined();
373
+ });
374
+ });
375
+
376
+ describe('VertexAIDriver error routing', () => {
377
+ it('should route to Claude-specific error handler', () => {
378
+ const headers = new Headers();
379
+ headers.set('request-id', 'req_test_routing');
380
+
381
+ const anthropicError = new RateLimitError(
382
+ 429,
383
+ {
384
+ type: 'error',
385
+ error: {
386
+ type: 'rate_limit_error',
387
+ message: 'Rate limit exceeded'
388
+ }
389
+ },
390
+ 'Rate limit exceeded',
391
+ headers
392
+ );
393
+
394
+ const error = modelDef.formatLlumiverseError(driver, anthropicError, {
395
+ provider: 'vertexai',
396
+ model: 'claude-haiku-4-5',
397
+ operation: 'execute',
398
+ });
399
+
400
+ expect(error).toBeInstanceOf(LlumiverseError);
401
+ expect(error.code).toBe(429);
402
+ expect(error.retryable).toBe(true);
403
+ expect(error.message).toContain('rate_limit_error');
404
+ expect(error.message).toContain('req_test_routing');
405
+ expect(error.name).toBe('RateLimitError');
406
+ });
407
+
408
+ it('should work with different Claude model versions', () => {
409
+ const models = ['claude-haiku-4-5', 'claude-3-7-sonnet-20250219', 'claude-opus-4-5'];
410
+
411
+ models.forEach((model) => {
412
+ const modelDef = new ClaudeModelDefinition(model);
413
+ const anthropicError = new BadRequestError(
414
+ 400,
415
+ { type: 'error', error: { type: 'invalid_request_error', message: 'Invalid parameter' } },
416
+ 'Invalid parameter',
417
+ new Headers()
418
+ );
419
+
420
+ const error = modelDef.formatLlumiverseError(driver, anthropicError, {
421
+ provider: 'vertexai',
422
+ model,
423
+ operation: 'execute',
424
+ });
425
+
426
+ expect(error.code).toBe(400);
427
+ expect(error.retryable).toBe(false);
428
+ expect(error.name).toBe('BadRequestError');
429
+ });
430
+ });
431
+ });
432
+ });