@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.
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,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":"AAAA,OAAO,EACH,OAAO,EACP,oCAAoC,EAEvC,MAAM,eAAe,CAAC;AACvB,OAAO,EACH,OAAO,EAAE,UAAU,EAAE,qBAAqB,EAAoB,gBAAgB,EAC9E,mBAAmB,EAKnB,aAAa,EAMhB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAwd/C,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,SAAS,GAAG,OAAO,EAAE,CA2B/E;AAyDD,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;IA6F5H,2BAA2B,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,qBAAqB,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,aAAa,CAAC,qBAAqB,CAAC,CAAC;CAyErK"}
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, PromptSegment, ExecutionOptions, CompletionChunkObject } from "@llumiverse/core";
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,aAAa,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAC/G,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;CAC9H;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,eAAe,CA2BjE"}
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-dev.20260224.234313Z",
3
+ "version": "1.0.0-dev.20260331.080752Z",
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.74.0",
52
- "@anthropic-ai/vertex-sdk": "^0.14.3",
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.4.0",
66
- "@google/genai": "^1.40.0",
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.79.3",
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.18.0",
74
+ "openai": "^6.22.0",
75
75
  "replicate": "^1.4.0",
76
- "@llumiverse/common": "1.0.0-dev.20260224.234313Z",
77
- "@llumiverse/core": "1.0.0-dev.20260224.234313Z"
76
+ "@llumiverse/core": "1.0.0-dev.20260331.080752Z",
77
+ "@llumiverse/common": "1.0.0-dev.20260331.080752Z"
78
78
  },
79
79
  "ts_dual_module": {
80
80
  "outDir": "lib"
@@ -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
- function mimeToImageType(mime: string): "png" | "jpeg" | "gif" | "webp" {
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
- return mime.split("/")[1] as "png" | "jpeg" | "gif" | "webp";
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
- function mimeToDocType(mime: string): "pdf" | "csv" | "doc" | "docx" | "xls" | "xlsx" | "html" | "txt" | "md" {
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
- return mime.split("/")[1] as "pdf" | "csv" | "doc" | "docx" | "xls" | "xlsx" | "html" | "txt" | "md";
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
- function mimeToVideoType(mime: string): "mov" | "mkv" | "mp4" | "webm" | "flv" | "mpeg" | "mpg" | "wmv" | "three_gp" {
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
- return mime.split("/")[1] as "mov" | "mkv" | "mp4" | "webm" | "flv" | "mpeg" | "mpg" | "wmv" | "three_gp";
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
+ });