feat(ai): add requestMetadata support to BedrockOptions for cost allocation tagging (#2511)
Add an optional requestMetadata field to BedrockOptions that forwards key-value pairs to the Bedrock Converse API ConverseStreamCommand. Tags appear in AWS Cost Explorer split cost allocation data, enabling callers to attribute inference costs to specific applications or contexts. Changes: - Add requestMetadata?: Record<string, string> to BedrockOptions with JSDoc documenting AWS constraints (max 50 pairs, key 64 chars, value 256 chars, no aws: prefix) - Pass requestMetadata to commandInput via conditional spread to avoid sending undefined when omitted - Export BedrockOptions from package root (consistent with other provider option types) - Add E2E tests: metadata forwarded to SDK payload, and omitted when not provided closes #2510
This commit is contained in:
@@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
||||||
|
|
||||||
- Fixed OpenAI Responses replay for foreign tool-call item IDs by hashing foreign `function_call.id` values into bounded `fc_<hash>` IDs instead of preserving backend-specific normalized shapes that OpenAI Codex rejects.
|
- Fixed OpenAI Responses replay for foreign tool-call item IDs by hashing foreign `function_call.id` values into bounded `fc_<hash>` IDs instead of preserving backend-specific normalized shapes that OpenAI Codex rejects.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export { Type } from "@sinclair/typebox";
|
|||||||
export * from "./api-registry.js";
|
export * from "./api-registry.js";
|
||||||
export * from "./env-api-keys.js";
|
export * from "./env-api-keys.js";
|
||||||
export * from "./models.js";
|
export * from "./models.js";
|
||||||
|
export type { BedrockOptions } from "./providers/amazon-bedrock.js";
|
||||||
export type { AnthropicOptions } from "./providers/anthropic.js";
|
export type { AnthropicOptions } from "./providers/anthropic.js";
|
||||||
export type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses.js";
|
export type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses.js";
|
||||||
export type { GoogleOptions } from "./providers/google.js";
|
export type { GoogleOptions } from "./providers/google.js";
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ export interface BedrockOptions extends StreamOptions {
|
|||||||
thinkingBudgets?: ThinkingBudgets;
|
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 */
|
/* 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;
|
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<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Block = (TextContent | ThinkingContent | ToolCall) & { index?: number; partialJson?: string };
|
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 },
|
inferenceConfig: { maxTokens: options.maxTokens, temperature: options.temperature },
|
||||||
toolConfig: convertToolConfig(context.tools, options.toolChoice),
|
toolConfig: convertToolConfig(context.tools, options.toolChoice),
|
||||||
additionalModelRequestFields: buildAdditionalModelRequestFields(model, options),
|
additionalModelRequestFields: buildAdditionalModelRequestFields(model, options),
|
||||||
|
...(options.requestMetadata !== undefined && { requestMetadata: options.requestMetadata }),
|
||||||
};
|
};
|
||||||
const nextCommandInput = await options?.onPayload?.(commandInput, model);
|
const nextCommandInput = await options?.onPayload?.(commandInput, model);
|
||||||
if (nextCommandInput !== undefined) {
|
if (nextCommandInput !== undefined) {
|
||||||
|
|||||||
@@ -1246,6 +1246,60 @@ describe("Generate E2E Tests", () => {
|
|||||||
expect(payload.additionalModelRequestFields?.output_config).toEqual({ effort: "max" });
|
expect(payload.additionalModelRequestFields?.output_config).toEqual({ effort: "max" });
|
||||||
expect(payload.additionalModelRequestFields?.anthropic_beta).toBeUndefined();
|
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
|
// Check if ollama is installed and local LLM tests are enabled
|
||||||
|
|||||||
Reference in New Issue
Block a user