@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.
- 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,5 +1,5 @@
|
|
|
1
1
|
import { Content, GenerateContentResponseUsageMetadata } from "@google/genai";
|
|
2
|
-
import { AIModel, Completion, CompletionChunkObject, ExecutionOptions, ExecutionTokenUsage, PromptSegment } from "@llumiverse/core";
|
|
2
|
+
import { AIModel, Completion, CompletionChunkObject, ExecutionOptions, ExecutionTokenUsage, LlumiverseError, LlumiverseErrorContext, PromptSegment } from "@llumiverse/core";
|
|
3
3
|
import { GenerateContentPrompt, VertexAIDriver } from "../index.js";
|
|
4
4
|
import { ModelDefinition } from "../models.js";
|
|
5
5
|
export declare function mergeConsecutiveRole(contents: Content[] | undefined): Content[];
|
|
@@ -14,5 +14,65 @@ export declare class GeminiModelDefinition implements ModelDefinition<GenerateCo
|
|
|
14
14
|
usageMetadataToTokenUsage(usageMetadata: GenerateContentResponseUsageMetadata | undefined): ExecutionTokenUsage;
|
|
15
15
|
requestTextCompletion(driver: VertexAIDriver, prompt: GenerateContentPrompt, options: ExecutionOptions): Promise<Completion>;
|
|
16
16
|
requestTextCompletionStream(driver: VertexAIDriver, prompt: GenerateContentPrompt, options: ExecutionOptions): Promise<AsyncIterable<CompletionChunkObject>>;
|
|
17
|
+
/**
|
|
18
|
+
* Format Google API errors into LlumiverseError with proper status codes and retryability.
|
|
19
|
+
*
|
|
20
|
+
* Google API errors follow AIP-193 standard:
|
|
21
|
+
* - ApiError.status: HTTP status code
|
|
22
|
+
* - ApiError.message: Error message
|
|
23
|
+
*
|
|
24
|
+
* Common error codes:
|
|
25
|
+
* - 400 (INVALID_ARGUMENT): Invalid request parameters
|
|
26
|
+
* - 401 (UNAUTHENTICATED): Authentication required
|
|
27
|
+
* - 403 (PERMISSION_DENIED): Insufficient permissions
|
|
28
|
+
* - 404 (NOT_FOUND): Resource not found
|
|
29
|
+
* - 429 (RESOURCE_EXHAUSTED): Rate limit/quota exceeded
|
|
30
|
+
* - 500 (INTERNAL): Internal server error
|
|
31
|
+
* - 503 (UNAVAILABLE): Service temporarily unavailable
|
|
32
|
+
* - 504 (DEADLINE_EXCEEDED): Request timeout
|
|
33
|
+
*
|
|
34
|
+
* @see https://google.aip.dev/193
|
|
35
|
+
* @see https://docs.cloud.google.com/vertex-ai/generative-ai/docs/model-reference/api-errors
|
|
36
|
+
*/
|
|
37
|
+
formatLlumiverseError(_driver: VertexAIDriver, error: unknown, context: LlumiverseErrorContext): LlumiverseError;
|
|
38
|
+
/**
|
|
39
|
+
* Type guard to check if error is a Google API error.
|
|
40
|
+
*/
|
|
41
|
+
private isGoogleApiError;
|
|
42
|
+
/**
|
|
43
|
+
* Determine if a Google API error is retryable based on HTTP status code.
|
|
44
|
+
*
|
|
45
|
+
* Retryable errors (per Google AIP-194):
|
|
46
|
+
* - 408 (REQUEST_TIMEOUT): Request timeout
|
|
47
|
+
* - 429 (RESOURCE_EXHAUSTED): Rate limit exceeded, quota exhausted
|
|
48
|
+
* - 500 (INTERNAL): Internal server error
|
|
49
|
+
* - 502 (BAD_GATEWAY): Bad gateway
|
|
50
|
+
* - 503 (UNAVAILABLE): Service temporarily unavailable
|
|
51
|
+
* - 504 (DEADLINE_EXCEEDED): Gateway timeout
|
|
52
|
+
*
|
|
53
|
+
* Non-retryable errors:
|
|
54
|
+
* - 400 (INVALID_ARGUMENT): Invalid request parameters
|
|
55
|
+
* - 401 (UNAUTHENTICATED): Authentication required
|
|
56
|
+
* - 403 (PERMISSION_DENIED): Insufficient permissions
|
|
57
|
+
* - 404 (NOT_FOUND): Resource not found
|
|
58
|
+
* - 409 (CONFLICT): Resource conflict
|
|
59
|
+
* - Other 4xx client errors
|
|
60
|
+
*
|
|
61
|
+
* @param httpStatusCode - The HTTP status code from the API error
|
|
62
|
+
* @returns True if retryable, false if not retryable, undefined if unknown
|
|
63
|
+
*/
|
|
64
|
+
private isGeminiErrorRetryable;
|
|
65
|
+
/**
|
|
66
|
+
* Extract error type name from error message.
|
|
67
|
+
* Google errors often include the error type in the message.
|
|
68
|
+
* Examples: "INVALID_ARGUMENT", "RESOURCE_EXHAUSTED", "PERMISSION_DENIED"
|
|
69
|
+
*/
|
|
70
|
+
private extractErrorName;
|
|
17
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Converts functionCall and functionResponse parts to text parts in Gemini Content[].
|
|
74
|
+
* Preserves tool call information while removing structured parts that require
|
|
75
|
+
* tools/toolConfig to be defined in the API request.
|
|
76
|
+
*/
|
|
77
|
+
export declare function convertGeminiFunctionPartsToText(contents: Content[]): Content[];
|
|
18
78
|
//# sourceMappingURL=gemini.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gemini.d.ts","sourceRoot":"","sources":["../../../../src/vertexai/models/gemini.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"gemini.d.ts","sourceRoot":"","sources":["../../../../src/vertexai/models/gemini.ts"],"names":[],"mappings":"AACA,OAAO,EACH,OAAO,EACP,oCAAoC,EAMvC,MAAM,eAAe,CAAC;AACvB,OAAO,EACH,OAAO,EAAE,UAAU,EAAE,qBAAqB,EAAoB,gBAAgB,EAC9E,mBAAmB,EAKK,eAAe,EAAE,sBAAsB,EAC/D,aAAa,EAOhB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AA4e/C,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,SAAS,GAAG,OAAO,EAAE,CA2B/E;AAgED,qBAAa,qBAAsB,YAAW,eAAe,CAAC,qBAAqB,CAAC;IAEhF,KAAK,EAAE,OAAO,CAAA;gBAEF,OAAO,EAAE,MAAM;IAU3B,uBAAuB,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,gBAAgB,GAAG;QAAE,MAAM,EAAE,UAAU,CAAC;QAAC,OAAO,EAAE,gBAAgB,CAAA;KAAE;IAyBnH,YAAY,CAAC,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,aAAa,EAAE,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IA+HjI,yBAAyB,CAAC,aAAa,EAAE,oCAAoC,GAAG,SAAS,GAAG,mBAAmB;IA0BzG,qBAAqB,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,qBAAqB,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;IA8G5H,2BAA2B,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,qBAAqB,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,aAAa,CAAC,qBAAqB,CAAC,CAAC;IAiFlK;;;;;;;;;;;;;;;;;;;OAmBG;IACH,qBAAqB,CACjB,OAAO,EAAE,cAAc,EACvB,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,sBAAsB,GAChC,eAAe;IAwClB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAUxB;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,OAAO,CAAC,sBAAsB;IAgB9B;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;CAkB3B;AAGD;;;;GAIG;AACH,wBAAgB,gCAAgC,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAsB/E"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AIModel, Completion,
|
|
1
|
+
import { AIModel, Completion, CompletionChunkObject, ExecutionOptions, LlumiverseError, LlumiverseErrorContext, PromptSegment } from "@llumiverse/core";
|
|
2
2
|
import { VertexAIDriver } from "./index.js";
|
|
3
3
|
export interface ModelDefinition<PromptT = any> {
|
|
4
4
|
model: AIModel;
|
|
@@ -10,6 +10,11 @@ export interface ModelDefinition<PromptT = any> {
|
|
|
10
10
|
result: Completion;
|
|
11
11
|
options: ExecutionOptions;
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* Format provider-specific errors into standardized LlumiverseError.
|
|
15
|
+
* Optional - if not provided, VertexAIDriver will use default error handling.
|
|
16
|
+
*/
|
|
17
|
+
formatLlumiverseError?(driver: VertexAIDriver, error: unknown, context: LlumiverseErrorContext): LlumiverseError;
|
|
13
18
|
}
|
|
14
19
|
export declare function getModelDefinition(model: string): ModelDefinition;
|
|
15
20
|
//# sourceMappingURL=models.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../../src/vertexai/models.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../../src/vertexai/models.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,eAAe,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACxJ,OAAO,EAAE,cAAc,EAAiB,MAAM,YAAY,CAAC;AAK3D,MAAM,WAAW,eAAe,CAAC,OAAO,GAAG,GAAG;IAC1C,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,YAAY,EAAE,CAAC,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,aAAa,EAAE,EAAE,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACjH,qBAAqB,EAAE,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IACnH,2BAA2B,EAAE,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,aAAa,CAAC,qBAAqB,CAAC,CAAC,CAAC;IACnJ,uBAAuB,CAAC,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,gBAAgB,GAAG;QAAE,MAAM,EAAE,UAAU,CAAC;QAAC,OAAO,EAAE,gBAAgB,CAAA;KAAE,CAAC;IAC3H;;;OAGG;IACH,qBAAqB,CAAC,CAAC,MAAM,EAAE,cAAc,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,sBAAsB,GAAG,eAAe,CAAC;CACpH;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,eAAe,CA2BjE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llumiverse/drivers",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "LLM driver implementations. Currently supported are: openai, huggingface, bedrock, replicate.",
|
|
6
6
|
"files": [
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
"vitest": "^4.0.18"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@anthropic-ai/sdk": "^0.
|
|
52
|
-
"@anthropic-ai/vertex-sdk": "^0.14.
|
|
51
|
+
"@anthropic-ai/sdk": "^0.78.0",
|
|
52
|
+
"@anthropic-ai/vertex-sdk": "^0.14.4",
|
|
53
53
|
"@aws-sdk/client-bedrock": "^3.985.0",
|
|
54
54
|
"@aws-sdk/client-bedrock-runtime": "^3.985.0",
|
|
55
55
|
"@aws-sdk/client-s3": "^3.985.0",
|
|
@@ -62,19 +62,19 @@
|
|
|
62
62
|
"@azure/core-sse": "^2.3.0",
|
|
63
63
|
"@azure/identity": "^4.13.0",
|
|
64
64
|
"@azure/openai": "2.0.0",
|
|
65
|
-
"@google-cloud/aiplatform": "^6.
|
|
66
|
-
"@google/genai": "^1.
|
|
65
|
+
"@google-cloud/aiplatform": "^6.5.0",
|
|
66
|
+
"@google/genai": "^1.46.0",
|
|
67
67
|
"@huggingface/inference": "4.13.11",
|
|
68
|
-
"@vertesia/api-fetch-client": "^0.
|
|
68
|
+
"@vertesia/api-fetch-client": "^0.82.4",
|
|
69
69
|
"eventsource": "^4.1.0",
|
|
70
70
|
"google-auth-library": "^10.5.0",
|
|
71
71
|
"groq-sdk": "^0.37.0",
|
|
72
72
|
"mnemonist": "^0.40.3",
|
|
73
73
|
"node-web-stream-adapters": "^0.2.1",
|
|
74
|
-
"openai": "^6.
|
|
74
|
+
"openai": "^6.22.0",
|
|
75
75
|
"replicate": "^1.4.0",
|
|
76
|
-
"@llumiverse/common": "1.0.0
|
|
77
|
-
"@llumiverse/core": "1.0.0
|
|
76
|
+
"@llumiverse/common": "1.0.0",
|
|
77
|
+
"@llumiverse/core": "1.0.0"
|
|
78
78
|
},
|
|
79
79
|
"ts_dual_module": {
|
|
80
80
|
"outDir": "lib"
|
package/src/bedrock/converse.ts
CHANGED
|
@@ -1,39 +1,110 @@
|
|
|
1
|
-
import { DataSource, ExecutionOptions, readStreamAsString, readStreamAsUint8Array } from "@llumiverse/core";
|
|
2
|
-
import { PromptSegment, PromptRole } from "@llumiverse/core";
|
|
3
1
|
import {
|
|
2
|
+
ContentBlock,
|
|
4
3
|
ConversationRole,
|
|
5
4
|
ConverseRequest,
|
|
6
5
|
Message,
|
|
7
6
|
SystemContentBlock,
|
|
8
|
-
ContentBlock,
|
|
9
7
|
ToolResultContentBlock,
|
|
10
8
|
} from "@aws-sdk/client-bedrock-runtime";
|
|
9
|
+
import { DataSource, ExecutionOptions, PromptRole, PromptSegment, readStreamAsString, readStreamAsUint8Array } from "@llumiverse/core";
|
|
11
10
|
import { parseS3UrlToUri } from "./s3.js";
|
|
12
11
|
|
|
13
12
|
function roleConversion(role: PromptRole): ConversationRole {
|
|
14
13
|
return role === PromptRole.assistant ? ConversationRole.ASSISTANT : ConversationRole.USER;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
type BedrockImageFormat = "png" | "jpeg" | "gif" | "webp";
|
|
17
|
+
const BEDROCK_IMAGE_FORMATS = new Set<string>(["png", "jpeg", "gif", "webp"]);
|
|
18
|
+
const mimeToImageMap: Record<string, BedrockImageFormat> = {
|
|
19
|
+
"image/png": "png",
|
|
20
|
+
"image/jpeg": "jpeg",
|
|
21
|
+
"image/gif": "gif",
|
|
22
|
+
"image/webp": "webp",
|
|
23
|
+
};
|
|
24
|
+
function mimeToImageType(mime: string): BedrockImageFormat {
|
|
25
|
+
const mapped = mimeToImageMap[mime];
|
|
26
|
+
if (mapped) return mapped;
|
|
18
27
|
if (mime.startsWith("image/")) {
|
|
19
|
-
|
|
28
|
+
const subtype = mime.split("/")[1];
|
|
29
|
+
if (subtype && BEDROCK_IMAGE_FORMATS.has(subtype)) {
|
|
30
|
+
return subtype as BedrockImageFormat;
|
|
31
|
+
}
|
|
20
32
|
}
|
|
21
33
|
return "png";
|
|
22
34
|
}
|
|
23
35
|
|
|
24
|
-
|
|
36
|
+
type BedrockDocFormat = "pdf" | "csv" | "doc" | "docx" | "xls" | "xlsx" | "html" | "txt" | "md";
|
|
37
|
+
const BEDROCK_DOC_FORMATS = new Set<string>(["pdf", "csv", "doc", "docx", "xls", "xlsx", "html", "txt", "md"]);
|
|
38
|
+
const mimeToDocMap: Record<string, BedrockDocFormat> = {
|
|
39
|
+
"application/pdf": "pdf",
|
|
40
|
+
"text/csv": "csv",
|
|
41
|
+
"application/msword": "doc",
|
|
42
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
|
|
43
|
+
"application/vnd.ms-excel": "xls",
|
|
44
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
|
|
45
|
+
"text/html": "html",
|
|
46
|
+
"text/plain": "txt",
|
|
47
|
+
"text/markdown": "md",
|
|
48
|
+
};
|
|
49
|
+
function mimeToDocType(mime: string): BedrockDocFormat {
|
|
50
|
+
// 1. Exact map lookup (handles complex MIME types like vnd.openxmlformats-*)
|
|
51
|
+
const mapped = mimeToDocMap[mime];
|
|
52
|
+
if (mapped) return mapped;
|
|
53
|
+
// 2. Fallback: extract subtype for simple application/ or text/ MIME types
|
|
25
54
|
if (mime.startsWith("application/") || mime.startsWith("text/")) {
|
|
26
|
-
|
|
55
|
+
const subtype = mime.split("/")[1];
|
|
56
|
+
if (subtype && BEDROCK_DOC_FORMATS.has(subtype)) {
|
|
57
|
+
return subtype as BedrockDocFormat;
|
|
58
|
+
}
|
|
27
59
|
}
|
|
28
60
|
return "txt";
|
|
29
61
|
}
|
|
30
|
-
|
|
62
|
+
type BedrockVideoFormat = "mov" | "mkv" | "mp4" | "webm" | "flv" | "mpeg" | "mpg" | "wmv" | "three_gp";
|
|
63
|
+
const BEDROCK_VIDEO_FORMATS = new Set<string>(["mov", "mkv", "mp4", "webm", "flv", "mpeg", "mpg", "wmv", "three_gp"]);
|
|
64
|
+
const mimeToVideoMap: Record<string, BedrockVideoFormat> = {
|
|
65
|
+
"video/quicktime": "mov",
|
|
66
|
+
"video/x-matroska": "mkv",
|
|
67
|
+
"video/mp4": "mp4",
|
|
68
|
+
"video/webm": "webm",
|
|
69
|
+
"video/x-flv": "flv",
|
|
70
|
+
"video/mpeg": "mpeg",
|
|
71
|
+
"video/x-ms-wmv": "wmv",
|
|
72
|
+
"video/3gpp": "three_gp",
|
|
73
|
+
};
|
|
74
|
+
function mimeToVideoType(mime: string): BedrockVideoFormat {
|
|
75
|
+
const mapped = mimeToVideoMap[mime];
|
|
76
|
+
if (mapped) return mapped;
|
|
31
77
|
if (mime.startsWith("video/")) {
|
|
32
|
-
|
|
78
|
+
const subtype = mime.split("/")[1];
|
|
79
|
+
if (subtype && BEDROCK_VIDEO_FORMATS.has(subtype)) {
|
|
80
|
+
return subtype as BedrockVideoFormat;
|
|
81
|
+
}
|
|
33
82
|
}
|
|
34
83
|
return "mp4";
|
|
35
84
|
}
|
|
36
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Cleans a filename to conform to Bedrock's restrictions:
|
|
88
|
+
* - Alphanumeric characters, whitespace, hyphens, parentheses, square brackets.
|
|
89
|
+
* - No more than one consecutive whitespace character.
|
|
90
|
+
* - Decodes URI components (e.g., %20 -> space).
|
|
91
|
+
*/
|
|
92
|
+
function cleanBedrockFilename(name: string | undefined): string | undefined {
|
|
93
|
+
if (!name) return name;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// Decode URI components like %20
|
|
97
|
+
name = decodeURIComponent(name);
|
|
98
|
+
} catch {
|
|
99
|
+
// Ignore decoding errors
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return name
|
|
103
|
+
.replace(/[^\w\s\-()[\]]/g, " ") // Replace invalid characters with space
|
|
104
|
+
.replace(/\s+/g, " ") // Collapse consecutive whitespaces
|
|
105
|
+
.trim();
|
|
106
|
+
}
|
|
107
|
+
|
|
37
108
|
type FileProcessingMode = 'content' | 'tool';
|
|
38
109
|
|
|
39
110
|
async function processFile<T extends FileProcessingMode>(
|
|
@@ -78,7 +149,7 @@ async function processFile<T extends FileProcessingMode>(
|
|
|
78
149
|
const documentBlock = {
|
|
79
150
|
document: {
|
|
80
151
|
format: mimeToDocType(f.mime_type),
|
|
81
|
-
name: f.name,
|
|
152
|
+
name: cleanBedrockFilename(f.name),
|
|
82
153
|
source: { bytes: await readStreamAsUint8Array(source) },
|
|
83
154
|
},
|
|
84
155
|
};
|
|
@@ -235,6 +306,10 @@ export async function formatConversePrompt(segments: PromptSegment[], options: E
|
|
|
235
306
|
for (const file of segment.files ?? []) {
|
|
236
307
|
toolContentBlocks.push(await processFileToToolContentBlock(file));
|
|
237
308
|
}
|
|
309
|
+
// Bedrock requires at least one content block in toolResult
|
|
310
|
+
if (toolContentBlocks.length === 0) {
|
|
311
|
+
toolContentBlocks.push({ text: '[No output]' });
|
|
312
|
+
}
|
|
238
313
|
messages.push({
|
|
239
314
|
content: [{
|
|
240
315
|
toolResult: {
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { LlumiverseError } from '@llumiverse/core';
|
|
2
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { BedrockDriver } from './index.js';
|
|
4
|
+
|
|
5
|
+
describe('BedrockDriver Error Handling', () => {
|
|
6
|
+
let driver: BedrockDriver;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
driver = new BedrockDriver({ region: 'us-east-1' });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('formatLlumiverseError', () => {
|
|
13
|
+
it('should handle ValidationException with status code in message', () => {
|
|
14
|
+
const awsError = {
|
|
15
|
+
name: 'ValidationException',
|
|
16
|
+
message: "1 validation error detected: Value '32424.0' at 'inferenceConfig.temperature' failed to satisfy constraint: Member must have value less than or equal to 1",
|
|
17
|
+
$fault: 'client',
|
|
18
|
+
$metadata: {
|
|
19
|
+
httpStatusCode: 400,
|
|
20
|
+
requestId: 'e3ed39d8-bdf5-40e6-9d2c-9a1cc8323f61',
|
|
21
|
+
attempts: 1,
|
|
22
|
+
totalRetryDelay: 0,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const error = driver.formatLlumiverseError(awsError, {
|
|
27
|
+
provider: 'bedrock',
|
|
28
|
+
model: 'test-model',
|
|
29
|
+
operation: 'execute',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(error).toBeInstanceOf(LlumiverseError);
|
|
33
|
+
expect(error.code).toBe(400);
|
|
34
|
+
expect(error.retryable).toBe(false);
|
|
35
|
+
expect(error.message).toContain('[400]');
|
|
36
|
+
expect(error.message).toContain('validation error detected');
|
|
37
|
+
expect(error.message).toContain('Request ID: e3ed39d8-bdf5-40e6-9d2c-9a1cc8323f61');
|
|
38
|
+
expect(error.context.provider).toBe('bedrock');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should handle ThrottlingException as retryable', () => {
|
|
42
|
+
const awsError = {
|
|
43
|
+
name: 'ThrottlingException',
|
|
44
|
+
message: 'The number of requests exceeds the limit',
|
|
45
|
+
$fault: 'client',
|
|
46
|
+
$metadata: {
|
|
47
|
+
httpStatusCode: 429,
|
|
48
|
+
requestId: 'abc123',
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const error = driver.formatLlumiverseError(awsError, {
|
|
53
|
+
provider: 'bedrock',
|
|
54
|
+
model: 'test-model',
|
|
55
|
+
operation: 'execute',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(error.retryable).toBe(true);
|
|
59
|
+
expect(error.code).toBe(429);
|
|
60
|
+
expect(error.message).toContain('[429]');
|
|
61
|
+
expect(error.message).toContain('Request ID: abc123');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should handle InternalServerException as retryable', () => {
|
|
65
|
+
const awsError = {
|
|
66
|
+
name: 'InternalServerException',
|
|
67
|
+
message: 'An internal server error occurred',
|
|
68
|
+
$fault: 'server',
|
|
69
|
+
$metadata: {
|
|
70
|
+
httpStatusCode: 500,
|
|
71
|
+
requestId: 'server-error-123',
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const error = driver.formatLlumiverseError(awsError, {
|
|
76
|
+
provider: 'bedrock',
|
|
77
|
+
model: 'test-model',
|
|
78
|
+
operation: 'execute',
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(error.retryable).toBe(true);
|
|
82
|
+
expect(error.code).toBe(500);
|
|
83
|
+
expect(error.message).toContain('[500]');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should handle ServiceUnavailableException as retryable', () => {
|
|
87
|
+
const awsError = {
|
|
88
|
+
name: 'ServiceUnavailableException',
|
|
89
|
+
message: 'Service is temporarily unavailable',
|
|
90
|
+
$fault: 'server',
|
|
91
|
+
$metadata: {
|
|
92
|
+
httpStatusCode: 503,
|
|
93
|
+
requestId: 'unavail-123',
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const error = driver.formatLlumiverseError(awsError, {
|
|
98
|
+
provider: 'bedrock',
|
|
99
|
+
model: 'test-model',
|
|
100
|
+
operation: 'execute',
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(error.retryable).toBe(true);
|
|
104
|
+
expect(error.code).toBe(503);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should handle AccessDeniedException as not retryable', () => {
|
|
108
|
+
const awsError = {
|
|
109
|
+
name: 'AccessDeniedException',
|
|
110
|
+
message: 'Access denied',
|
|
111
|
+
$fault: 'client',
|
|
112
|
+
$metadata: {
|
|
113
|
+
httpStatusCode: 403,
|
|
114
|
+
requestId: 'access-denied-123',
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const error = driver.formatLlumiverseError(awsError, {
|
|
119
|
+
provider: 'bedrock',
|
|
120
|
+
model: 'test-model',
|
|
121
|
+
operation: 'execute',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(error.retryable).toBe(false);
|
|
125
|
+
expect(error.code).toBe(403);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should handle ResourceNotFoundException as not retryable', () => {
|
|
129
|
+
const awsError = {
|
|
130
|
+
name: 'ResourceNotFoundException',
|
|
131
|
+
message: 'Resource not found',
|
|
132
|
+
$fault: 'client',
|
|
133
|
+
$metadata: {
|
|
134
|
+
httpStatusCode: 404,
|
|
135
|
+
requestId: 'not-found-123',
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const error = driver.formatLlumiverseError(awsError, {
|
|
140
|
+
provider: 'bedrock',
|
|
141
|
+
model: 'test-model',
|
|
142
|
+
operation: 'execute',
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(error.retryable).toBe(false);
|
|
146
|
+
expect(error.code).toBe(404);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should handle ServiceQuotaExceededException as retryable', () => {
|
|
150
|
+
const awsError = {
|
|
151
|
+
name: 'ServiceQuotaExceededException',
|
|
152
|
+
message: 'Service quota exceeded',
|
|
153
|
+
$fault: 'client',
|
|
154
|
+
$metadata: {
|
|
155
|
+
httpStatusCode: 402,
|
|
156
|
+
requestId: 'quota-123',
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const error = driver.formatLlumiverseError(awsError, {
|
|
161
|
+
provider: 'bedrock',
|
|
162
|
+
model: 'test-model',
|
|
163
|
+
operation: 'execute',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(error.retryable).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should handle ConflictException as not retryable', () => {
|
|
170
|
+
const awsError = {
|
|
171
|
+
name: 'ConflictException',
|
|
172
|
+
message: 'Conflict',
|
|
173
|
+
$fault: 'client',
|
|
174
|
+
$metadata: {
|
|
175
|
+
httpStatusCode: 409,
|
|
176
|
+
requestId: 'conflict-123',
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const error = driver.formatLlumiverseError(awsError, {
|
|
181
|
+
provider: 'bedrock',
|
|
182
|
+
model: 'test-model',
|
|
183
|
+
operation: 'execute',
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(error.retryable).toBe(false);
|
|
187
|
+
expect(error.code).toBe(409);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should handle ResourceInUseException as not retryable', () => {
|
|
191
|
+
const awsError = {
|
|
192
|
+
name: 'ResourceInUseException',
|
|
193
|
+
message: 'Resource in use',
|
|
194
|
+
$fault: 'client',
|
|
195
|
+
$metadata: {
|
|
196
|
+
httpStatusCode: 400,
|
|
197
|
+
requestId: 'in-use-123',
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const error = driver.formatLlumiverseError(awsError, {
|
|
202
|
+
provider: 'bedrock',
|
|
203
|
+
model: 'test-model',
|
|
204
|
+
operation: 'execute',
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(error.retryable).toBe(false);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should handle error without status code using error name', () => {
|
|
211
|
+
const awsError = {
|
|
212
|
+
name: 'ThrottlingException',
|
|
213
|
+
message: 'Rate limited',
|
|
214
|
+
$fault: 'client',
|
|
215
|
+
$metadata: {
|
|
216
|
+
requestId: 'no-status-123',
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const error = driver.formatLlumiverseError(awsError, {
|
|
221
|
+
provider: 'bedrock',
|
|
222
|
+
model: 'test-model',
|
|
223
|
+
operation: 'execute',
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(error.code).toBeUndefined(); // No status code available
|
|
227
|
+
expect(error.name).toBe('ThrottlingException'); // Error name preserved
|
|
228
|
+
expect(error.retryable).toBe(true);
|
|
229
|
+
expect(error.message).not.toContain('[ThrottlingException]'); // status code format only for numbers
|
|
230
|
+
expect(error.message).toContain('Rate limited');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should fall back to fault type for unknown errors', () => {
|
|
234
|
+
const serverError = {
|
|
235
|
+
name: 'UnknownServerError',
|
|
236
|
+
message: 'Unknown error',
|
|
237
|
+
$fault: 'server',
|
|
238
|
+
$metadata: {
|
|
239
|
+
httpStatusCode: 599,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const error = driver.formatLlumiverseError(serverError, {
|
|
244
|
+
provider: 'bedrock',
|
|
245
|
+
model: 'test-model',
|
|
246
|
+
operation: 'execute',
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
expect(error.retryable).toBe(true); // 5xx is retryable
|
|
250
|
+
|
|
251
|
+
const clientError = {
|
|
252
|
+
name: 'UnknownClientError',
|
|
253
|
+
message: 'Unknown error',
|
|
254
|
+
$fault: 'client',
|
|
255
|
+
$metadata: {
|
|
256
|
+
httpStatusCode: 499,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const error2 = driver.formatLlumiverseError(clientError, {
|
|
261
|
+
provider: 'bedrock',
|
|
262
|
+
model: 'test-model',
|
|
263
|
+
operation: 'execute',
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
expect(error2.retryable).toBe(false); // 4xx is not retryable
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should handle non-AWS errors by delegating to parent', () => {
|
|
270
|
+
const regularError = new Error('Regular error');
|
|
271
|
+
|
|
272
|
+
const error = driver.formatLlumiverseError(regularError, {
|
|
273
|
+
provider: 'bedrock',
|
|
274
|
+
model: 'test-model',
|
|
275
|
+
operation: 'execute',
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
expect(error).toBeInstanceOf(LlumiverseError);
|
|
279
|
+
expect(error.code).toBeUndefined(); // No numeric status code available
|
|
280
|
+
expect(error.retryable).toBeUndefined(); // Unknown errors - let consumer decide
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('should preserve original error for debugging', () => {
|
|
284
|
+
const awsError = {
|
|
285
|
+
name: 'ValidationException',
|
|
286
|
+
message: 'Validation failed',
|
|
287
|
+
$fault: 'client',
|
|
288
|
+
$metadata: {
|
|
289
|
+
httpStatusCode: 400,
|
|
290
|
+
requestId: 'test-123',
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const error = driver.formatLlumiverseError(awsError, {
|
|
295
|
+
provider: 'bedrock',
|
|
296
|
+
model: 'test-model',
|
|
297
|
+
operation: 'execute',
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
expect(error.originalError).toBe(awsError);
|
|
301
|
+
expect((error.originalError as any).$metadata.requestId).toBe('test-123');
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe('isBedrockErrorRetryable', () => {
|
|
306
|
+
it('should classify retryable errors correctly', () => {
|
|
307
|
+
const retryableErrors = [
|
|
308
|
+
'ThrottlingException',
|
|
309
|
+
'ServiceUnavailableException',
|
|
310
|
+
'InternalServerException',
|
|
311
|
+
'ServiceQuotaExceededException',
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
retryableErrors.forEach((errorName) => {
|
|
315
|
+
const result = (driver as any).isBedrockErrorRetryable(errorName, undefined, undefined);
|
|
316
|
+
expect(result, `${errorName} should be retryable`).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should classify non-retryable errors correctly', () => {
|
|
321
|
+
const nonRetryableErrors = [
|
|
322
|
+
'ValidationException',
|
|
323
|
+
'AccessDeniedException',
|
|
324
|
+
'ResourceNotFoundException',
|
|
325
|
+
'ConflictException',
|
|
326
|
+
'ResourceInUseException',
|
|
327
|
+
'TooManyTagsException',
|
|
328
|
+
];
|
|
329
|
+
|
|
330
|
+
nonRetryableErrors.forEach((errorName) => {
|
|
331
|
+
const result = (driver as any).isBedrockErrorRetryable(errorName, undefined, undefined);
|
|
332
|
+
expect(result, `${errorName} should not be retryable`).toBe(false);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('should use HTTP status codes when available', () => {
|
|
337
|
+
expect((driver as any).isBedrockErrorRetryable('UnknownError', 429, undefined)).toBe(true);
|
|
338
|
+
expect((driver as any).isBedrockErrorRetryable('UnknownError', 408, undefined)).toBe(true);
|
|
339
|
+
expect((driver as any).isBedrockErrorRetryable('UnknownError', 529, undefined)).toBe(true);
|
|
340
|
+
expect((driver as any).isBedrockErrorRetryable('UnknownError', 500, undefined)).toBe(true);
|
|
341
|
+
expect((driver as any).isBedrockErrorRetryable('UnknownError', 503, undefined)).toBe(true);
|
|
342
|
+
expect((driver as any).isBedrockErrorRetryable('UnknownError', 400, undefined)).toBe(false);
|
|
343
|
+
expect((driver as any).isBedrockErrorRetryable('UnknownError', 403, undefined)).toBe(false);
|
|
344
|
+
expect((driver as any).isBedrockErrorRetryable('UnknownError', 404, undefined)).toBe(false);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should use fault type as fallback', () => {
|
|
348
|
+
expect((driver as any).isBedrockErrorRetryable('UnknownError', undefined, 'server')).toBe(true);
|
|
349
|
+
expect((driver as any).isBedrockErrorRetryable('UnknownError', undefined, 'client')).toBe(false);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
});
|