From 0415adb27470b85e702ae33ae81fe1d57c3f8210 Mon Sep 17 00:00:00 2001 From: Andrew Klatzke Date: Fri, 18 Sep 2026 11:22:35 -0800 Subject: [PATCH 1/4] fix(AIC-3382): support Bedrock configs in LangChain handlers Apply inference-profile prefixes before model construction and report the configured provider so Bedrock calls and telemetry stay aligned. Co-authored-by: Cursor --- examples/streaming.ts | 5 ++ .../src/__tests__/handler.test.ts | 88 +++++++++++++++++++ .../src/__tests__/spans.test.ts | 4 +- packages/langchain-agents/src/handler.ts | 37 +++++--- .../src/__tests__/handler.test.ts | 83 +++++++++++++++++ .../src/__tests__/spans.test.ts | 6 +- packages/langchain-messages/src/handler.ts | 36 +++++--- 7 files changed, 233 insertions(+), 26 deletions(-) diff --git a/examples/streaming.ts b/examples/streaming.ts index 8b24ca28..5770959b 100644 --- a/examples/streaming.ts +++ b/examples/streaming.ts @@ -23,11 +23,16 @@ export async function run(key: string, userInput: string): Promise { }).stream(userInput, newContext()), ); + let wroteChunk = false; for await (const event of stream) { if (event.type === 'chunk') { process.stdout.write(event.text); + wroteChunk = true; } else { // Final event — full response + normalized usage + if (!wroteChunk) { + process.stdout.write(event.response); + } process.stdout.write('\n\n'); process.stderr.write(`[debug] done event response length: ${event.response?.length ?? 0}\n`); process.stderr.write(`[debug] done event response preview: ${String(event.response).slice(0, 100)}\n`); diff --git a/packages/langchain-agents/src/__tests__/handler.test.ts b/packages/langchain-agents/src/__tests__/handler.test.ts index 39b4911b..271bd72f 100644 --- a/packages/langchain-agents/src/__tests__/handler.test.ts +++ b/packages/langchain-agents/src/__tests__/handler.test.ts @@ -313,6 +313,20 @@ describe('createLangChainAgentsHandler', () => { expect(modelSpan?.setAttribute).toHaveBeenCalledWith('gen_ai.usage.total_tokens', 12); }); + it.each([ + ['OpenAI', 'openai'], + ['Bedrock', 'bedrock'], + ['Azure', 'azure'], + ['Anthropic', 'anthropic'], + ['', 'openai'], + ] as const)('sets gen_ai.provider.name from config %s', async (providerName, expected) => { + mockCreateAgent.mockReturnValue(makeCallbackAgent({ output: 'ok', inputTokens: 1, outputTokens: 1 })); + const cfg = { ...baseConfig, provider: { name: providerName } }; + await createLangChainAgentsHandler({} as any)(cfg as any, 'q'); + expect(mockSpan.setAttribute).toHaveBeenCalledWith('gen_ai.provider.name', expected); + expect(mockSpan.setAttribute).toHaveBeenCalledWith('gen_ai.system', 'langchain'); + }); + // ── LaunchDarkly correlation ──────────────────────────────────────────────── // // The `feature_flag` span event is how the AI Config Monitoring tab locates a trace @@ -1008,6 +1022,80 @@ describe('model source', () => { expect(mockCreateAgent).toHaveBeenCalledWith(expect.objectContaining({ model: constructed })); }); + it('prepends model.region onto the Bedrock model id once', async () => { + const constructed = { tag: 'bedrock' }; + MockChatOpenAI.mockImplementation(function MockChatOpenAI() { + return constructed; + }); + const cfg = { + ...parameterized, + provider: { name: 'Bedrock' }, + model: { name: 'anthropic.claude-sonnet-4-5', region: 'us', parameters: { temperature: 0.2 } }, + }; + await createLangChainAgentsHandler()(cfg as any, 'q'); + expect(MockChatOpenAI).toHaveBeenCalledWith({ + temperature: 0.2, + model: 'us.anthropic.claude-sonnet-4-5', + }); + expect(cfg.model.name).toBe('anthropic.claude-sonnet-4-5'); + }); + + it('does not double a Bedrock inference-profile prefix', async () => { + const constructed = { tag: 'bedrock' }; + MockChatOpenAI.mockImplementation(function MockChatOpenAI() { + return constructed; + }); + const cfg = { + ...parameterized, + provider: { name: 'Bedrock' }, + model: { name: 'us.anthropic.claude-sonnet-4-5', region: 'us' }, + }; + await createLangChainAgentsHandler()(cfg as any, 'q'); + expect(MockChatOpenAI).toHaveBeenCalledWith({ model: 'us.anthropic.claude-sonnet-4-5' }); + }); + + it('leaves a Bedrock model name unchanged when region is absent', async () => { + const constructed = { tag: 'bedrock' }; + MockChatOpenAI.mockImplementation(function MockChatOpenAI() { + return constructed; + }); + const cfg = { + ...parameterized, + provider: { name: 'Bedrock' }, + model: { name: 'anthropic.claude-sonnet-4-5' }, + }; + await createLangChainAgentsHandler()(cfg as any, 'q'); + expect(MockChatOpenAI).toHaveBeenCalledWith({ model: 'anthropic.claude-sonnet-4-5' }); + }); + + it('ignores model.region for a non-Bedrock provider', async () => { + const constructed = { tag: 'openai' }; + MockChatOpenAI.mockImplementation(function MockChatOpenAI() { + return constructed; + }); + const cfg = { + ...parameterized, + provider: { name: 'OpenAI' }, + model: { name: 'gpt-4o', region: 'us' }, + }; + await createLangChainAgentsHandler()(cfg as any, 'q'); + expect(MockChatOpenAI).toHaveBeenCalledWith({ model: 'gpt-4o' }); + }); + + it('passes a prefixed Bedrock name to a factory without mutating the original config', async () => { + const llm = { invoke: vi.fn() }; + const factory = vi.fn().mockReturnValue(llm); + const cfg = { + ...parameterized, + provider: { name: 'Bedrock' }, + model: { name: 'anthropic.claude-sonnet-4-5', region: 'us', parameters: { temperature: 0.2 } }, + }; + await createLangChainAgentsHandler(factory)(cfg as any, 'q'); + expect(factory.mock.calls[0][0].model.name).toBe('us.anthropic.claude-sonnet-4-5'); + expect(cfg.model.name).toBe('anthropic.claude-sonnet-4-5'); + expect(factory.mock.calls[0][0]).not.toBe(cfg); + }); + it('resolves a factory on the streaming path', async () => { const llm = { invoke: vi.fn() }; const factory = vi.fn().mockReturnValue(llm); diff --git a/packages/langchain-agents/src/__tests__/spans.test.ts b/packages/langchain-agents/src/__tests__/spans.test.ts index 8e77bde8..329f4140 100644 --- a/packages/langchain-agents/src/__tests__/spans.test.ts +++ b/packages/langchain-agents/src/__tests__/spans.test.ts @@ -153,8 +153,8 @@ describe('langchain-agents span tree against a real tracer', () => { expect(attrs['gen_ai.usage.input_tokens']).toBe(10); expect(attrs['gen_ai.usage.output_tokens']).toBe(5); expect(attrs['gen_ai.usage.total_tokens']).toBe(15); - // `gen_ai.provider.name` names who served the model; `langchain` is the framework and is not a - // member of the semconv enum. The legacy `gen_ai.system` keeps the value the handler shipped. + // `gen_ai.provider.name` is the configured provider, lower-cased. The legacy `gen_ai.system` + // keeps the shipped framework value. expect(attrs['gen_ai.provider.name']).toBe('openai'); expect(attrs['gen_ai.system']).toBe('langchain'); }); diff --git a/packages/langchain-agents/src/handler.ts b/packages/langchain-agents/src/handler.ts index 254a4b25..b4969d12 100644 --- a/packages/langchain-agents/src/handler.ts +++ b/packages/langchain-agents/src/handler.ts @@ -37,16 +37,30 @@ import { toLangChainMessages } from './messages.js'; const TRACER_NAME = '@launchdarkly/ai-langchain-agents'; /** - * The provider that actually serves the model. + * The configured provider, lower-cased, for `gen_ai.provider.name`. * * `gen_ai.provider.name` names who served the request, and its semconv enum has no `langchain` - * member — LangChain is the framework, not the provider. This mirrors the choice - * `resolveBaseModel` makes, so the attribute agrees with the client that is really used. - * `gen_ai.system` keeps the `langchain` value the handler shipped, so existing dashboards do not - * break. + * member — LangChain is the framework, not the provider. Empty or missing names fall back to + * `openai`. `gen_ai.system` keeps the `langchain` value the handler shipped, so existing + * dashboards do not break. */ function servingProvider(config: AiConfigRep): string { - return (config.provider?.name ?? '').toLowerCase() === 'anthropic' ? 'anthropic' : 'openai'; + return (config.provider?.name || 'openai').toLowerCase(); +} + +function resolvedModelName(config: AiConfigRep, fallbackName = ''): string { + const name = config.model?.name || fallbackName; + const provider = (config.provider?.name ?? '').toLowerCase(); + if (provider !== 'bedrock') return name; + const prefix = config.model?.region ?? ''; + if (!prefix || name.startsWith(`${prefix}.`)) return name; + return `${prefix}.${name}`; +} + +function configForModelCall(config: AiConfigRep): AiConfigRep { + const resolved = resolvedModelName(config); + if (config.model?.name === resolved) return config; + return { ...config, model: { ...config.model, name: resolved } }; } /** @@ -248,20 +262,21 @@ export type LangChainModelSource = BaseChatModel | ((config: AiConfigRep) => Bas function modelConstructorArgs(config: AiConfigRep, fallbackName: string): Record { const parameters = config.model?.parameters && typeof config.model.parameters === 'object' ? config.model.parameters : {}; - return { ...parameters, model: config.model?.name || fallbackName }; + return { ...parameters, model: resolvedModelName(config, fallbackName) }; } async function resolveBaseModel(aiConfig: AiConfigRep, llm?: LangChainModelSource): Promise { - if (typeof llm === 'function') return llm(aiConfig); + const invocation = configForModelCall(aiConfig); + if (typeof llm === 'function') return llm(invocation); if (llm) return llm; - const provider = (aiConfig.provider?.name ?? '').toLowerCase(); + const provider = (invocation.provider?.name ?? '').toLowerCase(); if (provider === 'anthropic') { const { ChatAnthropic } = await import('@langchain/anthropic'); // biome-ignore lint/suspicious/noExplicitAny: parameter bag is caller-owned and not remapped - return new ChatAnthropic(modelConstructorArgs(aiConfig, 'claude-3-5-sonnet-20241022') as any); + return new ChatAnthropic(modelConstructorArgs(invocation, 'claude-3-5-sonnet-20241022') as any); } // biome-ignore lint/suspicious/noExplicitAny: parameter bag is caller-owned and not remapped - return new ChatOpenAI(modelConstructorArgs(aiConfig, 'gpt-4o') as any); + return new ChatOpenAI(modelConstructorArgs(invocation, 'gpt-4o') as any); } const buildAgentTools = (configTools: Record, toolHandlers: Record) => diff --git a/packages/langchain-messages/src/__tests__/handler.test.ts b/packages/langchain-messages/src/__tests__/handler.test.ts index bafcf9ef..0c4b94c3 100644 --- a/packages/langchain-messages/src/__tests__/handler.test.ts +++ b/packages/langchain-messages/src/__tests__/handler.test.ts @@ -457,6 +457,19 @@ describe('createLangChainHandler', () => { expect(modelSpan?.setAttribute).toHaveBeenCalledWith('gen_ai.usage.total_tokens', 12); }); + it.each([ + ['OpenAI', 'openai'], + ['Bedrock', 'bedrock'], + ['Azure', 'azure'], + ['Anthropic', 'anthropic'], + ['', 'openai'], + ] as const)('sets gen_ai.provider.name from config %s', async (providerName, expected) => { + const cfg = { ...baseConfig, provider: { name: providerName } }; + await createLangChainHandler(makeMockLLM() as any)(cfg as any, 'q'); + expect(mockSpan.setAttribute).toHaveBeenCalledWith('gen_ai.provider.name', expected); + expect(mockSpan.setAttribute).toHaveBeenCalledWith('gen_ai.system', 'langchain'); + }); + it('emits no content at all by default', async () => { const llm = makeMockLLM('answer'); await createLangChainHandler(llm as any)(baseConfig as any, 'question'); @@ -1285,6 +1298,76 @@ describe('model source', () => { }); }); + it('prepends model.region onto the Bedrock model id once', async () => { + vi.mocked(ChatOpenAI).mockImplementation(function MockChatOpenAI() { + return makeMockLLM('bedrock') as any; + }); + const cfg = { + ...parameterized, + provider: { name: 'Bedrock' }, + model: { name: 'anthropic.claude-sonnet-4-5', region: 'us', parameters: { temperature: 0.2 } }, + }; + await createLangChainHandler()(cfg as any, 'q'); + expect(ChatOpenAI).toHaveBeenCalledWith({ + temperature: 0.2, + model: 'us.anthropic.claude-sonnet-4-5', + }); + expect(cfg.model.name).toBe('anthropic.claude-sonnet-4-5'); + }); + + it('does not double a Bedrock inference-profile prefix', async () => { + vi.mocked(ChatOpenAI).mockImplementation(function MockChatOpenAI() { + return makeMockLLM('bedrock') as any; + }); + const cfg = { + ...parameterized, + provider: { name: 'Bedrock' }, + model: { name: 'us.anthropic.claude-sonnet-4-5', region: 'us' }, + }; + await createLangChainHandler()(cfg as any, 'q'); + expect(ChatOpenAI).toHaveBeenCalledWith({ model: 'us.anthropic.claude-sonnet-4-5' }); + }); + + it('leaves a Bedrock model name unchanged when region is absent', async () => { + vi.mocked(ChatOpenAI).mockImplementation(function MockChatOpenAI() { + return makeMockLLM('bedrock') as any; + }); + const cfg = { + ...parameterized, + provider: { name: 'Bedrock' }, + model: { name: 'anthropic.claude-sonnet-4-5' }, + }; + await createLangChainHandler()(cfg as any, 'q'); + expect(ChatOpenAI).toHaveBeenCalledWith({ model: 'anthropic.claude-sonnet-4-5' }); + }); + + it('ignores model.region for a non-Bedrock provider', async () => { + vi.mocked(ChatOpenAI).mockImplementation(function MockChatOpenAI() { + return makeMockLLM('openai') as any; + }); + const cfg = { + ...parameterized, + provider: { name: 'OpenAI' }, + model: { name: 'gpt-4o', region: 'us' }, + }; + await createLangChainHandler()(cfg as any, 'q'); + expect(ChatOpenAI).toHaveBeenCalledWith({ model: 'gpt-4o' }); + }); + + it('passes a prefixed Bedrock name to a factory without mutating the original config', async () => { + const llm = makeMockLLM('from-factory'); + const factory = vi.fn().mockReturnValue(llm); + const cfg = { + ...parameterized, + provider: { name: 'Bedrock' }, + model: { name: 'anthropic.claude-sonnet-4-5', region: 'us', parameters: { temperature: 0.2 } }, + }; + await createLangChainHandler(factory)(cfg as any, 'q'); + expect(factory.mock.calls[0][0].model.name).toBe('us.anthropic.claude-sonnet-4-5'); + expect(cfg.model.name).toBe('anthropic.claude-sonnet-4-5'); + expect(factory.mock.calls[0][0]).not.toBe(cfg); + }); + it('resolves a factory on the streaming path', async () => { const llm = makeMockLLM('streamed'); (llm as any).stream = vi.fn().mockImplementation(async function* () { diff --git a/packages/langchain-messages/src/__tests__/spans.test.ts b/packages/langchain-messages/src/__tests__/spans.test.ts index 421ad03a..4324039d 100644 --- a/packages/langchain-messages/src/__tests__/spans.test.ts +++ b/packages/langchain-messages/src/__tests__/spans.test.ts @@ -145,9 +145,9 @@ describe('langchain-messages span tree against a real tracer', () => { expect(attrs['gen_ai.usage.input_tokens']).toBe(20); // two turns of 10 expect(attrs['gen_ai.usage.output_tokens']).toBe(10); expect(attrs['gen_ai.usage.total_tokens']).toBe(30); - // `gen_ai.provider.name` names who served the model, and its semconv enum has no `langchain` - // member — LangChain is the framework. The legacy `gen_ai.system` keeps the shipped value. - expect(attrs['gen_ai.provider.name']).toBe('openai'); + // `gen_ai.provider.name` is the configured provider, lower-cased. The legacy `gen_ai.system` + // keeps the shipped framework value. + expect(attrs['gen_ai.provider.name']).toBe('langchain'); expect(attrs['gen_ai.system']).toBe('langchain'); }); diff --git a/packages/langchain-messages/src/handler.ts b/packages/langchain-messages/src/handler.ts index 9c0f885e..47a24dfd 100644 --- a/packages/langchain-messages/src/handler.ts +++ b/packages/langchain-messages/src/handler.ts @@ -54,15 +54,30 @@ function numberOrZero(value: unknown): number { } /** - * The provider that actually serves the model. + * The configured provider, lower-cased, for `gen_ai.provider.name`. * * `gen_ai.provider.name` names who served the request, and its semconv enum has no `langchain` - * member — LangChain is the framework, not the provider. This mirrors the choice `resolveBaseModel` - * makes, so the attribute agrees with the client that is really used. `gen_ai.system` keeps the - * `langchain` value the handler shipped, so existing dashboards do not break. + * member — LangChain is the framework, not the provider. Empty or missing names fall back to + * `openai`. `gen_ai.system` keeps the `langchain` value the handler shipped, so existing + * dashboards do not break. */ function servingProvider(config: AiConfigRep): string { - return (config.provider?.name ?? '').toLowerCase() === 'anthropic' ? 'anthropic' : 'openai'; + return (config.provider?.name || 'openai').toLowerCase(); +} + +function resolvedModelName(config: AiConfigRep, fallbackName = ''): string { + const name = config.model?.name || fallbackName; + const provider = (config.provider?.name ?? '').toLowerCase(); + if (provider !== 'bedrock') return name; + const prefix = config.model?.region ?? ''; + if (!prefix || name.startsWith(`${prefix}.`)) return name; + return `${prefix}.${name}`; +} + +function configForModelCall(config: AiConfigRep): AiConfigRep { + const resolved = resolvedModelName(config); + if (config.model?.name === resolved) return config; + return { ...config, model: { ...config.model, name: resolved } }; } /** @@ -158,7 +173,7 @@ function modelConstructorArgs(config: AiConfigRep, fallbackName: string): Record const parameters = config.model?.parameters && typeof config.model.parameters === 'object' ? config.model.parameters : {}; // Name from the config always wins over a colliding `model` key in the parameter bag. - return { ...parameters, model: config.model?.name || fallbackName }; + return { ...parameters, model: resolvedModelName(config, fallbackName) }; } /** @@ -170,9 +185,10 @@ function modelConstructorArgs(config: AiConfigRep, fallbackName: string): Record * passed through unchanged. */ async function resolveBaseModel(config: AiConfigRep, llm?: LangChainModelSource): Promise { - if (typeof llm === 'function') return llm(config); + const invocation = configForModelCall(config); + if (typeof llm === 'function') return llm(invocation); if (llm) return llm; - const providerName = (config.provider?.name ?? '').toLowerCase(); + const providerName = (invocation.provider?.name ?? '').toLowerCase(); if (providerName === 'anthropic') { // biome-ignore lint/suspicious/noExplicitAny: @langchain/anthropic loaded via dynamic import with no static types let mod: any; @@ -183,7 +199,7 @@ async function resolveBaseModel(config: AiConfigRep, llm?: LangChainModelSource) 'Using Anthropic models requires @langchain/anthropic. Install it with: npm install @langchain/anthropic', ); } - return new mod.ChatAnthropic(modelConstructorArgs(config, 'claude-3-5-sonnet-20241022')); + return new mod.ChatAnthropic(modelConstructorArgs(invocation, 'claude-3-5-sonnet-20241022')); } // biome-ignore lint/suspicious/noExplicitAny: @langchain/openai loaded via dynamic import with no static types let mod: any; @@ -192,7 +208,7 @@ async function resolveBaseModel(config: AiConfigRep, llm?: LangChainModelSource) } catch { throw new Error('Using OpenAI models requires @langchain/openai. Install it with: npm install @langchain/openai'); } - return new mod.ChatOpenAI(modelConstructorArgs(config, 'gpt-4o')); + return new mod.ChatOpenAI(modelConstructorArgs(invocation, 'gpt-4o')); } const buildTools = ( From c3d8bd59bedcbf02cd29ad20e0d988e8bda53ab3 Mon Sep 17 00:00:00 2001 From: Andrew Klatzke Date: Fri, 18 Sep 2026 11:36:24 -0800 Subject: [PATCH 2/4] fix(AIC-3382): construct Bedrock LangChain models by default Route Bedrock configs through ChatBedrockConverse so callers no longer need to provide a custom model factory. Co-authored-by: Cursor --- packages/langchain-agents/package.json | 9 + .../src/__tests__/handler.test.ts | 19 +- packages/langchain-agents/src/handler.ts | 11 + packages/langchain-messages/package.json | 5 + .../src/__tests__/handler.test.ts | 18 +- packages/langchain-messages/src/handler.ts | 14 +- yarn.lock | 338 +++++++++++++++++- 7 files changed, 399 insertions(+), 15 deletions(-) diff --git a/packages/langchain-agents/package.json b/packages/langchain-agents/package.json index 5d76d9a4..dee84a89 100644 --- a/packages/langchain-agents/package.json +++ b/packages/langchain-agents/package.json @@ -51,7 +51,16 @@ "@opentelemetry/api": "^1.9.1", "langchain": "^1.0.0" }, + "peerDependencies": { + "@langchain/aws": ">=1.0" + }, + "peerDependenciesMeta": { + "@langchain/aws": { + "optional": true + } + }, "devDependencies": { + "@langchain/aws": "1.3.9", "@opentelemetry/context-async-hooks": "^2.8.0", "@opentelemetry/sdk-trace-base": "^2.8.0", "@types/node": "^22.0.0", diff --git a/packages/langchain-agents/src/__tests__/handler.test.ts b/packages/langchain-agents/src/__tests__/handler.test.ts index 271bd72f..c63656f3 100644 --- a/packages/langchain-agents/src/__tests__/handler.test.ts +++ b/packages/langchain-agents/src/__tests__/handler.test.ts @@ -33,6 +33,7 @@ vi.mock('@langchain/core/tools', () => ({ const MockChatOpenAI = vi.hoisted(() => vi.fn().mockImplementation(() => ({}))); const MockChatAnthropic = vi.hoisted(() => vi.fn().mockImplementation(() => ({}))); +const MockChatBedrockConverse = vi.hoisted(() => vi.fn().mockImplementation(() => ({}))); vi.mock('@langchain/openai', () => ({ ChatOpenAI: MockChatOpenAI, @@ -42,6 +43,10 @@ vi.mock('@langchain/anthropic', () => ({ ChatAnthropic: MockChatAnthropic, })); +vi.mock('@langchain/aws', () => ({ + ChatBedrockConverse: MockChatBedrockConverse, +})); + vi.mock('@opentelemetry/api', async (importOriginal) => { const actual = await importOriginal(); return { @@ -978,6 +983,7 @@ describe('model source', () => { beforeEach(() => { MockChatOpenAI.mockClear(); MockChatAnthropic.mockClear(); + MockChatBedrockConverse.mockClear(); mockCreateAgent.mockReset(); mockCreateAgent.mockReturnValue(makeMockAgent()); }); @@ -1024,7 +1030,7 @@ describe('model source', () => { it('prepends model.region onto the Bedrock model id once', async () => { const constructed = { tag: 'bedrock' }; - MockChatOpenAI.mockImplementation(function MockChatOpenAI() { + MockChatBedrockConverse.mockImplementation(function MockChatBedrockConverse() { return constructed; }); const cfg = { @@ -1033,16 +1039,17 @@ describe('model source', () => { model: { name: 'anthropic.claude-sonnet-4-5', region: 'us', parameters: { temperature: 0.2 } }, }; await createLangChainAgentsHandler()(cfg as any, 'q'); - expect(MockChatOpenAI).toHaveBeenCalledWith({ + expect(MockChatBedrockConverse).toHaveBeenCalledWith({ temperature: 0.2, model: 'us.anthropic.claude-sonnet-4-5', }); + expect(mockCreateAgent).toHaveBeenCalledWith(expect.objectContaining({ model: constructed })); expect(cfg.model.name).toBe('anthropic.claude-sonnet-4-5'); }); it('does not double a Bedrock inference-profile prefix', async () => { const constructed = { tag: 'bedrock' }; - MockChatOpenAI.mockImplementation(function MockChatOpenAI() { + MockChatBedrockConverse.mockImplementation(function MockChatBedrockConverse() { return constructed; }); const cfg = { @@ -1051,12 +1058,12 @@ describe('model source', () => { model: { name: 'us.anthropic.claude-sonnet-4-5', region: 'us' }, }; await createLangChainAgentsHandler()(cfg as any, 'q'); - expect(MockChatOpenAI).toHaveBeenCalledWith({ model: 'us.anthropic.claude-sonnet-4-5' }); + expect(MockChatBedrockConverse).toHaveBeenCalledWith({ model: 'us.anthropic.claude-sonnet-4-5' }); }); it('leaves a Bedrock model name unchanged when region is absent', async () => { const constructed = { tag: 'bedrock' }; - MockChatOpenAI.mockImplementation(function MockChatOpenAI() { + MockChatBedrockConverse.mockImplementation(function MockChatBedrockConverse() { return constructed; }); const cfg = { @@ -1065,7 +1072,7 @@ describe('model source', () => { model: { name: 'anthropic.claude-sonnet-4-5' }, }; await createLangChainAgentsHandler()(cfg as any, 'q'); - expect(MockChatOpenAI).toHaveBeenCalledWith({ model: 'anthropic.claude-sonnet-4-5' }); + expect(MockChatBedrockConverse).toHaveBeenCalledWith({ model: 'anthropic.claude-sonnet-4-5' }); }); it('ignores model.region for a non-Bedrock provider', async () => { diff --git a/packages/langchain-agents/src/handler.ts b/packages/langchain-agents/src/handler.ts index b4969d12..7070446a 100644 --- a/packages/langchain-agents/src/handler.ts +++ b/packages/langchain-agents/src/handler.ts @@ -275,6 +275,17 @@ async function resolveBaseModel(aiConfig: AiConfigRep, llm?: LangChainModelSourc // biome-ignore lint/suspicious/noExplicitAny: parameter bag is caller-owned and not remapped return new ChatAnthropic(modelConstructorArgs(invocation, 'claude-3-5-sonnet-20241022') as any); } + if (provider === 'bedrock') { + // biome-ignore lint/suspicious/noExplicitAny: @langchain/aws loaded via dynamic import with no static types + let mod: any; + try { + mod = await import('@langchain/aws'); + } catch { + throw new Error('Using Bedrock models requires @langchain/aws. Install it with: npm install @langchain/aws'); + } + // biome-ignore lint/suspicious/noExplicitAny: parameter bag is caller-owned and not remapped + return new mod.ChatBedrockConverse(modelConstructorArgs(invocation, '') as any); + } // biome-ignore lint/suspicious/noExplicitAny: parameter bag is caller-owned and not remapped return new ChatOpenAI(modelConstructorArgs(invocation, 'gpt-4o') as any); } diff --git a/packages/langchain-messages/package.json b/packages/langchain-messages/package.json index 2d1df407..124b8c8e 100644 --- a/packages/langchain-messages/package.json +++ b/packages/langchain-messages/package.json @@ -48,18 +48,23 @@ }, "peerDependencies": { "@langchain/anthropic": ">=0.3", + "@langchain/aws": ">=1.0", "@langchain/openai": ">=1.0" }, "peerDependenciesMeta": { "@langchain/anthropic": { "optional": true }, + "@langchain/aws": { + "optional": true + }, "@langchain/openai": { "optional": true } }, "devDependencies": { "@langchain/anthropic": "1.5.1", + "@langchain/aws": "1.3.9", "@langchain/openai": "^1.4.7", "@opentelemetry/context-async-hooks": "^2.8.0", "@opentelemetry/sdk-trace-base": "^2.8.0", diff --git a/packages/langchain-messages/src/__tests__/handler.test.ts b/packages/langchain-messages/src/__tests__/handler.test.ts index 0c4b94c3..9c437b67 100644 --- a/packages/langchain-messages/src/__tests__/handler.test.ts +++ b/packages/langchain-messages/src/__tests__/handler.test.ts @@ -66,7 +66,12 @@ vi.mock('@langchain/anthropic', () => ({ ChatAnthropic: vi.fn().mockImplementation(() => ({})), })); +vi.mock('@langchain/aws', () => ({ + ChatBedrockConverse: vi.fn().mockImplementation(() => ({})), +})); + import { ChatAnthropic } from '@langchain/anthropic'; +import { ChatBedrockConverse } from '@langchain/aws'; import { ChatOpenAI } from '@langchain/openai'; import { createLangChainHandler } from '../handler.js'; @@ -1254,6 +1259,7 @@ describe('model source', () => { beforeEach(() => { vi.mocked(ChatOpenAI).mockClear(); vi.mocked(ChatAnthropic).mockClear(); + vi.mocked(ChatBedrockConverse).mockClear(); }); it('calls a factory with the evaluated config and uses the returned model', async () => { @@ -1299,7 +1305,7 @@ describe('model source', () => { }); it('prepends model.region onto the Bedrock model id once', async () => { - vi.mocked(ChatOpenAI).mockImplementation(function MockChatOpenAI() { + vi.mocked(ChatBedrockConverse).mockImplementation(function MockChatBedrockConverse() { return makeMockLLM('bedrock') as any; }); const cfg = { @@ -1308,7 +1314,7 @@ describe('model source', () => { model: { name: 'anthropic.claude-sonnet-4-5', region: 'us', parameters: { temperature: 0.2 } }, }; await createLangChainHandler()(cfg as any, 'q'); - expect(ChatOpenAI).toHaveBeenCalledWith({ + expect(ChatBedrockConverse).toHaveBeenCalledWith({ temperature: 0.2, model: 'us.anthropic.claude-sonnet-4-5', }); @@ -1316,7 +1322,7 @@ describe('model source', () => { }); it('does not double a Bedrock inference-profile prefix', async () => { - vi.mocked(ChatOpenAI).mockImplementation(function MockChatOpenAI() { + vi.mocked(ChatBedrockConverse).mockImplementation(function MockChatBedrockConverse() { return makeMockLLM('bedrock') as any; }); const cfg = { @@ -1325,11 +1331,11 @@ describe('model source', () => { model: { name: 'us.anthropic.claude-sonnet-4-5', region: 'us' }, }; await createLangChainHandler()(cfg as any, 'q'); - expect(ChatOpenAI).toHaveBeenCalledWith({ model: 'us.anthropic.claude-sonnet-4-5' }); + expect(ChatBedrockConverse).toHaveBeenCalledWith({ model: 'us.anthropic.claude-sonnet-4-5' }); }); it('leaves a Bedrock model name unchanged when region is absent', async () => { - vi.mocked(ChatOpenAI).mockImplementation(function MockChatOpenAI() { + vi.mocked(ChatBedrockConverse).mockImplementation(function MockChatBedrockConverse() { return makeMockLLM('bedrock') as any; }); const cfg = { @@ -1338,7 +1344,7 @@ describe('model source', () => { model: { name: 'anthropic.claude-sonnet-4-5' }, }; await createLangChainHandler()(cfg as any, 'q'); - expect(ChatOpenAI).toHaveBeenCalledWith({ model: 'anthropic.claude-sonnet-4-5' }); + expect(ChatBedrockConverse).toHaveBeenCalledWith({ model: 'anthropic.claude-sonnet-4-5' }); }); it('ignores model.region for a non-Bedrock provider', async () => { diff --git a/packages/langchain-messages/src/handler.ts b/packages/langchain-messages/src/handler.ts index 47a24dfd..d29aada0 100644 --- a/packages/langchain-messages/src/handler.ts +++ b/packages/langchain-messages/src/handler.ts @@ -181,8 +181,8 @@ function modelConstructorArgs(config: AiConfigRep, fallbackName: string): Record * A function in `llm` is called with the evaluated config. An instance is used as-is. * Otherwise, the provider and model name from the AI config are used to * instantiate the appropriate model via a dynamic import, so that neither - * @langchain/openai nor @langchain/anthropic is a hard dependency. Parameters are - * passed through unchanged. + * @langchain/openai, @langchain/anthropic, nor @langchain/aws is a hard + * dependency. Parameters are passed through unchanged. */ async function resolveBaseModel(config: AiConfigRep, llm?: LangChainModelSource): Promise { const invocation = configForModelCall(config); @@ -201,6 +201,16 @@ async function resolveBaseModel(config: AiConfigRep, llm?: LangChainModelSource) } return new mod.ChatAnthropic(modelConstructorArgs(invocation, 'claude-3-5-sonnet-20241022')); } + if (providerName === 'bedrock') { + // biome-ignore lint/suspicious/noExplicitAny: @langchain/aws loaded via dynamic import with no static types + let mod: any; + try { + mod = await import('@langchain/aws'); + } catch { + throw new Error('Using Bedrock models requires @langchain/aws. Install it with: npm install @langchain/aws'); + } + return new mod.ChatBedrockConverse(modelConstructorArgs(invocation, '')); + } // biome-ignore lint/suspicious/noExplicitAny: @langchain/openai loaded via dynamic import with no static types let mod: any; try { diff --git a/yarn.lock b/yarn.lock index 96b77c51..53e5f39d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -64,6 +64,276 @@ json-schema-to-ts "^3.1.1" standardwebhooks "^1.0.0" +"@aws-sdk/client-bedrock-agent-runtime@^3.1045.0": + version "3.1135.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-bedrock-agent-runtime/-/client-bedrock-agent-runtime-3.1135.0.tgz#e943595e166eb500119a04bfe686162d6abae58a" + integrity sha512-hAM1FAW4LDBNy36Y8Hv0ZAShvKdvdKYXXh5EWiLP75GkAKQr3dolMOTHBps9db8a+iM865pYYmKipe7It0SwqA== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/credential-provider-node" "^3.972.83" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/fetch-http-handler" "^5.7.2" + "@smithy/node-http-handler" "^4.11.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/client-bedrock-runtime@^3.1006.0": + version "3.1135.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1135.0.tgz#271bfb1c8e24b5f95ab7f75c509ba0810da8428f" + integrity sha512-DDTMmWwVAJYsOOb83thk8AkBAJMQIJDm89n/doYbtlks9KGc//mbPYYvxfWgVTqFlUu+oCqwF2sJT2FxnSMk1Q== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/credential-provider-node" "^3.972.83" + "@aws-sdk/eventstream-handler-node" "^3.972.34" + "@aws-sdk/middleware-eventstream" "^3.972.29" + "@aws-sdk/middleware-websocket" "^3.972.53" + "@aws-sdk/token-providers" "3.1135.0" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/fetch-http-handler" "^5.7.2" + "@smithy/node-http-handler" "^4.11.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/client-kendra@^3.1006.0": + version "3.1135.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-kendra/-/client-kendra-3.1135.0.tgz#2b1fdcfbefc41175ce51b4a33e25dd78af658bb4" + integrity sha512-qvXL482IVbYhY6CPjyHqoNyBogFm8gmMA/i638aqrKWt5FGfqeLp2kclRMxasJH50k4gPuZXD5FBe0iytLITXA== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/credential-provider-node" "^3.972.83" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/fetch-http-handler" "^5.7.2" + "@smithy/node-http-handler" "^4.11.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/core@^3.978.0": + version "3.978.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.978.0.tgz#115e5d2e03edd380fe270d4617db0f4cc75869d6" + integrity sha512-2yX9LUmxPklVjSGTb8dfnWRJSiFQ3TeH2nn7G1mdKHTfnabzF0+gfrS8rYfLWmZrQ8A3mEcxMJjRc51dL5KWaA== + dependencies: + "@aws-sdk/types" "^3.974.5" + "@aws-sdk/xml-builder" "^3.972.40" + "@aws/lambda-invoke-store" "^0.3.0" + "@smithy/core" "^3.33.3" + "@smithy/signature-v4" "^5.6.12" + "@smithy/types" "^4.17.2" + bowser "^2.11.0" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-env@^3.972.71": + version "3.972.71" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.71.tgz#98261bde85bd0f2e2d4a7ba97dce679072e16aff" + integrity sha512-JN+JHruYZw3GUZB8YGAlDk4wTDPOEAEEdEzj5nS0xodWR4smzHsN7PnK2j6IeOsDIj2aqua5DSbhXl9Gtf90FQ== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-http@^3.972.73": + version "3.972.73" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.73.tgz#879466b23dcaec4bee635abf1103e1f5f35d122d" + integrity sha512-uyYYnJOnlis8uQzaYGPd7N1JoioCoNpXgnkXYixsWJXHXgXyYi8WXJSDfofxJeWfQIGWLe2Nwyq60Uc7MZdVOg== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/fetch-http-handler" "^5.7.2" + "@smithy/node-http-handler" "^4.11.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-ini@^3.973.16": + version "3.973.16" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.16.tgz#b51db2069b915d563e05969cac845f624aed1dec" + integrity sha512-i++ly+0Uxa+u3ebSSyr0S/3CFhFJDxCXT3+Zj+mW2bXenEx5bKGCdTIKFu39SgXBNhWDjex/8cXUx9MUTMCrTw== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/credential-provider-env" "^3.972.71" + "@aws-sdk/credential-provider-http" "^3.972.73" + "@aws-sdk/credential-provider-login" "^3.972.78" + "@aws-sdk/credential-provider-process" "^3.972.71" + "@aws-sdk/credential-provider-sso" "^3.973.15" + "@aws-sdk/credential-provider-web-identity" "^3.972.77" + "@aws-sdk/nested-clients" "^3.997.45" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/credential-provider-imds" "^4.4.16" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-login@^3.972.78": + version "3.972.78" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.78.tgz#c1c7ca497201a7f2bd6ba7683fa3a896c4e218f8" + integrity sha512-eUtswnXu0+Ii9ieRK+0L7aPFV3Z/dnW2VntJzjBP9xs8s+8p5nBNuymIXtXwZ+5r5+XJP3e32nMkuZ/r0HozEA== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/nested-clients" "^3.997.45" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-node@^3.972.29", "@aws-sdk/credential-provider-node@^3.972.83": + version "3.972.83" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.83.tgz#f4e16b3a1d5d0237460c1b532de37eb6505985dc" + integrity sha512-jdso7ejzfRnatxMUZK4S/U6KbaDPCvfIV4XL+IQAPFDBt5rj5Fq595euqlK8Le4lNCMFR9oUpt+1l0aMgaayOQ== + dependencies: + "@aws-sdk/credential-provider-env" "^3.972.71" + "@aws-sdk/credential-provider-http" "^3.972.73" + "@aws-sdk/credential-provider-ini" "^3.973.16" + "@aws-sdk/credential-provider-process" "^3.972.71" + "@aws-sdk/credential-provider-sso" "^3.973.15" + "@aws-sdk/credential-provider-web-identity" "^3.972.77" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/credential-provider-imds" "^4.4.16" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-process@^3.972.71": + version "3.972.71" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.71.tgz#5a2ff72bcc2d7a658ca6472193ea33437c8b5a4e" + integrity sha512-lYmXJa4gvq4xN1lrT5NiP5vIYYKcGWAdj8y+8o6dlcateB5eF3Dn8DtmjjHKfMBrTPAMr2pebIiX/UOj8c1/UA== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-sso@^3.973.15": + version "3.973.15" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.15.tgz#6a1391ca239f6d505dcb55ea6655b8c14fad5af7" + integrity sha512-6Jhcf4v0pSFdjk1EW2kvzuEBKD+UZ2uNcHUIglKKLndD20YhvkL2kdmDOV5/j4mYuWWwe/a1FQ1aomU86/Cg5Q== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/nested-clients" "^3.997.45" + "@aws-sdk/token-providers" "3.1129.0" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-web-identity@^3.972.77": + version "3.972.77" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.77.tgz#5e288111937fcb6f9f02236fa153095d6e24d22f" + integrity sha512-uylIQSUWpfLuH2LovxEEfwzJGM/SabLOfLMg6YXu/E8jJEKUdpdILCVCQCdFvHyu/7dLJOHPMfrSwduxO56NkQ== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/nested-clients" "^3.997.45" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/eventstream-handler-node@^3.972.34": + version "3.972.34" + resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.34.tgz#e47796b979899da15316b61ed73b1e401edfe97a" + integrity sha512-cTeVzpu1xEAkryTZBYhGwnQ6gOGyp8ZYZvmn0Sg/nI/ABmy/CRHHxPDJDUi9PxwxUtGGaatvfRUB3FCgT/rSWw== + dependencies: + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/middleware-eventstream@^3.972.29": + version "3.972.29" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.29.tgz#6509eab458688e6048e289d7166848ef73996f80" + integrity sha512-dlRzHCgyB8W6hLuDC5pcT5q+ziPt00n4QGgGBE17ucLVU4zMa6lsbuUdQ2Pm75Z5VA8GF+R/+SgrRcaTdIzSIQ== + dependencies: + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/middleware-websocket@^3.972.53": + version "3.972.53" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.53.tgz#02d815968250eff8a271f0f26e689ac79f73b97d" + integrity sha512-bIrDaMENQmYRBHntOiOheqkiw5+fhKW4Lqb+mS1uqF0VwvdWI22fW2HFgWrng66CmYd+4k8ePlpj38sEfTuMLQ== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/fetch-http-handler" "^5.7.2" + "@smithy/signature-v4" "^5.6.12" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/nested-clients@^3.997.45": + version "3.997.45" + resolved "https://registry.yarnpkg.com/@aws-sdk/nested-clients/-/nested-clients-3.997.45.tgz#3ed0e5fcd25c889025b2379a9434594fe7056a4a" + integrity sha512-mooq9Q+jLa18VoM7HouczmslZU60iiB0aKc/Ztnq/luIL1ud0z4DnYprLR/ZO1gp331S9tJctM1HZr7u6YKBXQ== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/signature-v4-multi-region" "^3.996.46" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/fetch-http-handler" "^5.7.2" + "@smithy/node-http-handler" "^4.11.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/signature-v4-multi-region@^3.996.46": + version "3.996.46" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.46.tgz#ee57afe5282da4b57968061ef8706d32890267ab" + integrity sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ== + dependencies: + "@aws-sdk/types" "^3.974.5" + "@smithy/signature-v4" "^5.6.12" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/token-providers@3.1129.0": + version "3.1129.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.1129.0.tgz#a4a11a789d7259874ccf3733e538fc70f8a289e4" + integrity sha512-Sbl3rpzQdsG4ZK2zh0JWUYyZPKKorJlVOddA2T0DVbKJFrsW8J6wgnslxxUH04+WaBMr4A1HzJZvZX0xUvkniA== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/nested-clients" "^3.997.45" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/token-providers@3.1135.0": + version "3.1135.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.1135.0.tgz#81f4e70394082db32ab566c5fe26dd2eb7d1b24a" + integrity sha512-JEYc/NHSDKGLmtQtaG5xZ1hmyFZ0Lav6GGAG417VaYyUURT0EeQEAnfVIkZQsLg5+P8BxCAv8YLIEpU2aNh++A== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/nested-clients" "^3.997.45" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/types@^3.974.5": + version "3.974.5" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.974.5.tgz#a52691531738d191b8b3fd1ae7757c2eeec7afee" + integrity sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ== + dependencies: + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/xml-builder@^3.972.40": + version "3.972.40" + resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.972.40.tgz#3cdb5f3400860ad91301aecb87fb5148a66d6ff7" + integrity sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA== + dependencies: + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws/lambda-invoke-store@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz#708802d987f8e17bdf4af4de1031660ce1cdd65f" + integrity sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ== + "@babel/runtime@^7.18.3": version "7.29.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.7.tgz#12022450c45a4da6d8d8287b18a4ff2ddb23f768" @@ -298,6 +568,16 @@ "@anthropic-ai/sdk" "^0.103.0" zod "^3.25.76 || ^4" +"@langchain/aws@1.3.9": + version "1.3.9" + resolved "https://registry.yarnpkg.com/@langchain/aws/-/aws-1.3.9.tgz#2c0ea45165d9cdb915becf5c12ff5af55736a552" + integrity sha512-EA0iSMn05hCgF2RkYWTG9knmq7G2OOyfoVNx5xbnPxDS9qHGs/8mCe3qVihSSN6TSl5OyLYsYhfoKSSJSJjscg== + dependencies: + "@aws-sdk/client-bedrock-agent-runtime" "^3.1045.0" + "@aws-sdk/client-bedrock-runtime" "^3.1006.0" + "@aws-sdk/client-kendra" "^3.1006.0" + "@aws-sdk/credential-provider-node" "^3.972.29" + "@langchain/core@^1.1.48": version "1.1.49" resolved "https://registry.yarnpkg.com/@langchain/core/-/core-1.1.49.tgz#2e334c843961e9bfc52f3fcff19247602e0be866" @@ -635,6 +915,57 @@ resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz#e3fcee093fbb5ce765e1ad088ff4de2889f6f9be" integrity sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw== +"@smithy/core@^3.33.2", "@smithy/core@^3.33.3": + version "3.34.1" + resolved "https://registry.yarnpkg.com/@smithy/core/-/core-3.34.1.tgz#2fb8133e9f3e99c3f88c8136da2a46cfee4e4fe4" + integrity sha512-dLcOUxz8YCv1RZUMKq6GbyUf95pLbrqh34bPvpCZ1+CByFF31BEAFewZjsGCnVsZTKdThNENfGyAgk2TJqVwSw== + dependencies: + "@smithy/types" "^4.18.0" + tslib "^2.6.2" + +"@smithy/credential-provider-imds@^4.4.16": + version "4.5.2" + resolved "https://registry.yarnpkg.com/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz#da3ea9636b5566b36b0761382d841a6d8fdde14f" + integrity sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg== + dependencies: + "@smithy/core" "^3.33.2" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@smithy/fetch-http-handler@^5.7.2": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz#349464e31682d739a74efa42362756be49eb2883" + integrity sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA== + dependencies: + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.18.0" + tslib "^2.6.2" + +"@smithy/node-http-handler@^4.11.3": + version "4.12.1" + resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz#e4da9379d228517ba85845081e4e6758ca315c5d" + integrity sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw== + dependencies: + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.18.0" + tslib "^2.6.2" + +"@smithy/signature-v4@^5.6.12": + version "5.7.3" + resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-5.7.3.tgz#76603bf1ac9aa44e1d2f4f358b0cc0cf84961e46" + integrity sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow== + dependencies: + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@smithy/types@^4.17.2", "@smithy/types@^4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.18.0.tgz#f68aca5b5e39f1f9def3827224e4a1e43d233347" + integrity sha512-CgB6HHWer/vrKps24ulRIbpcpb7K4xAU7SkZ7YHzBPlwHsvsrCJFEXK421s+cJzX+ZrqtA/TuU5w1HzI7k9N8A== + dependencies: + tslib "^2.6.2" + "@stablelib/base64@^1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/@stablelib/base64/-/base64-1.0.1.tgz#bdfc1c6d3a62d7a3b7bbc65b6cce1bb4561641be" @@ -811,6 +1142,11 @@ body-parser@^2.2.1: raw-body "^3.0.2" type-is "^2.1.0" +bowser@^2.11.0: + version "2.14.1" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.14.1.tgz#4ea39bf31e305184522d7ad7bfd91389e4f0cb79" + integrity sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg== + bytes@^3.1.2, bytes@~3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" @@ -1875,7 +2211,7 @@ ts-algebra@^2.0.0: resolved "https://registry.yarnpkg.com/ts-algebra/-/ts-algebra-2.0.0.tgz#4e3e0953878f26518fce7f6bb115064a65388b7a" integrity sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw== -tslib@^2.4.0: +tslib@^2.4.0, tslib@^2.6.2: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== From 766f4996e24d4fd23c9af36e24642158636a5f7f Mon Sep 17 00:00:00 2001 From: Andrew Klatzke Date: Fri, 18 Sep 2026 13:05:30 -0800 Subject: [PATCH 3/4] fix(AIC-3382): strip duplicate Bedrock tools Prevent payload-owned tool definitions from leaking into ChatBedrockConverse constructor arguments while preserving the evaluated config. Co-authored-by: Cursor --- .../src/__tests__/handler.test.ts | 29 +++++++++++++++---- packages/langchain-agents/src/handler.ts | 6 ++-- .../src/__tests__/handler.test.ts | 20 +++++++++++-- packages/langchain-messages/src/handler.ts | 6 ++-- 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/packages/langchain-agents/src/__tests__/handler.test.ts b/packages/langchain-agents/src/__tests__/handler.test.ts index c63656f3..c499bd7b 100644 --- a/packages/langchain-agents/src/__tests__/handler.test.ts +++ b/packages/langchain-agents/src/__tests__/handler.test.ts @@ -1008,8 +1008,17 @@ describe('model source', () => { MockChatOpenAI.mockImplementation(function MockChatOpenAI() { return constructed; }); - await createLangChainAgentsHandler()(parameterized as any, 'q'); - expect(MockChatOpenAI).toHaveBeenCalledWith({ temperature: 0.2, max_tokens: 512, model: 'gpt-4o' }); + const cfg = { + ...parameterized, + model: { ...parameterized.model, parameters: { ...parameterized.model.parameters, tools: ['openai-tool'] } }, + }; + await createLangChainAgentsHandler()(cfg as any, 'q'); + expect(MockChatOpenAI).toHaveBeenCalledWith({ + temperature: 0.2, + max_tokens: 512, + tools: ['openai-tool'], + model: 'gpt-4o', + }); expect(mockCreateAgent).toHaveBeenCalledWith(expect.objectContaining({ model: constructed })); }); @@ -1036,14 +1045,24 @@ describe('model source', () => { const cfg = { ...parameterized, provider: { name: 'Bedrock' }, - model: { name: 'anthropic.claude-sonnet-4-5', region: 'us', parameters: { temperature: 0.2 } }, + tools: { + search: { name: 'search', type: 'function', parameters: { type: 'object' }, description: 'Search' }, + }, + model: { + name: 'anthropic.claude-sonnet-4-5', + region: 'us', + parameters: { temperature: 0.2, tools: [{ name: 'duplicated-search' }] }, + }, }; - await createLangChainAgentsHandler()(cfg as any, 'q'); + await createLangChainAgentsHandler()(cfg as any, 'q', { search: vi.fn() }); expect(MockChatBedrockConverse).toHaveBeenCalledWith({ temperature: 0.2, model: 'us.anthropic.claude-sonnet-4-5', }); - expect(mockCreateAgent).toHaveBeenCalledWith(expect.objectContaining({ model: constructed })); + expect(mockCreateAgent).toHaveBeenCalledWith( + expect.objectContaining({ model: constructed, tools: expect.arrayContaining([expect.anything()]) }), + ); + expect(cfg.model.parameters.tools).toEqual([{ name: 'duplicated-search' }]); expect(cfg.model.name).toBe('anthropic.claude-sonnet-4-5'); }); diff --git a/packages/langchain-agents/src/handler.ts b/packages/langchain-agents/src/handler.ts index 7070446a..98650611 100644 --- a/packages/langchain-agents/src/handler.ts +++ b/packages/langchain-agents/src/handler.ts @@ -260,8 +260,10 @@ export function buildSpanCallbacks( export type LangChainModelSource = BaseChatModel | ((config: AiConfigRep) => BaseChatModel | Promise); function modelConstructorArgs(config: AiConfigRep, fallbackName: string): Record { - const parameters = - config.model?.parameters && typeof config.model.parameters === 'object' ? config.model.parameters : {}; + const parameters = { + ...(config.model?.parameters && typeof config.model.parameters === 'object' ? config.model.parameters : {}), + }; + if ((config.provider?.name ?? '').toLowerCase() === 'bedrock') delete parameters.tools; return { ...parameters, model: resolvedModelName(config, fallbackName) }; } diff --git a/packages/langchain-messages/src/__tests__/handler.test.ts b/packages/langchain-messages/src/__tests__/handler.test.ts index 9c437b67..15853414 100644 --- a/packages/langchain-messages/src/__tests__/handler.test.ts +++ b/packages/langchain-messages/src/__tests__/handler.test.ts @@ -1283,8 +1283,17 @@ describe('model source', () => { vi.mocked(ChatOpenAI).mockImplementation(function MockChatOpenAI() { return makeMockLLM('default-openai') as any; }); - await createLangChainHandler()(parameterized as any, 'q'); - expect(ChatOpenAI).toHaveBeenCalledWith({ temperature: 0.2, max_tokens: 512, model: 'gpt-4o' }); + const cfg = { + ...parameterized, + model: { ...parameterized.model, parameters: { ...parameterized.model.parameters, tools: ['openai-tool'] } }, + }; + await createLangChainHandler()(cfg as any, 'q'); + expect(ChatOpenAI).toHaveBeenCalledWith({ + temperature: 0.2, + max_tokens: 512, + tools: ['openai-tool'], + model: 'gpt-4o', + }); }); it('spreads model.parameters into the default Anthropic constructor', async () => { @@ -1311,13 +1320,18 @@ describe('model source', () => { const cfg = { ...parameterized, provider: { name: 'Bedrock' }, - model: { name: 'anthropic.claude-sonnet-4-5', region: 'us', parameters: { temperature: 0.2 } }, + model: { + name: 'anthropic.claude-sonnet-4-5', + region: 'us', + parameters: { temperature: 0.2, tools: [{ name: 'duplicated-search' }] }, + }, }; await createLangChainHandler()(cfg as any, 'q'); expect(ChatBedrockConverse).toHaveBeenCalledWith({ temperature: 0.2, model: 'us.anthropic.claude-sonnet-4-5', }); + expect(cfg.model.parameters.tools).toEqual([{ name: 'duplicated-search' }]); expect(cfg.model.name).toBe('anthropic.claude-sonnet-4-5'); }); diff --git a/packages/langchain-messages/src/handler.ts b/packages/langchain-messages/src/handler.ts index d29aada0..6abb89d0 100644 --- a/packages/langchain-messages/src/handler.ts +++ b/packages/langchain-messages/src/handler.ts @@ -170,8 +170,10 @@ function normalizeOutputSchema(schema: Record): Record BaseChatModel | Promise); function modelConstructorArgs(config: AiConfigRep, fallbackName: string): Record { - const parameters = - config.model?.parameters && typeof config.model.parameters === 'object' ? config.model.parameters : {}; + const parameters = { + ...(config.model?.parameters && typeof config.model.parameters === 'object' ? config.model.parameters : {}), + }; + if ((config.provider?.name ?? '').toLowerCase() === 'bedrock') delete parameters.tools; // Name from the config always wins over a colliding `model` key in the parameter bag. return { ...parameters, model: resolvedModelName(config, fallbackName) }; } From fc1079ddb993ee32b4a97268e35996d0dea8a104 Mon Sep 17 00:00:00 2001 From: Andrew Klatzke Date: Fri, 18 Sep 2026 13:12:48 -0800 Subject: [PATCH 4/4] fix(AIC-3382): align streaming example output with Python Print usage and judges on stdout and drop leftover debug logs so both samples behave the same. Co-authored-by: Cursor --- examples/streaming.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/streaming.ts b/examples/streaming.ts index 5770959b..3a94077b 100644 --- a/examples/streaming.ts +++ b/examples/streaming.ts @@ -31,15 +31,14 @@ export async function run(key: string, userInput: string): Promise { } else { // Final event — full response + normalized usage if (!wroteChunk) { - process.stdout.write(event.response); + process.stdout.write(event.response ?? ''); } process.stdout.write('\n\n'); - process.stderr.write(`[debug] done event response length: ${event.response?.length ?? 0}\n`); - process.stderr.write(`[debug] done event response preview: ${String(event.response).slice(0, 100)}\n`); - + process.stdout.write(`Usage: ${JSON.stringify(event.usage, null, 2)}\n`); if (event.judgeResults) { - process.stderr.write(`[judges] ${JSON.stringify(event.judgeResults)}\n`); + process.stdout.write(`Judge results: ${JSON.stringify(event.judgeResults, null, 2)}\n`); } + process.stdout.write('\n'); } } }