From 00d268ff77085b2163e9abd21ef5bf60a3c052ab Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Fri, 3 Oct 2025 15:39:26 +0100 Subject: [PATCH 1/4] feat(langchain/createAgent): Add support for callOptions to middleware --- .../agents/middlewareAgent/nodes/AgentNode.ts | 6 +- .../middlewareAgent/tests/callOptions.test.ts | 284 ++++++++++++++++++ .../src/agents/middlewareAgent/types.ts | 5 + 3 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 libs/langchain/src/agents/middlewareAgent/tests/callOptions.test.ts diff --git a/libs/langchain/src/agents/middlewareAgent/nodes/AgentNode.ts b/libs/langchain/src/agents/middlewareAgent/nodes/AgentNode.ts index ea69397d842d..64f67e5c8fd1 100644 --- a/libs/langchain/src/agents/middlewareAgent/nodes/AgentNode.ts +++ b/libs/langchain/src/agents/middlewareAgent/nodes/AgentNode.ts @@ -290,7 +290,11 @@ export class AgentNode< } const signal = mergeAbortSignals(this.#options.signal, config.signal); - const invokeConfig = { ...config, signal }; + const invokeConfig = { + ...config, + signal, + ...preparedOptions?.callOptions, + }; const response = (await modelWithTools.invoke( modelInput, invokeConfig diff --git a/libs/langchain/src/agents/middlewareAgent/tests/callOptions.test.ts b/libs/langchain/src/agents/middlewareAgent/tests/callOptions.test.ts new file mode 100644 index 000000000000..eb0b785413db --- /dev/null +++ b/libs/langchain/src/agents/middlewareAgent/tests/callOptions.test.ts @@ -0,0 +1,284 @@ +import { expect, describe, it, vi, type MockInstance } from "vitest"; +import { HumanMessage, AIMessage } from "@langchain/core/messages"; +import type { LanguageModelLike } from "@langchain/core/language_models/base"; + +import { createAgent, createMiddleware } from "../index.js"; + +function createMockModel(name = "ChatAnthropic", model = "anthropic") { + // Mock Anthropic model + const invokeCallback = vi + .fn() + .mockResolvedValue(new AIMessage("Response from model")); + return { + getName: () => name, + bindTools: vi.fn().mockReturnThis(), + _streamResponseChunks: vi.fn().mockReturnThis(), + bind: vi.fn().mockReturnThis(), + invoke: invokeCallback, + lc_runnable: true, + _modelType: model, + _generate: vi.fn(), + _llmType: () => model, + } as unknown as LanguageModelLike; +} + +describe("callOptions middleware support", () => { + it("should pass callOptions from middleware to model.invoke", async () => { + const model = createMockModel(); + const middleware = createMiddleware({ + name: "testMiddleware", + modifyModelRequest: async (request) => { + return { + ...request, + callOptions: { + temperature: 0.5, + max_tokens: 100, + }, + }; + }, + }); + + const agent = createAgent({ + model, + tools: [], + middleware: [middleware] as const, + }); + + await agent.invoke({ + messages: [new HumanMessage("Hello, world!")], + }); + + expect(model.invoke).toHaveBeenCalled(); + const callArgs = (model.invoke as unknown as MockInstance).mock.calls[0]; + const config = callArgs[1]; + expect(config).toHaveProperty("temperature", 0.5); + expect(config).toHaveProperty("max_tokens", 100); + }); + + it("should pass headers from middleware callOptions to model.invoke", async () => { + const model = createMockModel(); + const middleware = createMiddleware({ + name: "testMiddleware", + modifyModelRequest: async (request) => { + return { + ...request, + callOptions: { + headers: { + "X-Custom-Header": "middleware-value", + "X-Middleware-Only": "middleware-only", + }, + }, + }; + }, + }); + + const agent = createAgent({ + model, + tools: [], + middleware: [middleware] as const, + }); + + await agent.invoke({ + messages: [new HumanMessage("Hello, world!")], + }); + + expect(model.invoke).toHaveBeenCalled(); + const callArgs = (model.invoke as unknown as MockInstance).mock.calls[0]; + const config = callArgs[1]; + expect(config.headers).toEqual({ + "X-Custom-Header": "middleware-value", + "X-Middleware-Only": "middleware-only", + }); + }); + + it("should pass headers from middleware callOptions to model.invoke config", async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const model = createMockModel() as any; + + // Middleware that adds headers via callOptions + const middleware = createMiddleware({ + name: "testMiddleware", + modifyModelRequest: async (request) => { + return { + ...request, + callOptions: { + headers: { + "X-Middleware-Header": "from-middleware", + "X-Custom-Header": "custom-value", + }, + }, + }; + }, + }); + + const agent = createAgent({ + model, + tools: [], + middleware: [middleware] as const, + }); + + await agent.invoke({ + messages: [new HumanMessage("Hello, world!")], + }); + + // Verify model.invoke was called with headers in the config + expect(model.invoke).toHaveBeenCalled(); + const callArgs = (model.invoke as unknown as MockInstance).mock.calls[0]; + const config = callArgs[1]; + + expect(config.headers).toEqual({ + "X-Middleware-Header": "from-middleware", + "X-Custom-Header": "custom-value", + }); + }); + + it("should handle callOptions without headers", async () => { + const model = createMockModel(); + const middleware = createMiddleware({ + name: "testMiddleware", + modifyModelRequest: async (request) => { + return { + ...request, + callOptions: { + temperature: 0.7, + }, + }; + }, + }); + + const agent = createAgent({ + model, + tools: [], + middleware: [middleware] as const, + }); + + await agent.invoke({ + messages: [new HumanMessage("Hello, world!")], + }); + + expect(model.invoke).toHaveBeenCalled(); + const callArgs = (model.invoke as unknown as MockInstance).mock.calls[0]; + const config = callArgs[1]; + expect(config).toHaveProperty("temperature", 0.7); + expect(config.headers).toBeUndefined(); + }); + + it("should only add headers when either config or callOptions has headers", async () => { + const model = createMockModel(); + const middleware = createMiddleware({ + name: "testMiddleware", + modifyModelRequest: async (request) => { + return { + ...request, + callOptions: { + temperature: 0.8, + }, + }; + }, + }); + + const agent = createAgent({ + model, + tools: [], + middleware: [middleware] as const, + }); + + await agent.invoke({ + messages: [new HumanMessage("Hello, world!")], + }); + + expect(model.invoke).toHaveBeenCalled(); + const callArgs = (model.invoke as unknown as MockInstance).mock.calls[0]; + const config = callArgs[1]; + expect(config).not.toHaveProperty("headers"); + }); + + it("should support multiple middleware with callOptions", async () => { + const model = createMockModel(); + const middleware1 = createMiddleware({ + name: "middleware1", + modifyModelRequest: async (request) => { + return { + ...request, + callOptions: { + temperature: 0.5, + headers: { + "X-Middleware-1": "value1", + }, + }, + }; + }, + }); + + const middleware2 = createMiddleware({ + name: "middleware2", + modifyModelRequest: async (request) => { + return { + ...request, + callOptions: { + max_tokens: 200, + headers: { + "X-Middleware-2": "value2", + }, + }, + }; + }, + }); + + const agent = createAgent({ + model, + tools: [], + middleware: [middleware1, middleware2] as const, + }); + + await agent.invoke({ + messages: [new HumanMessage("Hello, world!")], + }); + + expect(model.invoke).toHaveBeenCalled(); + const callArgs = (model.invoke as unknown as MockInstance).mock.calls[0]; + const config = callArgs[1]; + // Last middleware wins for overlapping properties + expect(config).toHaveProperty("max_tokens", 200); + expect(config.headers).toEqual({ + "X-Middleware-2": "value2", + }); + }); + + it("should preserve signal from config when merging callOptions", async () => { + const model = createMockModel(); + const abortController = new AbortController(); + const middleware = createMiddleware({ + name: "testMiddleware", + modifyModelRequest: async (request) => { + return { + ...request, + callOptions: { + temperature: 0.5, + }, + }; + }, + }); + + const agent = createAgent({ + model, + tools: [], + middleware: [middleware] as const, + }); + + await agent.invoke( + { + messages: [new HumanMessage("Hello, world!")], + }, + { + signal: abortController.signal, + } + ); + + expect(model.invoke).toHaveBeenCalled(); + const callArgs = (model.invoke as unknown as MockInstance).mock.calls[0]; + const config = callArgs[1]; + expect(config).toHaveProperty("signal"); + expect(config).toHaveProperty("temperature", 0.5); + }); +}); diff --git a/libs/langchain/src/agents/middlewareAgent/types.ts b/libs/langchain/src/agents/middlewareAgent/types.ts index ba24e53f24ff..911520b3034c 100644 --- a/libs/langchain/src/agents/middlewareAgent/types.ts +++ b/libs/langchain/src/agents/middlewareAgent/types.ts @@ -152,6 +152,11 @@ export interface ModelRequest { * The tools to make available for this step. */ tools: string[]; + + /** + * Extra call options to pass to the model. + */ + callOptions?: Record; } /** From f53edc20ef41effc43f8ffc04344acae9ae0197d Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 14 Oct 2025 12:16:05 +0100 Subject: [PATCH 2/4] `modelSettings` instead of `callOptions` --- libs/langchain/src/agents/nodes/AgentNode.ts | 7 +- libs/langchain/src/agents/nodes/types.ts | 15 ++- ...lOptions.test.ts => modelSettings.test.ts} | 122 ++++++++++-------- 3 files changed, 82 insertions(+), 62 deletions(-) rename libs/langchain/src/agents/tests/{callOptions.test.ts => modelSettings.test.ts} (59%) diff --git a/libs/langchain/src/agents/nodes/AgentNode.ts b/libs/langchain/src/agents/nodes/AgentNode.ts index 3956c4a6fa8b..c0da86659240 100644 --- a/libs/langchain/src/agents/nodes/AgentNode.ts +++ b/libs/langchain/src/agents/nodes/AgentNode.ts @@ -300,11 +300,7 @@ export class AgentNode< modelInput = { ...modelInput, messages: request.messages }; const signal = mergeAbortSignals(this.#options.signal, config.signal); - const invokeConfig = { - ...config, - signal, - ...request?.callOptions, - }; + const invokeConfig = { ...config, signal }; const response = (await modelWithTools.invoke( modelInput, invokeConfig @@ -810,6 +806,7 @@ export class AgentNode< */ const modelWithTools = await bindTools(model, allTools, { ...options, + ...(preparedOptions?.modelSettings ?? {}), tool_choice: toolChoice, }); diff --git a/libs/langchain/src/agents/nodes/types.ts b/libs/langchain/src/agents/nodes/types.ts index e2a8b2cc9308..06f3ea0d36c5 100644 --- a/libs/langchain/src/agents/nodes/types.ts +++ b/libs/langchain/src/agents/nodes/types.ts @@ -56,7 +56,18 @@ export interface ModelRequest< runtime: Runtime; /** - * Extra call options to pass to the model. + * Additional settings to bind to the model when preparing it for invocation. + * These settings are applied via `bindTools()` and can include parameters like + * `headers`, `container`, etc. The model is re-bound on each request, + * so these settings can vary per invocation. + * + * @example + * ```ts + * modelSettings: { + * headers: { "anthropic-beta": "code-execution-2025-08-25" }, + * container: "container_abc123" + * } + * ``` */ - callOptions?: Record; + modelSettings?: Record; } diff --git a/libs/langchain/src/agents/tests/callOptions.test.ts b/libs/langchain/src/agents/tests/modelSettings.test.ts similarity index 59% rename from libs/langchain/src/agents/tests/callOptions.test.ts rename to libs/langchain/src/agents/tests/modelSettings.test.ts index 66d2046e1e1d..eba7dc51cfc8 100644 --- a/libs/langchain/src/agents/tests/callOptions.test.ts +++ b/libs/langchain/src/agents/tests/modelSettings.test.ts @@ -22,15 +22,16 @@ function createMockModel(name = "ChatAnthropic", model = "anthropic") { } as unknown as LanguageModelLike; } -describe("callOptions middleware support", () => { - it("should pass callOptions from middleware to model.invoke", async () => { - const model = createMockModel(); +describe("modelSettings middleware support", () => { + it("should pass modelSettings from middleware to model.bindTools", async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const model = createMockModel() as any; const middleware = createMiddleware({ name: "testMiddleware", wrapModelRequest: async (request, handler) => { return handler({ ...request, - callOptions: { + modelSettings: { temperature: 0.5, max_tokens: 100, }, @@ -48,21 +49,22 @@ describe("callOptions middleware support", () => { messages: [new HumanMessage("Hello, world!")], }); - expect(model.invoke).toHaveBeenCalled(); - const callArgs = (model.invoke as unknown as MockInstance).mock.calls[0]; - const config = callArgs[1]; - expect(config).toHaveProperty("temperature", 0.5); - expect(config).toHaveProperty("max_tokens", 100); + expect(model.bindTools).toHaveBeenCalled(); + const callArgs = (model.bindTools as unknown as MockInstance).mock.calls[0]; + const options = callArgs[1]; + expect(options).toHaveProperty("temperature", 0.5); + expect(options).toHaveProperty("max_tokens", 100); }); - it("should pass headers from middleware callOptions to model.invoke", async () => { - const model = createMockModel(); + it("should pass headers from middleware modelSettings to model.bindTools", async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const model = createMockModel() as any; const middleware = createMiddleware({ name: "testMiddleware", wrapModelRequest: async (request, handler) => { return handler({ ...request, - callOptions: { + modelSettings: { headers: { "X-Custom-Header": "middleware-value", "X-Middleware-Only": "middleware-only", @@ -82,26 +84,26 @@ describe("callOptions middleware support", () => { messages: [new HumanMessage("Hello, world!")], }); - expect(model.invoke).toHaveBeenCalled(); - const callArgs = (model.invoke as unknown as MockInstance).mock.calls[0]; - const config = callArgs[1]; - expect(config.headers).toEqual({ + expect(model.bindTools).toHaveBeenCalled(); + const callArgs = (model.bindTools as unknown as MockInstance).mock.calls[0]; + const options = callArgs[1]; + expect(options.headers).toEqual({ "X-Custom-Header": "middleware-value", "X-Middleware-Only": "middleware-only", }); }); - it("should pass headers from middleware callOptions to model.invoke config", async () => { + it("should pass headers from middleware modelSettings to model.bindTools", async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const model = createMockModel() as any; - // Middleware that adds headers via callOptions + // Middleware that adds headers via modelSettings const middleware = createMiddleware({ name: "testMiddleware", wrapModelRequest: async (request, handler) => { return handler({ ...request, - callOptions: { + modelSettings: { headers: { "X-Middleware-Header": "from-middleware", "X-Custom-Header": "custom-value", @@ -121,25 +123,26 @@ describe("callOptions middleware support", () => { messages: [new HumanMessage("Hello, world!")], }); - // Verify model.invoke was called with headers in the config - expect(model.invoke).toHaveBeenCalled(); - const callArgs = (model.invoke as unknown as MockInstance).mock.calls[0]; - const config = callArgs[1]; + // Verify model.bindTools was called with headers in the options + expect(model.bindTools).toHaveBeenCalled(); + const callArgs = (model.bindTools as unknown as MockInstance).mock.calls[0]; + const options = callArgs[1]; - expect(config.headers).toEqual({ + expect(options.headers).toEqual({ "X-Middleware-Header": "from-middleware", "X-Custom-Header": "custom-value", }); }); - it("should handle callOptions without headers", async () => { - const model = createMockModel(); + it("should handle modelSettings without headers", async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const model = createMockModel() as any; const middleware = createMiddleware({ name: "testMiddleware", wrapModelRequest: async (request, handler) => { return handler({ ...request, - callOptions: { + modelSettings: { temperature: 0.7, }, }); @@ -156,21 +159,22 @@ describe("callOptions middleware support", () => { messages: [new HumanMessage("Hello, world!")], }); - expect(model.invoke).toHaveBeenCalled(); - const callArgs = (model.invoke as unknown as MockInstance).mock.calls[0]; - const config = callArgs[1]; - expect(config).toHaveProperty("temperature", 0.7); - expect(config.headers).toBeUndefined(); + expect(model.bindTools).toHaveBeenCalled(); + const callArgs = (model.bindTools as unknown as MockInstance).mock.calls[0]; + const options = callArgs[1]; + expect(options).toHaveProperty("temperature", 0.7); + expect(options.headers).toBeUndefined(); }); - it("should only add headers when either config or callOptions has headers", async () => { - const model = createMockModel(); + it("should only add headers when modelSettings has headers", async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const model = createMockModel() as any; const middleware = createMiddleware({ name: "testMiddleware", wrapModelRequest: async (request, handler) => { return handler({ ...request, - callOptions: { + modelSettings: { temperature: 0.8, }, }); @@ -187,20 +191,21 @@ describe("callOptions middleware support", () => { messages: [new HumanMessage("Hello, world!")], }); - expect(model.invoke).toHaveBeenCalled(); - const callArgs = (model.invoke as unknown as MockInstance).mock.calls[0]; - const config = callArgs[1]; - expect(config).not.toHaveProperty("headers"); + expect(model.bindTools).toHaveBeenCalled(); + const callArgs = (model.bindTools as unknown as MockInstance).mock.calls[0]; + const options = callArgs[1]; + expect(options).not.toHaveProperty("headers"); }); - it("should support multiple middleware with callOptions", async () => { - const model = createMockModel(); + it("should support multiple middleware with modelSettings", async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const model = createMockModel() as any; const middleware1 = createMiddleware({ name: "middleware1", wrapModelRequest: async (request, handler) => { return handler({ ...request, - callOptions: { + modelSettings: { temperature: 0.5, headers: { "X-Middleware-1": "value1", @@ -215,7 +220,7 @@ describe("callOptions middleware support", () => { wrapModelRequest: async (request, handler) => { return handler({ ...request, - callOptions: { + modelSettings: { max_tokens: 200, headers: { "X-Middleware-2": "value2", @@ -235,25 +240,26 @@ describe("callOptions middleware support", () => { messages: [new HumanMessage("Hello, world!")], }); - expect(model.invoke).toHaveBeenCalled(); - const callArgs = (model.invoke as unknown as MockInstance).mock.calls[0]; - const config = callArgs[1]; + expect(model.bindTools).toHaveBeenCalled(); + const callArgs = (model.bindTools as unknown as MockInstance).mock.calls[0]; + const options = callArgs[1]; // Last middleware wins for overlapping properties - expect(config).toHaveProperty("max_tokens", 200); - expect(config.headers).toEqual({ + expect(options).toHaveProperty("max_tokens", 200); + expect(options.headers).toEqual({ "X-Middleware-2": "value2", }); }); - it("should preserve signal from config when merging callOptions", async () => { - const model = createMockModel(); + it("should pass modelSettings to bindTools while preserving invoke signal", async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const model = createMockModel() as any; const abortController = new AbortController(); const middleware = createMiddleware({ name: "testMiddleware", wrapModelRequest: async (request, handler) => { return handler({ ...request, - callOptions: { + modelSettings: { temperature: 0.5, }, }); @@ -275,10 +281,16 @@ describe("callOptions middleware support", () => { } ); + // modelSettings goes to bindTools + expect(model.bindTools).toHaveBeenCalled(); + const bindCallArgs = (model.bindTools as unknown as MockInstance).mock.calls[0]; + const bindOptions = bindCallArgs[1]; + expect(bindOptions).toHaveProperty("temperature", 0.5); + + // signal still goes to invoke expect(model.invoke).toHaveBeenCalled(); - const callArgs = (model.invoke as unknown as MockInstance).mock.calls[0]; - const config = callArgs[1]; - expect(config).toHaveProperty("signal"); - expect(config).toHaveProperty("temperature", 0.5); + const invokeCallArgs = (model.invoke as unknown as MockInstance).mock.calls[0]; + const invokeConfig = invokeCallArgs[1]; + expect(invokeConfig).toHaveProperty("signal"); }); }); From 041bf6a0eded20b30a6b813a8c84afc6a0c18300 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 14 Oct 2025 12:15:04 +0100 Subject: [PATCH 3/4] Add more realistic test --- .../src/agents/tests/modelSettings.test.ts | 92 +++++++++++++++++-- 1 file changed, 82 insertions(+), 10 deletions(-) diff --git a/libs/langchain/src/agents/tests/modelSettings.test.ts b/libs/langchain/src/agents/tests/modelSettings.test.ts index eba7dc51cfc8..27d024ef1c21 100644 --- a/libs/langchain/src/agents/tests/modelSettings.test.ts +++ b/libs/langchain/src/agents/tests/modelSettings.test.ts @@ -3,6 +3,7 @@ import { HumanMessage, AIMessage } from "@langchain/core/messages"; import type { LanguageModelLike } from "@langchain/core/language_models/base"; import { createAgent, createMiddleware } from "../index.js"; +import { ChatAnthropic } from "@langchain/anthropic"; function createMockModel(name = "ChatAnthropic", model = "anthropic") { // Mock Anthropic model @@ -28,7 +29,7 @@ describe("modelSettings middleware support", () => { const model = createMockModel() as any; const middleware = createMiddleware({ name: "testMiddleware", - wrapModelRequest: async (request, handler) => { + wrapModelCall: async (request, handler) => { return handler({ ...request, modelSettings: { @@ -61,7 +62,7 @@ describe("modelSettings middleware support", () => { const model = createMockModel() as any; const middleware = createMiddleware({ name: "testMiddleware", - wrapModelRequest: async (request, handler) => { + wrapModelCall: async (request, handler) => { return handler({ ...request, modelSettings: { @@ -100,7 +101,7 @@ describe("modelSettings middleware support", () => { // Middleware that adds headers via modelSettings const middleware = createMiddleware({ name: "testMiddleware", - wrapModelRequest: async (request, handler) => { + wrapModelCall: async (request, handler) => { return handler({ ...request, modelSettings: { @@ -139,7 +140,7 @@ describe("modelSettings middleware support", () => { const model = createMockModel() as any; const middleware = createMiddleware({ name: "testMiddleware", - wrapModelRequest: async (request, handler) => { + wrapModelCall: async (request, handler) => { return handler({ ...request, modelSettings: { @@ -171,7 +172,7 @@ describe("modelSettings middleware support", () => { const model = createMockModel() as any; const middleware = createMiddleware({ name: "testMiddleware", - wrapModelRequest: async (request, handler) => { + wrapModelCall: async (request, handler) => { return handler({ ...request, modelSettings: { @@ -202,7 +203,7 @@ describe("modelSettings middleware support", () => { const model = createMockModel() as any; const middleware1 = createMiddleware({ name: "middleware1", - wrapModelRequest: async (request, handler) => { + wrapModelCall: async (request, handler) => { return handler({ ...request, modelSettings: { @@ -217,7 +218,7 @@ describe("modelSettings middleware support", () => { const middleware2 = createMiddleware({ name: "middleware2", - wrapModelRequest: async (request, handler) => { + wrapModelCall: async (request, handler) => { return handler({ ...request, modelSettings: { @@ -256,7 +257,7 @@ describe("modelSettings middleware support", () => { const abortController = new AbortController(); const middleware = createMiddleware({ name: "testMiddleware", - wrapModelRequest: async (request, handler) => { + wrapModelCall: async (request, handler) => { return handler({ ...request, modelSettings: { @@ -283,14 +284,85 @@ describe("modelSettings middleware support", () => { // modelSettings goes to bindTools expect(model.bindTools).toHaveBeenCalled(); - const bindCallArgs = (model.bindTools as unknown as MockInstance).mock.calls[0]; + const bindCallArgs = (model.bindTools as unknown as MockInstance).mock + .calls[0]; const bindOptions = bindCallArgs[1]; expect(bindOptions).toHaveProperty("temperature", 0.5); // signal still goes to invoke expect(model.invoke).toHaveBeenCalled(); - const invokeCallArgs = (model.invoke as unknown as MockInstance).mock.calls[0]; + const invokeCallArgs = (model.invoke as unknown as MockInstance).mock + .calls[0]; const invokeConfig = invokeCallArgs[1]; expect(invokeConfig).toHaveProperty("signal"); }); + + it("should pass modelSettings to real Anthropic model via bindTools", async () => { + // Mock the Anthropic client + const mockCreate = vi.fn().mockResolvedValue({ + id: "msg_123", + type: "message", + role: "assistant", + content: [{ type: "text", text: "Response from model" }], + model: "claude-sonnet-4-20250514", + stop_reason: "end_turn", + usage: { input_tokens: 10, output_tokens: 20 }, + }); + + const mockClient = { + messages: { + create: mockCreate, + }, + }; + + // Create real ChatAnthropic with mocked client + const model = new ChatAnthropic({ + model: "claude-sonnet-4-20250514", + temperature: 0, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createClient: () => mockClient as any, + }); + + const middleware = createMiddleware({ + name: "testMiddleware", + wrapModelCall: async (request, handler) => { + return handler({ + ...request, + modelSettings: { + temperature: 0.5, + headers: { + "anthropic-beta": + "code-execution-2025-08-25,files-api-2025-04-14", + }, + container: "container_abc123", + }, + }); + }, + }); + + const agent = createAgent({ + model, + tools: [], + middleware: [middleware] as const, + }); + + await agent.invoke({ + messages: [new HumanMessage("Hello, world!")], + }); + + // Verify the client was called + expect(mockCreate).toHaveBeenCalled(); + + // Check the actual parameters passed to the Anthropic client + const clientCallArgs = mockCreate.mock.calls[0][0]; + expect(clientCallArgs).toHaveProperty("container", "container_abc123"); + + // Check that headers were passed via options (second parameter) + const clientOptions = mockCreate.mock.calls[0][1]; + expect(clientOptions).toHaveProperty("headers"); + expect(clientOptions.headers).toHaveProperty( + "anthropic-beta", + "code-execution-2025-08-25,files-api-2025-04-14" + ); + }); }); From 705f4a141235b5a1a92d70aeefe5e8da1331b87e Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 14 Oct 2025 12:19:34 +0100 Subject: [PATCH 4/4] Remove unnecessary tests --- .../src/agents/tests/modelSettings.test.ts | 300 +----------------- 1 file changed, 3 insertions(+), 297 deletions(-) diff --git a/libs/langchain/src/agents/tests/modelSettings.test.ts b/libs/langchain/src/agents/tests/modelSettings.test.ts index 27d024ef1c21..67879871b180 100644 --- a/libs/langchain/src/agents/tests/modelSettings.test.ts +++ b/libs/langchain/src/agents/tests/modelSettings.test.ts @@ -1,302 +1,9 @@ -import { expect, describe, it, vi, type MockInstance } from "vitest"; -import { HumanMessage, AIMessage } from "@langchain/core/messages"; -import type { LanguageModelLike } from "@langchain/core/language_models/base"; - -import { createAgent, createMiddleware } from "../index.js"; import { ChatAnthropic } from "@langchain/anthropic"; - -function createMockModel(name = "ChatAnthropic", model = "anthropic") { - // Mock Anthropic model - const invokeCallback = vi - .fn() - .mockResolvedValue(new AIMessage("Response from model")); - return { - getName: () => name, - bindTools: vi.fn().mockReturnThis(), - _streamResponseChunks: vi.fn().mockReturnThis(), - bind: vi.fn().mockReturnThis(), - invoke: invokeCallback, - lc_runnable: true, - _modelType: model, - _generate: vi.fn(), - _llmType: () => model, - } as unknown as LanguageModelLike; -} +import { HumanMessage } from "@langchain/core/messages"; +import { describe, expect, it, vi } from "vitest"; +import { createAgent, createMiddleware } from "../index.js"; describe("modelSettings middleware support", () => { - it("should pass modelSettings from middleware to model.bindTools", async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const model = createMockModel() as any; - const middleware = createMiddleware({ - name: "testMiddleware", - wrapModelCall: async (request, handler) => { - return handler({ - ...request, - modelSettings: { - temperature: 0.5, - max_tokens: 100, - }, - }); - }, - }); - - const agent = createAgent({ - model, - tools: [], - middleware: [middleware] as const, - }); - - await agent.invoke({ - messages: [new HumanMessage("Hello, world!")], - }); - - expect(model.bindTools).toHaveBeenCalled(); - const callArgs = (model.bindTools as unknown as MockInstance).mock.calls[0]; - const options = callArgs[1]; - expect(options).toHaveProperty("temperature", 0.5); - expect(options).toHaveProperty("max_tokens", 100); - }); - - it("should pass headers from middleware modelSettings to model.bindTools", async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const model = createMockModel() as any; - const middleware = createMiddleware({ - name: "testMiddleware", - wrapModelCall: async (request, handler) => { - return handler({ - ...request, - modelSettings: { - headers: { - "X-Custom-Header": "middleware-value", - "X-Middleware-Only": "middleware-only", - }, - }, - }); - }, - }); - - const agent = createAgent({ - model, - tools: [], - middleware: [middleware] as const, - }); - - await agent.invoke({ - messages: [new HumanMessage("Hello, world!")], - }); - - expect(model.bindTools).toHaveBeenCalled(); - const callArgs = (model.bindTools as unknown as MockInstance).mock.calls[0]; - const options = callArgs[1]; - expect(options.headers).toEqual({ - "X-Custom-Header": "middleware-value", - "X-Middleware-Only": "middleware-only", - }); - }); - - it("should pass headers from middleware modelSettings to model.bindTools", async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const model = createMockModel() as any; - - // Middleware that adds headers via modelSettings - const middleware = createMiddleware({ - name: "testMiddleware", - wrapModelCall: async (request, handler) => { - return handler({ - ...request, - modelSettings: { - headers: { - "X-Middleware-Header": "from-middleware", - "X-Custom-Header": "custom-value", - }, - }, - }); - }, - }); - - const agent = createAgent({ - model, - tools: [], - middleware: [middleware] as const, - }); - - await agent.invoke({ - messages: [new HumanMessage("Hello, world!")], - }); - - // Verify model.bindTools was called with headers in the options - expect(model.bindTools).toHaveBeenCalled(); - const callArgs = (model.bindTools as unknown as MockInstance).mock.calls[0]; - const options = callArgs[1]; - - expect(options.headers).toEqual({ - "X-Middleware-Header": "from-middleware", - "X-Custom-Header": "custom-value", - }); - }); - - it("should handle modelSettings without headers", async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const model = createMockModel() as any; - const middleware = createMiddleware({ - name: "testMiddleware", - wrapModelCall: async (request, handler) => { - return handler({ - ...request, - modelSettings: { - temperature: 0.7, - }, - }); - }, - }); - - const agent = createAgent({ - model, - tools: [], - middleware: [middleware] as const, - }); - - await agent.invoke({ - messages: [new HumanMessage("Hello, world!")], - }); - - expect(model.bindTools).toHaveBeenCalled(); - const callArgs = (model.bindTools as unknown as MockInstance).mock.calls[0]; - const options = callArgs[1]; - expect(options).toHaveProperty("temperature", 0.7); - expect(options.headers).toBeUndefined(); - }); - - it("should only add headers when modelSettings has headers", async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const model = createMockModel() as any; - const middleware = createMiddleware({ - name: "testMiddleware", - wrapModelCall: async (request, handler) => { - return handler({ - ...request, - modelSettings: { - temperature: 0.8, - }, - }); - }, - }); - - const agent = createAgent({ - model, - tools: [], - middleware: [middleware] as const, - }); - - await agent.invoke({ - messages: [new HumanMessage("Hello, world!")], - }); - - expect(model.bindTools).toHaveBeenCalled(); - const callArgs = (model.bindTools as unknown as MockInstance).mock.calls[0]; - const options = callArgs[1]; - expect(options).not.toHaveProperty("headers"); - }); - - it("should support multiple middleware with modelSettings", async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const model = createMockModel() as any; - const middleware1 = createMiddleware({ - name: "middleware1", - wrapModelCall: async (request, handler) => { - return handler({ - ...request, - modelSettings: { - temperature: 0.5, - headers: { - "X-Middleware-1": "value1", - }, - }, - }); - }, - }); - - const middleware2 = createMiddleware({ - name: "middleware2", - wrapModelCall: async (request, handler) => { - return handler({ - ...request, - modelSettings: { - max_tokens: 200, - headers: { - "X-Middleware-2": "value2", - }, - }, - }); - }, - }); - - const agent = createAgent({ - model, - tools: [], - middleware: [middleware1, middleware2] as const, - }); - - await agent.invoke({ - messages: [new HumanMessage("Hello, world!")], - }); - - expect(model.bindTools).toHaveBeenCalled(); - const callArgs = (model.bindTools as unknown as MockInstance).mock.calls[0]; - const options = callArgs[1]; - // Last middleware wins for overlapping properties - expect(options).toHaveProperty("max_tokens", 200); - expect(options.headers).toEqual({ - "X-Middleware-2": "value2", - }); - }); - - it("should pass modelSettings to bindTools while preserving invoke signal", async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const model = createMockModel() as any; - const abortController = new AbortController(); - const middleware = createMiddleware({ - name: "testMiddleware", - wrapModelCall: async (request, handler) => { - return handler({ - ...request, - modelSettings: { - temperature: 0.5, - }, - }); - }, - }); - - const agent = createAgent({ - model, - tools: [], - middleware: [middleware] as const, - }); - - await agent.invoke( - { - messages: [new HumanMessage("Hello, world!")], - }, - { - signal: abortController.signal, - } - ); - - // modelSettings goes to bindTools - expect(model.bindTools).toHaveBeenCalled(); - const bindCallArgs = (model.bindTools as unknown as MockInstance).mock - .calls[0]; - const bindOptions = bindCallArgs[1]; - expect(bindOptions).toHaveProperty("temperature", 0.5); - - // signal still goes to invoke - expect(model.invoke).toHaveBeenCalled(); - const invokeCallArgs = (model.invoke as unknown as MockInstance).mock - .calls[0]; - const invokeConfig = invokeCallArgs[1]; - expect(invokeConfig).toHaveProperty("signal"); - }); - it("should pass modelSettings to real Anthropic model via bindTools", async () => { // Mock the Anthropic client const mockCreate = vi.fn().mockResolvedValue({ @@ -329,7 +36,6 @@ describe("modelSettings middleware support", () => { return handler({ ...request, modelSettings: { - temperature: 0.5, headers: { "anthropic-beta": "code-execution-2025-08-25,files-api-2025-04-14",