@llumiverse/drivers 1.0.0-dev.20260224.234313Z → 1.0.0-dev.20260331.080752Z
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.
- package/lib/cjs/bedrock/converse.js +86 -12
- package/lib/cjs/bedrock/converse.js.map +1 -1
- package/lib/cjs/bedrock/index.js +208 -1
- package/lib/cjs/bedrock/index.js.map +1 -1
- package/lib/cjs/groq/index.js +7 -4
- package/lib/cjs/groq/index.js.map +1 -1
- package/lib/cjs/openai/index.js +457 -26
- package/lib/cjs/openai/index.js.map +1 -1
- package/lib/cjs/openai/openai_compatible.js +1 -0
- package/lib/cjs/openai/openai_compatible.js.map +1 -1
- package/lib/cjs/vertexai/index.js +42 -0
- package/lib/cjs/vertexai/index.js.map +1 -1
- package/lib/cjs/vertexai/models/claude.js +230 -2
- package/lib/cjs/vertexai/models/claude.js.map +1 -1
- package/lib/cjs/vertexai/models/gemini.js +261 -41
- package/lib/cjs/vertexai/models/gemini.js.map +1 -1
- package/lib/cjs/vertexai/models.js +1 -1
- package/lib/cjs/vertexai/models.js.map +1 -1
- package/lib/esm/bedrock/converse.js +80 -6
- package/lib/esm/bedrock/converse.js.map +1 -1
- package/lib/esm/bedrock/index.js +207 -2
- package/lib/esm/bedrock/index.js.map +1 -1
- package/lib/esm/groq/index.js +7 -4
- package/lib/esm/groq/index.js.map +1 -1
- package/lib/esm/openai/index.js +456 -27
- package/lib/esm/openai/index.js.map +1 -1
- package/lib/esm/openai/openai_compatible.js +1 -0
- package/lib/esm/openai/openai_compatible.js.map +1 -1
- package/lib/esm/vertexai/index.js +43 -1
- package/lib/esm/vertexai/index.js.map +1 -1
- package/lib/esm/vertexai/models/claude.js +229 -3
- package/lib/esm/vertexai/models/claude.js.map +1 -1
- package/lib/esm/vertexai/models/gemini.js +262 -43
- package/lib/esm/vertexai/models/gemini.js.map +1 -1
- package/lib/esm/vertexai/models.js +1 -1
- package/lib/esm/vertexai/models.js.map +1 -1
- package/lib/types/bedrock/converse.d.ts +1 -2
- package/lib/types/bedrock/converse.d.ts.map +1 -1
- package/lib/types/bedrock/index.d.ts +53 -1
- package/lib/types/bedrock/index.d.ts.map +1 -1
- package/lib/types/openai/index.d.ts +96 -1
- package/lib/types/openai/index.d.ts.map +1 -1
- package/lib/types/openai/openai_compatible.d.ts +5 -0
- package/lib/types/openai/openai_compatible.d.ts.map +1 -1
- package/lib/types/openai/openai_format.d.ts +1 -1
- package/lib/types/vertexai/index.d.ts +11 -1
- package/lib/types/vertexai/index.d.ts.map +1 -1
- package/lib/types/vertexai/models/claude.d.ts +64 -1
- package/lib/types/vertexai/models/claude.d.ts.map +1 -1
- package/lib/types/vertexai/models/gemini.d.ts +61 -1
- package/lib/types/vertexai/models/gemini.d.ts.map +1 -1
- package/lib/types/vertexai/models.d.ts +6 -1
- package/lib/types/vertexai/models.d.ts.map +1 -1
- package/package.json +9 -9
- package/src/bedrock/converse.ts +85 -10
- package/src/bedrock/error-handling.test.ts +352 -0
- package/src/bedrock/index.ts +225 -1
- package/src/groq/index.ts +9 -4
- package/src/openai/error-handling.test.ts +567 -0
- package/src/openai/index.ts +505 -29
- package/src/openai/openai_compatible.ts +7 -0
- package/src/openai/openai_format.ts +1 -1
- package/src/vertexai/index.ts +56 -5
- package/src/vertexai/models/claude-error-handling.test.ts +432 -0
- package/src/vertexai/models/claude.ts +273 -7
- package/src/vertexai/models/gemini-error-handling.test.ts +353 -0
- package/src/vertexai/models/gemini.ts +304 -48
- package/src/vertexai/models.ts +7 -2
|
@@ -1,14 +1,37 @@
|
|
|
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';
|
|
1
14
|
import { ContentBlock, ContentBlockParam, DocumentBlockParam, ImageBlockParam, Message, MessageParam, TextBlockParam, ToolResultBlockParam } from "@anthropic-ai/sdk/resources/index.js";
|
|
15
|
+
import { MessageStreamParams } from "@anthropic-ai/sdk/resources/index.mjs";
|
|
16
|
+
import { MessageCreateParamsBase, MessageCreateParamsNonStreaming, RawMessageStreamEvent } from "@anthropic-ai/sdk/resources/messages.js";
|
|
2
17
|
import {
|
|
3
|
-
AIModel, Completion, CompletionChunkObject, ExecutionOptions,
|
|
4
|
-
|
|
5
|
-
|
|
18
|
+
AIModel, Completion, CompletionChunkObject, ExecutionOptions,
|
|
19
|
+
getConversationMeta,
|
|
20
|
+
getMaxTokensLimitVertexAi,
|
|
21
|
+
incrementConversationTurn,
|
|
22
|
+
JSONObject,
|
|
23
|
+
LlumiverseError, LlumiverseErrorContext,
|
|
24
|
+
ModelType,
|
|
25
|
+
PromptRole, PromptSegment, readStreamAsBase64, readStreamAsString, StatelessExecutionOptions,
|
|
26
|
+
stripBase64ImagesFromConversation,
|
|
27
|
+
stripHeartbeatsFromConversation,
|
|
28
|
+
ToolUse,
|
|
29
|
+
truncateLargeTextInConversation,
|
|
30
|
+
VertexAIClaudeOptions
|
|
6
31
|
} from "@llumiverse/core";
|
|
7
32
|
import { asyncMap } from "@llumiverse/core/async";
|
|
8
33
|
import { VertexAIDriver } from "../index.js";
|
|
9
34
|
import { ModelDefinition } from "../models.js";
|
|
10
|
-
import { MessageCreateParamsBase, MessageCreateParamsNonStreaming, RawMessageStreamEvent } from "@anthropic-ai/sdk/resources/messages.js";
|
|
11
|
-
import { MessageStreamParams } from "@anthropic-ai/sdk/resources/index.mjs";
|
|
12
35
|
|
|
13
36
|
export const ANTHROPIC_REGIONS: Record<string, string> = {
|
|
14
37
|
us: "us-east5",
|
|
@@ -301,6 +324,10 @@ export class ClaudeModelDefinition implements ModelDefinition<ClaudePrompt> {
|
|
|
301
324
|
};
|
|
302
325
|
let processedConversation = stripBase64ImagesFromConversation(conversation, stripOptions);
|
|
303
326
|
processedConversation = truncateLargeTextInConversation(processedConversation, stripOptions);
|
|
327
|
+
processedConversation = stripHeartbeatsFromConversation(processedConversation, {
|
|
328
|
+
keepForTurns: options.stripHeartbeatsAfterTurns ?? 1,
|
|
329
|
+
currentTurn,
|
|
330
|
+
});
|
|
304
331
|
|
|
305
332
|
return {
|
|
306
333
|
result: text ? [{ type: "text", value: text }] : [{ type: "text", value: '' }],
|
|
@@ -444,6 +471,170 @@ export class ClaudeModelDefinition implements ModelDefinition<ClaudePrompt> {
|
|
|
444
471
|
|
|
445
472
|
return stream;
|
|
446
473
|
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Format Anthropic API errors into LlumiverseError with proper status codes and retryability.
|
|
477
|
+
*
|
|
478
|
+
* Anthropic API errors have a specific structure:
|
|
479
|
+
* - APIError.status: HTTP status code (400, 401, 403, 404, 409, 422, 429, 500+)
|
|
480
|
+
* - APIError.error: Nested error object with type and message
|
|
481
|
+
* - APIError.requestID: Request ID for support (can be null)
|
|
482
|
+
*
|
|
483
|
+
* Common error types:
|
|
484
|
+
* - BadRequestError (400): Invalid request parameters
|
|
485
|
+
* - AuthenticationError (401): Authentication required
|
|
486
|
+
* - PermissionDeniedError (403): Insufficient permissions
|
|
487
|
+
* - NotFoundError (404): Resource not found
|
|
488
|
+
* - ConflictError (409): Resource conflict
|
|
489
|
+
* - UnprocessableEntityError (422): Validation error
|
|
490
|
+
* - RateLimitError (429): Rate limit exceeded
|
|
491
|
+
* - InternalServerError (500+): Server-side errors
|
|
492
|
+
* - APIConnectionError: Connection issues (no status code)
|
|
493
|
+
* - APIConnectionTimeoutError: Request timeout (no status code)
|
|
494
|
+
*
|
|
495
|
+
* @see https://docs.anthropic.com/en/api/errors
|
|
496
|
+
*/
|
|
497
|
+
formatLlumiverseError(
|
|
498
|
+
_driver: VertexAIDriver,
|
|
499
|
+
error: unknown,
|
|
500
|
+
context: LlumiverseErrorContext
|
|
501
|
+
): LlumiverseError {
|
|
502
|
+
// Check if it's an Anthropic API error
|
|
503
|
+
const isAnthropicError = this.isAnthropicApiError(error);
|
|
504
|
+
|
|
505
|
+
if (!isAnthropicError) {
|
|
506
|
+
// Not an Anthropic API error, use default handling
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const apiError = error as APIError;
|
|
511
|
+
const httpStatusCode = apiError.status;
|
|
512
|
+
|
|
513
|
+
// Extract error message and nested error details
|
|
514
|
+
let message = apiError.message || String(error);
|
|
515
|
+
|
|
516
|
+
// Extract error type from nested error object if available
|
|
517
|
+
let errorType: string | undefined;
|
|
518
|
+
if (apiError.error && typeof apiError.error === 'object') {
|
|
519
|
+
const nestedError = apiError.error as any;
|
|
520
|
+
if (nestedError.error && typeof nestedError.error === 'object') {
|
|
521
|
+
errorType = nestedError.error.type;
|
|
522
|
+
// Use the nested error message if it's more specific
|
|
523
|
+
if (nestedError.error.message) {
|
|
524
|
+
message = nestedError.error.message;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Build user-facing message with status code
|
|
530
|
+
let userMessage = message;
|
|
531
|
+
|
|
532
|
+
// Include status code in message (for end-user visibility)
|
|
533
|
+
if (httpStatusCode) {
|
|
534
|
+
userMessage = `[${httpStatusCode}] ${userMessage}`;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Include error type if available
|
|
538
|
+
if (errorType && errorType !== 'error') {
|
|
539
|
+
userMessage = `${errorType}: ${userMessage}`;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Add request ID if available (useful for Anthropic support)
|
|
543
|
+
if (apiError.requestID) {
|
|
544
|
+
userMessage += ` (Request ID: ${apiError.requestID})`;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Determine retryability based on Anthropic error types
|
|
548
|
+
const retryable = this.isClaudeErrorRetryable(error, httpStatusCode, errorType);
|
|
549
|
+
|
|
550
|
+
// Use the error constructor name as the error name
|
|
551
|
+
const errorName = error.constructor?.name || 'AnthropicError';
|
|
552
|
+
|
|
553
|
+
return new LlumiverseError(
|
|
554
|
+
`[${context.provider}] ${userMessage}`,
|
|
555
|
+
retryable,
|
|
556
|
+
context,
|
|
557
|
+
error,
|
|
558
|
+
httpStatusCode,
|
|
559
|
+
errorName
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Type guard to check if error is an Anthropic API error.
|
|
565
|
+
*/
|
|
566
|
+
private isAnthropicApiError(error: unknown): error is APIError {
|
|
567
|
+
return (
|
|
568
|
+
error !== null &&
|
|
569
|
+
typeof error === 'object' &&
|
|
570
|
+
error instanceof APIError
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Determine if an Anthropic API error is retryable.
|
|
576
|
+
*
|
|
577
|
+
* Retryable errors:
|
|
578
|
+
* - RateLimitError (429): Rate limit exceeded, retry with backoff
|
|
579
|
+
* - InternalServerError (500+): Server-side errors
|
|
580
|
+
* - APIConnectionTimeoutError: Request timeout
|
|
581
|
+
* - 408 (Request Timeout): Request timeout
|
|
582
|
+
* - 529 (Overloaded): Service overloaded
|
|
583
|
+
*
|
|
584
|
+
* Non-retryable errors:
|
|
585
|
+
* - BadRequestError (400): Invalid request parameters
|
|
586
|
+
* - AuthenticationError (401): Authentication failure
|
|
587
|
+
* - PermissionDeniedError (403): Insufficient permissions
|
|
588
|
+
* - NotFoundError (404): Resource not found
|
|
589
|
+
* - ConflictError (409): Resource conflict
|
|
590
|
+
* - UnprocessableEntityError (422): Validation error
|
|
591
|
+
* - Other 4xx client errors
|
|
592
|
+
* - invalid_request_error: Invalid request structure
|
|
593
|
+
*
|
|
594
|
+
* @param error - The error object
|
|
595
|
+
* @param httpStatusCode - The HTTP status code if available
|
|
596
|
+
* @param errorType - The nested error type if available
|
|
597
|
+
* @returns True if retryable, false if not retryable, undefined if unknown
|
|
598
|
+
*/
|
|
599
|
+
private isClaudeErrorRetryable(
|
|
600
|
+
error: unknown,
|
|
601
|
+
httpStatusCode: number | undefined,
|
|
602
|
+
errorType: string | undefined
|
|
603
|
+
): boolean | undefined {
|
|
604
|
+
// Check specific Anthropic error types by class
|
|
605
|
+
if (error instanceof RateLimitError) return true;
|
|
606
|
+
if (error instanceof InternalServerError) return true;
|
|
607
|
+
if (error instanceof APIConnectionTimeoutError) return true;
|
|
608
|
+
|
|
609
|
+
// Non-retryable by error type
|
|
610
|
+
if (error instanceof BadRequestError) return false;
|
|
611
|
+
if (error instanceof AuthenticationError) return false;
|
|
612
|
+
if (error instanceof PermissionDeniedError) return false;
|
|
613
|
+
if (error instanceof NotFoundError) return false;
|
|
614
|
+
if (error instanceof ConflictError) return false;
|
|
615
|
+
if (error instanceof UnprocessableEntityError) return false;
|
|
616
|
+
|
|
617
|
+
// Check nested error type
|
|
618
|
+
if (errorType === 'invalid_request_error') return false;
|
|
619
|
+
|
|
620
|
+
// Use HTTP status code
|
|
621
|
+
if (httpStatusCode !== undefined) {
|
|
622
|
+
if (httpStatusCode === 429) return true; // Rate limit
|
|
623
|
+
if (httpStatusCode === 408) return true; // Request timeout
|
|
624
|
+
if (httpStatusCode === 529) return true; // Overloaded
|
|
625
|
+
if (httpStatusCode >= 500 && httpStatusCode < 600) return true; // Server errors
|
|
626
|
+
if (httpStatusCode >= 400 && httpStatusCode < 500) return false; // Client errors
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Connection errors without status codes
|
|
630
|
+
if (error instanceof APIConnectionError && !(error instanceof APIConnectionTimeoutError)) {
|
|
631
|
+
// Generic connection errors might be retryable (network issues)
|
|
632
|
+
return true;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Unknown error type - let consumer decide retry strategy
|
|
636
|
+
return undefined;
|
|
637
|
+
}
|
|
447
638
|
}
|
|
448
639
|
|
|
449
640
|
function createPromptFromResponse(response: Message): ClaudePrompt {
|
|
@@ -679,7 +870,7 @@ function getClaudePayload(options: ExecutionOptions, prompt: ClaudePrompt): { pa
|
|
|
679
870
|
// Fix orphaned tool_use blocks (can occur when agent is stopped mid-tool-execution)
|
|
680
871
|
const fixedMessages = fixOrphanedToolUse(prompt.messages);
|
|
681
872
|
// Sanitize messages to remove empty text blocks (can occur from interrupted streaming)
|
|
682
|
-
|
|
873
|
+
let sanitizedMessages = sanitizeMessages(fixedMessages);
|
|
683
874
|
|
|
684
875
|
// Validate tools have input_schema.type set to 'object' as required by the Anthropic SDK
|
|
685
876
|
if (options.tools) {
|
|
@@ -690,10 +881,17 @@ function getClaudePayload(options: ExecutionOptions, prompt: ClaudePrompt): { pa
|
|
|
690
881
|
}
|
|
691
882
|
}
|
|
692
883
|
|
|
884
|
+
// When no tools are provided but conversation contains tool_use/tool_result blocks
|
|
885
|
+
// (e.g. checkpoint summary calls), convert tool blocks to text to avoid API errors
|
|
886
|
+
const hasTools = options.tools && options.tools.length > 0;
|
|
887
|
+
if (!hasTools && claudeMessagesContainToolBlocks(sanitizedMessages)) {
|
|
888
|
+
sanitizedMessages = convertClaudeToolBlocksToText(sanitizedMessages);
|
|
889
|
+
}
|
|
890
|
+
|
|
693
891
|
const payload = {
|
|
694
892
|
messages: sanitizedMessages,
|
|
695
893
|
system: prompt.system,
|
|
696
|
-
tools: options.tools as MessageCreateParamsBase['tools'],
|
|
894
|
+
tools: hasTools ? options.tools as MessageCreateParamsBase['tools'] : undefined,
|
|
697
895
|
temperature: model_options?.temperature,
|
|
698
896
|
model: modelName,
|
|
699
897
|
max_tokens: maxToken(options),
|
|
@@ -711,3 +909,71 @@ function getClaudePayload(options: ExecutionOptions, prompt: ClaudePrompt): { pa
|
|
|
711
909
|
|
|
712
910
|
return { payload, requestOptions };
|
|
713
911
|
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Checks whether any Claude message contains tool_use or tool_result content blocks.
|
|
915
|
+
*/
|
|
916
|
+
export function claudeMessagesContainToolBlocks(messages: MessageParam[]): boolean {
|
|
917
|
+
for (const msg of messages) {
|
|
918
|
+
if (!Array.isArray(msg.content)) continue;
|
|
919
|
+
for (const block of msg.content) {
|
|
920
|
+
if (typeof block === 'object' && block !== null && 'type' in block) {
|
|
921
|
+
if (block.type === 'tool_use' || block.type === 'tool_result') return true;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Converts tool_use and tool_result blocks to text in Claude messages.
|
|
930
|
+
* Preserves tool call information while removing structured blocks that
|
|
931
|
+
* require tools to be defined in the API request.
|
|
932
|
+
*/
|
|
933
|
+
export function convertClaudeToolBlocksToText(messages: MessageParam[]): MessageParam[] {
|
|
934
|
+
return messages.map(msg => {
|
|
935
|
+
if (!Array.isArray(msg.content)) return msg;
|
|
936
|
+
let hasToolBlocks = false;
|
|
937
|
+
for (const block of msg.content) {
|
|
938
|
+
if (typeof block === 'object' && block !== null && 'type' in block &&
|
|
939
|
+
(block.type === 'tool_use' || block.type === 'tool_result')) {
|
|
940
|
+
hasToolBlocks = true;
|
|
941
|
+
break;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
if (!hasToolBlocks) return msg;
|
|
945
|
+
|
|
946
|
+
const newContent: MessageParam['content'] = [];
|
|
947
|
+
for (const block of msg.content) {
|
|
948
|
+
if (typeof block === 'string') {
|
|
949
|
+
newContent.push(block);
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (block.type === 'tool_use') {
|
|
953
|
+
const inputStr = block.input ? JSON.stringify(block.input) : '';
|
|
954
|
+
const truncated = inputStr.length > 500 ? inputStr.substring(0, 500) + '...' : inputStr;
|
|
955
|
+
(newContent as Array<{ type: 'text'; text: string }>).push({
|
|
956
|
+
type: 'text',
|
|
957
|
+
text: `[Tool call: ${block.name}(${truncated})]`,
|
|
958
|
+
});
|
|
959
|
+
} else if (block.type === 'tool_result') {
|
|
960
|
+
let resultStr = 'No content';
|
|
961
|
+
if (typeof block.content === 'string') {
|
|
962
|
+
resultStr = block.content.length > 500 ? block.content.substring(0, 500) + '...' : block.content;
|
|
963
|
+
} else if (Array.isArray(block.content)) {
|
|
964
|
+
const texts = block.content
|
|
965
|
+
.filter((c): c is { type: 'text'; text: string } => c.type === 'text')
|
|
966
|
+
.map(c => c.text.length > 500 ? c.text.substring(0, 500) + '...' : c.text);
|
|
967
|
+
resultStr = texts.join('\n') || 'No text content';
|
|
968
|
+
}
|
|
969
|
+
(newContent as Array<{ type: 'text'; text: string }>).push({
|
|
970
|
+
type: 'text',
|
|
971
|
+
text: `[Tool result: ${resultStr}]`,
|
|
972
|
+
});
|
|
973
|
+
} else {
|
|
974
|
+
newContent.push(block as any);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return { ...msg, content: newContent };
|
|
978
|
+
});
|
|
979
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { LlumiverseError } from '@llumiverse/core';
|
|
2
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { VertexAIDriver } from '../index.js';
|
|
4
|
+
import { GeminiModelDefinition } from './gemini.js';
|
|
5
|
+
|
|
6
|
+
describe('GeminiModelDefinition Error Handling', () => {
|
|
7
|
+
let driver: VertexAIDriver;
|
|
8
|
+
let modelDef: GeminiModelDefinition;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
driver = new VertexAIDriver({
|
|
12
|
+
project: 'test-project',
|
|
13
|
+
region: 'us-central1',
|
|
14
|
+
});
|
|
15
|
+
modelDef = new GeminiModelDefinition('gemini-2.0-flash');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('formatLlumiverseError', () => {
|
|
19
|
+
it('should handle INVALID_ARGUMENT error (400)', () => {
|
|
20
|
+
const googleError = {
|
|
21
|
+
status: 400,
|
|
22
|
+
message: 'INVALID_ARGUMENT: Invalid value for temperature. Must be between 0 and 2.',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const error = modelDef.formatLlumiverseError(driver, googleError, {
|
|
26
|
+
provider: 'vertexai',
|
|
27
|
+
model: 'gemini-2.0-flash',
|
|
28
|
+
operation: 'execute',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(error).toBeInstanceOf(LlumiverseError);
|
|
32
|
+
expect(error.code).toBe(400);
|
|
33
|
+
expect(error.retryable).toBe(false);
|
|
34
|
+
expect(error.message).toContain('[400]');
|
|
35
|
+
expect(error.message).toContain('Invalid value for temperature');
|
|
36
|
+
expect(error.name).toBe('INVALID_ARGUMENT');
|
|
37
|
+
expect(error.context.provider).toBe('vertexai');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should handle UNAUTHENTICATED error (401)', () => {
|
|
41
|
+
const googleError = {
|
|
42
|
+
status: 401,
|
|
43
|
+
message: 'UNAUTHENTICATED: Request had invalid authentication credentials.',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const error = modelDef.formatLlumiverseError(driver, googleError, {
|
|
47
|
+
provider: 'vertexai',
|
|
48
|
+
model: 'gemini-2.0-flash',
|
|
49
|
+
operation: 'execute',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(error.retryable).toBe(false);
|
|
53
|
+
expect(error.code).toBe(401);
|
|
54
|
+
expect(error.name).toBe('UNAUTHENTICATED');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should handle PERMISSION_DENIED error (403)', () => {
|
|
58
|
+
const googleError = {
|
|
59
|
+
status: 403,
|
|
60
|
+
message: 'PERMISSION_DENIED: The caller does not have permission to execute the specified operation.',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const error = modelDef.formatLlumiverseError(driver, googleError, {
|
|
64
|
+
provider: 'vertexai',
|
|
65
|
+
model: 'gemini-2.0-flash',
|
|
66
|
+
operation: 'execute',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(error.retryable).toBe(false);
|
|
70
|
+
expect(error.code).toBe(403);
|
|
71
|
+
expect(error.name).toBe('PERMISSION_DENIED');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should handle NOT_FOUND error (404)', () => {
|
|
75
|
+
const googleError = {
|
|
76
|
+
status: 404,
|
|
77
|
+
message: 'NOT_FOUND: Requested entity was not found.',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const error = modelDef.formatLlumiverseError(driver, googleError, {
|
|
81
|
+
provider: 'vertexai',
|
|
82
|
+
model: 'gemini-2.0-flash',
|
|
83
|
+
operation: 'execute',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(error.retryable).toBe(false);
|
|
87
|
+
expect(error.code).toBe(404);
|
|
88
|
+
expect(error.name).toBe('NOT_FOUND');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should handle RESOURCE_EXHAUSTED error (429) as retryable', () => {
|
|
92
|
+
const googleError = {
|
|
93
|
+
status: 429,
|
|
94
|
+
message: 'RESOURCE_EXHAUSTED: Quota exceeded for quota metric',
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const error = modelDef.formatLlumiverseError(driver, googleError, {
|
|
98
|
+
provider: 'vertexai',
|
|
99
|
+
model: 'gemini-2.0-flash',
|
|
100
|
+
operation: 'execute',
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(error.retryable).toBe(true);
|
|
104
|
+
expect(error.code).toBe(429);
|
|
105
|
+
expect(error.message).toContain('[429]');
|
|
106
|
+
expect(error.name).toBe('RESOURCE_EXHAUSTED');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should handle INTERNAL error (500) as retryable', () => {
|
|
110
|
+
const googleError = {
|
|
111
|
+
status: 500,
|
|
112
|
+
message: 'INTERNAL: Internal server error',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const error = modelDef.formatLlumiverseError(driver, googleError, {
|
|
116
|
+
provider: 'vertexai',
|
|
117
|
+
model: 'gemini-2.0-flash',
|
|
118
|
+
operation: 'execute',
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(error.retryable).toBe(true);
|
|
122
|
+
expect(error.code).toBe(500);
|
|
123
|
+
expect(error.message).toContain('[500]');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should handle BAD_GATEWAY error (502) as retryable', () => {
|
|
127
|
+
const googleError = {
|
|
128
|
+
status: 502,
|
|
129
|
+
message: 'Bad gateway',
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const error = modelDef.formatLlumiverseError(driver, googleError, {
|
|
133
|
+
provider: 'vertexai',
|
|
134
|
+
model: 'gemini-2.0-flash',
|
|
135
|
+
operation: 'execute',
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(error.retryable).toBe(true);
|
|
139
|
+
expect(error.code).toBe(502);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should handle UNAVAILABLE error (503) as retryable', () => {
|
|
143
|
+
const googleError = {
|
|
144
|
+
status: 503,
|
|
145
|
+
message: 'UNAVAILABLE: The service is currently unavailable.',
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const error = modelDef.formatLlumiverseError(driver, googleError, {
|
|
149
|
+
provider: 'vertexai',
|
|
150
|
+
model: 'gemini-2.0-flash',
|
|
151
|
+
operation: 'execute',
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(error.retryable).toBe(true);
|
|
155
|
+
expect(error.code).toBe(503);
|
|
156
|
+
expect(error.name).toBe('UNAVAILABLE');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should handle DEADLINE_EXCEEDED error (504) as retryable', () => {
|
|
160
|
+
const googleError = {
|
|
161
|
+
status: 504,
|
|
162
|
+
message: 'DEADLINE_EXCEEDED: Request timeout',
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const error = modelDef.formatLlumiverseError(driver, googleError, {
|
|
166
|
+
provider: 'vertexai',
|
|
167
|
+
model: 'gemini-2.0-flash',
|
|
168
|
+
operation: 'execute',
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(error.retryable).toBe(true);
|
|
172
|
+
expect(error.code).toBe(504);
|
|
173
|
+
expect(error.name).toBe('DEADLINE_EXCEEDED');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should handle REQUEST_TIMEOUT error (408) as retryable', () => {
|
|
177
|
+
const googleError = {
|
|
178
|
+
status: 408,
|
|
179
|
+
message: 'Request timeout',
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const error = modelDef.formatLlumiverseError(driver, googleError, {
|
|
183
|
+
provider: 'vertexai',
|
|
184
|
+
model: 'gemini-2.0-flash',
|
|
185
|
+
operation: 'execute',
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(error.retryable).toBe(true);
|
|
189
|
+
expect(error.code).toBe(408);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should preserve original error for debugging', () => {
|
|
193
|
+
const googleError = {
|
|
194
|
+
status: 429,
|
|
195
|
+
message: 'RESOURCE_EXHAUSTED: Quota exceeded',
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const error = modelDef.formatLlumiverseError(driver, googleError, {
|
|
199
|
+
provider: 'vertexai',
|
|
200
|
+
model: 'gemini-2.0-flash',
|
|
201
|
+
operation: 'execute',
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(error.originalError).toBe(googleError);
|
|
205
|
+
expect((error.originalError as any).status).toBe(429);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should throw for non-Google API errors', () => {
|
|
209
|
+
const regularError = new Error('Regular error');
|
|
210
|
+
|
|
211
|
+
expect(() => {
|
|
212
|
+
modelDef.formatLlumiverseError(driver, regularError, {
|
|
213
|
+
provider: 'vertexai',
|
|
214
|
+
model: 'gemini-2.0-flash',
|
|
215
|
+
operation: 'execute',
|
|
216
|
+
});
|
|
217
|
+
}).toThrow();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should extract error name from bracket format', () => {
|
|
221
|
+
const googleError = {
|
|
222
|
+
status: 400,
|
|
223
|
+
message: '[INVALID_ARGUMENT] Invalid parameter',
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const error = modelDef.formatLlumiverseError(driver, googleError, {
|
|
227
|
+
provider: 'vertexai',
|
|
228
|
+
model: 'gemini-2.0-flash',
|
|
229
|
+
operation: 'execute',
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
expect(error.name).toBe('INVALID_ARGUMENT');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should extract error name from Error suffix format', () => {
|
|
236
|
+
const googleError = {
|
|
237
|
+
status: 400,
|
|
238
|
+
message: 'ValidationError: Invalid input',
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const error = modelDef.formatLlumiverseError(driver, googleError, {
|
|
242
|
+
provider: 'vertexai',
|
|
243
|
+
model: 'gemini-2.0-flash',
|
|
244
|
+
operation: 'execute',
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
expect(error.name).toBe('ValidationError');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should handle errors without extractable name', () => {
|
|
251
|
+
const googleError = {
|
|
252
|
+
status: 500,
|
|
253
|
+
message: 'Something went wrong',
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const error = modelDef.formatLlumiverseError(driver, googleError, {
|
|
257
|
+
provider: 'vertexai',
|
|
258
|
+
model: 'gemini-2.0-flash',
|
|
259
|
+
operation: 'execute',
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// When no name is extracted, defaults to 'LlumiverseError'
|
|
263
|
+
expect(error.name).toBe('LlumiverseError');
|
|
264
|
+
expect(error.code).toBe(500);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe('isGeminiErrorRetryable', () => {
|
|
269
|
+
it('should classify retryable status codes correctly', () => {
|
|
270
|
+
const retryableStatusCodes = [408, 429, 500, 502, 503, 504];
|
|
271
|
+
|
|
272
|
+
retryableStatusCodes.forEach((statusCode) => {
|
|
273
|
+
const result = (modelDef as any).isGeminiErrorRetryable(statusCode);
|
|
274
|
+
expect(result, `Status code ${statusCode} should be retryable`).toBe(true);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should classify non-retryable status codes correctly', () => {
|
|
279
|
+
const nonRetryableStatusCodes = [400, 401, 403, 404, 409];
|
|
280
|
+
|
|
281
|
+
nonRetryableStatusCodes.forEach((statusCode) => {
|
|
282
|
+
const result = (modelDef as any).isGeminiErrorRetryable(statusCode);
|
|
283
|
+
expect(result, `Status code ${statusCode} should not be retryable`).toBe(false);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should classify other 5xx errors as retryable', () => {
|
|
288
|
+
expect((modelDef as any).isGeminiErrorRetryable(501)).toBe(true);
|
|
289
|
+
expect((modelDef as any).isGeminiErrorRetryable(505)).toBe(true);
|
|
290
|
+
expect((modelDef as any).isGeminiErrorRetryable(599)).toBe(true);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should classify other 4xx errors as non-retryable', () => {
|
|
294
|
+
expect((modelDef as any).isGeminiErrorRetryable(402)).toBe(false);
|
|
295
|
+
expect((modelDef as any).isGeminiErrorRetryable(405)).toBe(false);
|
|
296
|
+
expect((modelDef as any).isGeminiErrorRetryable(499)).toBe(false);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe('VertexAIDriver error routing', () => {
|
|
301
|
+
it('should route to Gemini-specific error handler', () => {
|
|
302
|
+
const googleError = {
|
|
303
|
+
status: 429,
|
|
304
|
+
message: 'RESOURCE_EXHAUSTED: Quota exceeded',
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const error = driver.formatLlumiverseError(googleError, {
|
|
308
|
+
provider: 'vertexai',
|
|
309
|
+
model: 'gemini-2.0-flash',
|
|
310
|
+
operation: 'execute',
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
expect(error).toBeInstanceOf(LlumiverseError);
|
|
314
|
+
expect(error.code).toBe(429);
|
|
315
|
+
expect(error.retryable).toBe(true);
|
|
316
|
+
expect(error.name).toBe('RESOURCE_EXHAUSTED');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should fall back to default handler for non-Google errors', () => {
|
|
320
|
+
const regularError = new Error('Regular error');
|
|
321
|
+
|
|
322
|
+
const error = driver.formatLlumiverseError(regularError, {
|
|
323
|
+
provider: 'vertexai',
|
|
324
|
+
model: 'gemini-2.0-flash',
|
|
325
|
+
operation: 'execute',
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
expect(error).toBeInstanceOf(LlumiverseError);
|
|
329
|
+
expect(error.code).toBeUndefined();
|
|
330
|
+
expect(error.retryable).toBeUndefined(); // Unknown errors - let consumer decide
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should work with different Gemini model versions', () => {
|
|
334
|
+
const googleError = {
|
|
335
|
+
status: 400,
|
|
336
|
+
message: 'INVALID_ARGUMENT: Invalid parameter',
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const models = ['gemini-2.0-flash', 'gemini-2.5-flash', 'gemini-1.5-pro'];
|
|
340
|
+
|
|
341
|
+
models.forEach((model) => {
|
|
342
|
+
const error = driver.formatLlumiverseError(googleError, {
|
|
343
|
+
provider: 'vertexai',
|
|
344
|
+
model,
|
|
345
|
+
operation: 'execute',
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
expect(error.code).toBe(400);
|
|
349
|
+
expect(error.retryable).toBe(false);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
});
|