diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 132ef082..5ee22d9c 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,11 @@ ## [Unreleased] +### Added + +- Added `requestMetadata` option to `BedrockOptions` for AWS cost allocation tagging; key-value pairs are forwarded to the Bedrock Converse API `requestMetadata` field and appear in AWS Cost Explorer split cost allocation data. +- Exported `BedrockOptions` type from the package root entry point, consistent with other provider option types. + ### Fixed - Fixed OpenAI Responses replay for foreign tool-call item IDs by hashing foreign `function_call.id` values into bounded `fc_` IDs instead of preserving backend-specific normalized shapes that OpenAI Codex rejects. diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 73a2abc2..6edf18be 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -4,6 +4,7 @@ export { Type } from "@sinclair/typebox"; export * from "./api-registry.js"; export * from "./env-api-keys.js"; export * from "./models.js"; +export type { BedrockOptions } from "./providers/amazon-bedrock.js"; export type { AnthropicOptions } from "./providers/anthropic.js"; export type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses.js"; export type { GoogleOptions } from "./providers/google.js"; diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts index 20314f89..94cbf589 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -55,6 +55,11 @@ export interface BedrockOptions extends StreamOptions { thinkingBudgets?: ThinkingBudgets; /* Only supported by Claude 4.x models, see https://docs.aws.amazon.com/bedrock/latest/userguide/claude-messages-extended-thinking.html#claude-messages-extended-thinking-tool-use-interleaved */ interleavedThinking?: boolean; + /** Key-value pairs attached to the inference request for cost allocation tagging. + * Keys: max 64 chars, no `aws:` prefix. Values: max 256 chars. Max 50 pairs. + * Tags appear in AWS Cost Explorer split cost allocation data. + * @see https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ConverseStream.html */ + requestMetadata?: Record; } type Block = (TextContent | ThinkingContent | ToolCall) & { index?: number; partialJson?: string }; @@ -153,6 +158,7 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt inferenceConfig: { maxTokens: options.maxTokens, temperature: options.temperature }, toolConfig: convertToolConfig(context.tools, options.toolChoice), additionalModelRequestFields: buildAdditionalModelRequestFields(model, options), + ...(options.requestMetadata !== undefined && { requestMetadata: options.requestMetadata }), }; const nextCommandInput = await options?.onPayload?.(commandInput, model); if (nextCommandInput !== undefined) { diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index f4abb90a..07bccc88 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -1246,6 +1246,60 @@ describe("Generate E2E Tests", () => { expect(payload.additionalModelRequestFields?.output_config).toEqual({ effort: "max" }); expect(payload.additionalModelRequestFields?.anthropic_beta).toBeUndefined(); }); + + it("should pass requestMetadata to the SDK payload", { retry: 3 }, async () => { + const llmSonnet = getModel("amazon-bedrock", "global.anthropic.claude-sonnet-4-5-20250929-v1:0"); + let capturedPayload: unknown; + const metadata = { app: "pi-test", env: "ci" }; + const response = await complete( + llmSonnet, + { + messages: [ + { + role: "user", + content: "Say hi.", + timestamp: Date.now(), + }, + ], + }, + { + requestMetadata: metadata, + onPayload: (payload) => { + capturedPayload = payload; + }, + }, + ); + + expect(response.stopReason, `Error: ${response.errorMessage}`).not.toBe("error"); + expect(capturedPayload).toBeTruthy(); + expect((capturedPayload as { requestMetadata?: unknown }).requestMetadata).toEqual(metadata); + }); + + it("should omit requestMetadata from payload when not provided", { retry: 3 }, async () => { + const llmSonnet = getModel("amazon-bedrock", "global.anthropic.claude-sonnet-4-5-20250929-v1:0"); + let capturedPayload: unknown; + const response = await complete( + llmSonnet, + { + messages: [ + { + role: "user", + content: "Say hi.", + timestamp: Date.now(), + }, + ], + }, + { + onPayload: (payload) => { + capturedPayload = payload; + }, + }, + ); + + expect(response.stopReason, `Error: ${response.errorMessage}`).not.toBe("error"); + expect(capturedPayload).toBeTruthy(); + expect("requestMetadata" in (capturedPayload as object)).toBe(false); + }); }); // Check if ollama is installed and local LLM tests are enabled