langchain
Version:
Typescript bindings for langchain
1 lines • 6.33 kB
Source Map (JSON)
{"version":3,"file":"modelRouter.cjs","names":["initChatModel","createMiddleware","z","omitPayload","HumanMessage"],"sources":["../../../src/agents/middleware/modelRouter.ts"],"sourcesContent":["import type {\n BaseDecisionModel,\n ChoiceAnswer,\n Content,\n JsonValue,\n Question,\n} from \"@langchain/core/decisions\";\nimport { HumanMessage } from \"@langchain/core/messages\";\nimport { omitPayload } from \"@langchain/core/tracers/tracer_langchain\";\nimport { z } from \"zod/v4\";\n\nimport { initChatModel } from \"../../chat_models/universal.js\";\nimport { createMiddleware } from \"../middleware.js\";\nimport type { AgentLanguageModelLike } from \"../model.js\";\n\nconst QUESTION_ID = \"model_route\";\n\n/** A model available to the router and the criterion for selecting it. */\nexport interface ModelChoice {\n /** A chat model, or a model string accepted by `initChatModel`. */\n model: string | AgentLanguageModelLike;\n /** Description of the tasks suited to the model. */\n criteria: JsonValue;\n}\n\nexport interface ModelRouterMiddlewareConfig {\n /**\n * Named model choices, each containing a model and the criterion for\n * selecting it.\n */\n choices: Record<string, ModelChoice>;\n /** Instructions the classifier should follow when selecting a route. */\n instructions: Content;\n /** Decision model used to select a route. */\n classifier: BaseDecisionModel;\n}\n\n/**\n * Select an agent's model with a decision model's `Choice` answer (beta).\n *\n * The middleware classifies the latest human message once before an agent\n * run, stores the complete `ChoiceAnswer` in agent state as `modelRoute`, and\n * uses its selected label for every model call in the run. Keeping the\n * complete answer makes probabilities and confidence available in state and\n * traces. Classifier failures propagate and terminate the run rather than\n * silently selecting a different model.\n *\n * @example\n * ```ts\n * import { createAgent, modelRouterMiddleware } from \"langchain\";\n *\n * const router = modelRouterMiddleware({\n * choices: {\n * fast: {\n * model: \"openai:gpt-5-mini\",\n * criteria: \"Simple, well-scoped tasks.\",\n * },\n * powerful: {\n * model: powerfulModel,\n * criteria: \"Complex tasks requiring deeper reasoning.\",\n * },\n * },\n * instructions: \"Choose the least costly model suited to the task.\",\n * classifier,\n * });\n * const agent = createAgent({\n * model: \"openai:gpt-5-mini\",\n * middleware: [router],\n * });\n * ```\n *\n * @throws Error if `choices` is empty.\n */\nexport function modelRouterMiddleware(config: ModelRouterMiddlewareConfig) {\n const { choices, instructions, classifier } = config;\n const routes = Object.keys(choices);\n if (routes.length === 0) {\n throw new Error(\"modelRouterMiddleware requires at least one choice.\");\n }\n const questions: Record<string, Question> = {\n [QUESTION_ID]: {\n type: \"choice\",\n instructions,\n criteria: Object.fromEntries(\n routes.map((route) => [route, choices[route].criteria])\n ),\n },\n };\n\n // Caches the promise so concurrent runs share one `initChatModel` call.\n const models = new Map<string, Promise<AgentLanguageModelLike>>();\n const modelFor = (route: string) => {\n let pending = models.get(route);\n if (pending === undefined) {\n const { model } = choices[route];\n pending = Promise.resolve(\n typeof model === \"string\" ? initChatModel(model) : model\n );\n pending.catch(() => models.delete(route));\n models.set(route, pending);\n }\n return pending;\n };\n\n return createMiddleware({\n name: \"ModelRouterMiddleware\",\n stateSchema: z.object({ modelRoute: z.custom<ChoiceAnswer>().optional() }),\n tracePolicy: { processInputs: omitPayload },\n async beforeAgent(state, runtime) {\n const message = [...state.messages]\n .reverse()\n .find((candidate) => HumanMessage.isInstance(candidate));\n if (message === undefined) {\n throw new Error(\n \"modelRouterMiddleware requires a HumanMessage in the agent state to route on.\"\n );\n }\n const response = await classifier.invoke(\n { state: message, questions },\n { signal: runtime.signal }\n );\n const answer = response.choices[QUESTION_ID];\n if (!Object.hasOwn(choices, answer.choice)) {\n throw new Error(\n `modelRouterMiddleware: the classifier selected unknown route \"${answer.choice}\".`\n );\n }\n return { modelRoute: answer };\n },\n async wrapModelCall(request, handler) {\n const route = request.state.modelRoute;\n if (route === undefined) {\n throw new Error(\n \"modelRouterMiddleware found no routing answer in state.\"\n );\n }\n return handler({ ...request, model: await modelFor(route.choice) });\n },\n });\n}\n"],"mappings":";;;;;;AAeA,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0DpB,SAAgB,sBAAsB,QAAqC;CACzE,MAAM,EAAE,SAAS,cAAc,eAAe;CAC9C,MAAM,SAAS,OAAO,KAAK,OAAO;CAClC,IAAI,OAAO,WAAW,GACpB,MAAM,IAAI,MAAM,qDAAqD;CAEvE,MAAM,YAAsC,GACzC,cAAc;EACb,MAAM;EACN;EACA,UAAU,OAAO,YACf,OAAO,KAAK,UAAU,CAAC,OAAO,QAAQ,MAAM,CAAC,QAAQ,CAAC,CACxD;CACF,EACF;CAGA,MAAM,yBAAS,IAAI,IAA6C;CAChE,MAAM,YAAY,UAAkB;EAClC,IAAI,UAAU,OAAO,IAAI,KAAK;EAC9B,IAAI,YAAY,KAAA,GAAW;GACzB,MAAM,EAAE,UAAU,QAAQ;GAC1B,UAAU,QAAQ,QAChB,OAAO,UAAU,WAAWA,8BAAAA,cAAc,KAAK,IAAI,KACrD;GACA,QAAQ,YAAY,OAAO,OAAO,KAAK,CAAC;GACxC,OAAO,IAAI,OAAO,OAAO;EAC3B;EACA,OAAO;CACT;CAEA,OAAOC,mBAAAA,iBAAiB;EACtB,MAAM;EACN,aAAaC,OAAAA,EAAE,OAAO,EAAE,YAAYA,OAAAA,EAAE,OAAqB,CAAC,CAAC,SAAS,EAAE,CAAC;EACzE,aAAa,EAAE,eAAeC,yCAAAA,YAAY;EAC1C,MAAM,YAAY,OAAO,SAAS;GAChC,MAAM,UAAU,CAAC,GAAG,MAAM,QAAQ,CAAC,CAChC,QAAQ,CAAC,CACT,MAAM,cAAcC,yBAAAA,aAAa,WAAW,SAAS,CAAC;GACzD,IAAI,YAAY,KAAA,GACd,MAAM,IAAI,MACR,+EACF;GAMF,MAAM,UAAS,MAJQ,WAAW,OAChC;IAAE,OAAO;IAAS;GAAU,GAC5B,EAAE,QAAQ,QAAQ,OAAO,CAC3B,EAAA,CACwB,QAAQ;GAChC,IAAI,CAAC,OAAO,OAAO,SAAS,OAAO,MAAM,GACvC,MAAM,IAAI,MACR,iEAAiE,OAAO,OAAO,GACjF;GAEF,OAAO,EAAE,YAAY,OAAO;EAC9B;EACA,MAAM,cAAc,SAAS,SAAS;GACpC,MAAM,QAAQ,QAAQ,MAAM;GAC5B,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MACR,yDACF;GAEF,OAAO,QAAQ;IAAE,GAAG;IAAS,OAAO,MAAM,SAAS,MAAM,MAAM;GAAE,CAAC;EACpE;CACF,CAAC;AACH"}