From 66448041e51b50121f04588ef35ce304fdbcc1ab Mon Sep 17 00:00:00 2001 From: Varun S G Date: Sat, 5 Sep 2026 01:13:10 +0530 Subject: [PATCH 1/3] fix(agent): populate primary model from models fallback array (#121) --- .changeset/server-tools-models-fallback.md | 5 + packages/agent/src/inner-loop/call-model.ts | 12 ++ packages/agent/src/lib/async-params.ts | 13 +- packages/agent/src/lib/model-result.ts | 8 + .../agent/src/lib/next-turn-params.test.ts | 74 ++++++- packages/agent/src/lib/next-turn-params.ts | 22 +- .../unit/server-tool-models-fallback.test.ts | 197 ++++++++++++++++++ 7 files changed, 326 insertions(+), 5 deletions(-) create mode 100644 .changeset/server-tools-models-fallback.md create mode 100644 packages/agent/tests/unit/server-tool-models-fallback.test.ts diff --git a/.changeset/server-tools-models-fallback.md b/.changeset/server-tools-models-fallback.md new file mode 100644 index 00000000..e1cdf83c --- /dev/null +++ b/.changeset/server-tools-models-fallback.md @@ -0,0 +1,5 @@ +--- +'@openrouter/agent': patch +--- + +Populate primary model from models fallback array when model is omitted (#121) diff --git a/packages/agent/src/inner-loop/call-model.ts b/packages/agent/src/inner-loop/call-model.ts index 73d2d8ef..93fa9542 100644 --- a/packages/agent/src/inner-loop/call-model.ts +++ b/packages/agent/src/inner-loop/call-model.ts @@ -158,6 +158,18 @@ export function callModel< }; stripToolSetSnapshotMetadata(finalRequest); + // If model is not set but models is provided, default model to models[0]. + // OpenRouter server tools and backend handlers require the primary model + // to be populated on the request. + if ( + finalRequest['model'] === undefined && + Array.isArray(finalRequest['models']) && + finalRequest['models'].length > 0 && + typeof finalRequest['models'][0] === 'string' + ) { + finalRequest['model'] = finalRequest['models'][0]; + } + if (apiTools !== undefined) { finalRequest['tools'] = apiTools; } diff --git a/packages/agent/src/lib/async-params.ts b/packages/agent/src/lib/async-params.ts index f2f72e71..44e7f736 100644 --- a/packages/agent/src/lib/async-params.ts +++ b/packages/agent/src/lib/async-params.ts @@ -391,7 +391,18 @@ export async function resolveAsyncFunctions 0 && + typeof resolvedObj['models'][0] === 'string' + ) { + (resolvedObj as Record)['model'] = resolvedObj['models'][0]; + } + + return resolvedObj; } /** diff --git a/packages/agent/src/lib/model-result.ts b/packages/agent/src/lib/model-result.ts index bbc4f047..535a8fa9 100644 --- a/packages/agent/src/lib/model-result.ts +++ b/packages/agent/src/lib/model-result.ts @@ -5344,6 +5344,14 @@ export class ModelResult< ...rest, }; stripToolSetSnapshotMetadata(resolved); + if ( + resolved['model'] === undefined && + Array.isArray(resolved['models']) && + resolved['models'].length > 0 && + typeof resolved['models'][0] === 'string' + ) { + resolved['model'] = resolved['models'][0]; + } return this.applyResolvedForcedToolChoicePolicy(resolved as ResolvedCallModelInput); } diff --git a/packages/agent/src/lib/next-turn-params.test.ts b/packages/agent/src/lib/next-turn-params.test.ts index 80a57b04..a22550f6 100644 --- a/packages/agent/src/lib/next-turn-params.test.ts +++ b/packages/agent/src/lib/next-turn-params.test.ts @@ -1,7 +1,7 @@ import type * as models from '@openrouter/sdk/models'; import { describe, expect, it } from 'vitest'; -import { applyNextTurnParamsToRequest } from './next-turn-params.js'; +import { applyNextTurnParamsToRequest, buildNextTurnParamsContext } from './next-turn-params.js'; /** * Creates a minimal ResponsesRequest for testing applyNextTurnParamsToRequest. @@ -198,3 +198,75 @@ describe('applyNextTurnParamsToRequest with allowed_tools', () => { ); }); }); + +describe('buildNextTurnParamsContext with models fallback', () => { + it('defaults model to models[0] when model is undefined', () => { + const context = buildNextTurnParamsContext({ + models: [ + 'mistralai/mistral-small-2603', + 'mistralai/mistral-large-2512', + ], + input: 'hello', + }); + + expect(context.model).toBe('mistralai/mistral-small-2603'); + expect(context.models).toEqual([ + 'mistralai/mistral-small-2603', + 'mistralai/mistral-large-2512', + ]); + }); + + it('preserves explicit model when provided', () => { + const context = buildNextTurnParamsContext({ + model: 'anthropic/claude-3', + models: [ + 'mistralai/mistral-large-2512', + ], + input: 'hello', + }); + + expect(context.model).toBe('anthropic/claude-3'); + expect(context.models).toEqual([ + 'mistralai/mistral-large-2512', + ]); + }); +}); + +describe('applyNextTurnParamsToRequest with models fallback', () => { + it('updates model to models[0] when models is updated without model', () => { + const request = createBaseRequest({ + model: 'openai/gpt-4', + }); + + const result = applyNextTurnParamsToRequest(request, { + models: [ + 'mistralai/mistral-small-2603', + 'mistralai/mistral-large-2512', + ], + }); + + expect(result.model).toBe('mistralai/mistral-small-2603'); + expect(result.models).toEqual([ + 'mistralai/mistral-small-2603', + 'mistralai/mistral-large-2512', + ]); + }); + + it('preserves explicit model when both model and models are updated', () => { + const request = createBaseRequest({ + model: 'openai/gpt-4', + }); + + const result = applyNextTurnParamsToRequest(request, { + model: 'anthropic/claude-3', + models: [ + 'mistralai/mistral-large-2512', + ], + }); + + expect(result.model).toBe('anthropic/claude-3'); + expect(result.models).toEqual([ + 'mistralai/mistral-large-2512', + ]); + }); +}); diff --git a/packages/agent/src/lib/next-turn-params.ts b/packages/agent/src/lib/next-turn-params.ts index 7ba0807b..2fdc66d1 100644 --- a/packages/agent/src/lib/next-turn-params.ts +++ b/packages/agent/src/lib/next-turn-params.ts @@ -22,8 +22,14 @@ export function buildNextTurnParamsContext( return { input: request.input ?? [], toolChoice: request.toolChoice, - model: request.model ?? '', - models: request.models ?? [], + model: request.model ?? request.models?.[0] ?? '', + models: + request.models ?? + (request.model + ? [ + request.model, + ] + : []), temperature: request.temperature ?? null, maxOutputTokens: request.maxOutputTokens ?? null, topP: request.topP ?? null, @@ -181,8 +187,18 @@ export function applyNextTurnParamsToRequest( for (const [key, value] of Object.entries(computedParams)) { sanitized[key] = value === null ? undefined : value; } - return { + const nextRequest: models.ResponsesRequest = { ...request, ...sanitized, }; + if ( + (nextRequest.model === undefined || + ('models' in computedParams && !('model' in computedParams))) && + Array.isArray(nextRequest.models) && + nextRequest.models.length > 0 && + typeof nextRequest.models[0] === 'string' + ) { + nextRequest.model = nextRequest.models[0]; + } + return nextRequest; } diff --git a/packages/agent/tests/unit/server-tool-models-fallback.test.ts b/packages/agent/tests/unit/server-tool-models-fallback.test.ts new file mode 100644 index 00000000..8acd7907 --- /dev/null +++ b/packages/agent/tests/unit/server-tool-models-fallback.test.ts @@ -0,0 +1,197 @@ +import type { OpenRouterCore } from '@openrouter/sdk/core'; +import type * as models from '@openrouter/sdk/models'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { callModel } from '../../src/inner-loop/call-model.js'; +import { serverTool } from '../../src/lib/tool.js'; + +const mockBetaResponsesSend = vi.hoisted(() => vi.fn()); + +vi.mock('@openrouter/sdk/funcs/betaResponsesSend', () => ({ + betaResponsesSend: mockBetaResponsesSend, +})); + +function makeResponse( + id: string, + model: string, + output: models.OpenResponsesResult['output'], +): models.OpenResponsesResult { + return { + id, + object: 'response', + createdAt: 0, + model, + status: 'completed', + completedAt: 0, + output, + error: null, + incompleteDetails: null, + temperature: null, + topP: null, + presencePenalty: null, + frequencyPenalty: null, + metadata: null, + instructions: null, + tools: [], + toolChoice: 'auto', + parallelToolCalls: false, + } as models.OpenResponsesResult; +} + +const client = { + _options: {}, +} as OpenRouterCore; + +describe('Issue #121: server tools with model fallback via models', () => { + beforeEach(() => { + mockBetaResponsesSend.mockReset(); + }); + + it('populates model from models[0] when model is omitted (#121)', async () => { + mockBetaResponsesSend.mockResolvedValueOnce({ + ok: true, + value: makeResponse('resp_1', 'mistralai/mistral-small-2603', [ + { + id: 'item_1', + type: 'openrouter:datetime', + status: 'completed', + additionalProperties: { + datetime: '2026-09-05T00:00:00Z', + }, + } as models.OutputServerToolItem, + { + id: 'msg_1', + type: 'message', + role: 'assistant', + status: 'completed', + content: [ + { + type: 'output_text', + text: 'Today is Saturday.', + annotations: [], + }, + ], + }, + ]), + }); + + const result = callModel(client, { + models: [ + 'mistralai/mistral-small-2603', + 'mistralai/mistral-large-2512', + ], + input: 'What day is it?', + tools: [ + serverTool({ + type: 'openrouter:datetime', + }), + ], + }); + + const text = await result.getText(); + expect(text).toBe('Today is Saturday.'); + + // Verify betaResponsesSend was called with model set to models[0] + expect(mockBetaResponsesSend).toHaveBeenCalledTimes(1); + const sentRequest = mockBetaResponsesSend.mock.calls[0][1].responsesRequest; + expect(sentRequest.model).toBe('mistralai/mistral-small-2603'); + expect(sentRequest.models).toEqual([ + 'mistralai/mistral-small-2603', + 'mistralai/mistral-large-2512', + ]); + expect(sentRequest.tools).toEqual([ + { + type: 'openrouter:datetime', + }, + ]); + }); + + it('preserves explicit model when provided alongside models', async () => { + mockBetaResponsesSend.mockResolvedValueOnce({ + ok: true, + value: makeResponse('resp_2', 'primary-model', [ + { + id: 'msg_2', + type: 'message', + role: 'assistant', + status: 'completed', + content: [ + { + type: 'output_text', + text: 'Hello', + annotations: [], + }, + ], + }, + ]), + }); + + const result = callModel(client, { + model: 'primary-model', + models: [ + 'fallback-model-1', + 'fallback-model-2', + ], + input: 'Hello', + tools: [ + serverTool({ + type: 'openrouter:datetime', + }), + ], + }); + + await result.getText(); + + expect(mockBetaResponsesSend).toHaveBeenCalledTimes(1); + const sentRequest = mockBetaResponsesSend.mock.calls[0][1].responsesRequest; + expect(sentRequest.model).toBe('primary-model'); + expect(sentRequest.models).toEqual([ + 'fallback-model-1', + 'fallback-model-2', + ]); + }); + + it('populates model from models[0] when models is an async function', async () => { + mockBetaResponsesSend.mockResolvedValueOnce({ + ok: true, + value: makeResponse('resp_3', 'dynamic-model-1', [ + { + id: 'msg_3', + type: 'message', + role: 'assistant', + status: 'completed', + content: [ + { + type: 'output_text', + text: 'Dynamic model response', + annotations: [], + }, + ], + }, + ]), + }); + + const result = callModel(client, { + models: async () => [ + 'dynamic-model-1', + 'dynamic-model-2', + ], + input: 'Test', + tools: [ + serverTool({ + type: 'openrouter:datetime', + }), + ], + }); + + const text = await result.getText(); + expect(text).toBe('Dynamic model response'); + + expect(mockBetaResponsesSend).toHaveBeenCalledTimes(1); + const sentRequest = mockBetaResponsesSend.mock.calls[0][1].responsesRequest; + expect(sentRequest.model).toBe('dynamic-model-1'); + expect(sentRequest.models).toEqual([ + 'dynamic-model-1', + 'dynamic-model-2', + ]); + }); +}); From 4f6ab0d7f10918ea322da2add4f2a9ff5c91eda9 Mon Sep 17 00:00:00 2001 From: Luke Parke <5702154+LukasParke@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:03:54 +0000 Subject: [PATCH 2/3] refactor(agent): consolidate models[0] fallback into one helper - Add fillPrimaryModelFromModels (src/lib/primary-model.ts) and call it once in ModelResult.resolveRequestForContext for both the static and async-function paths, plus in applyNextTurnParamsToRequest. - Drop the duplicate blocks in call-model.ts (ModelResult already resolves it) and async-params.ts (keeps the public resolveAsyncFunctions unchanged). - Revert buildNextTurnParamsContext reporting models as [model] when the caller never set models; add a test pinning that. - Document that applyNextTurnParamsToRequest resets an explicit model to models[0] when only models changes. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/agent/src/inner-loop/call-model.ts | 12 -------- packages/agent/src/lib/async-params.ts | 13 +-------- packages/agent/src/lib/model-result.ts | 11 ++----- .../agent/src/lib/next-turn-params.test.ts | 10 +++++++ packages/agent/src/lib/next-turn-params.ts | 27 +++++++---------- packages/agent/src/lib/primary-model.ts | 29 +++++++++++++++++++ 6 files changed, 54 insertions(+), 48 deletions(-) create mode 100644 packages/agent/src/lib/primary-model.ts diff --git a/packages/agent/src/inner-loop/call-model.ts b/packages/agent/src/inner-loop/call-model.ts index 93fa9542..73d2d8ef 100644 --- a/packages/agent/src/inner-loop/call-model.ts +++ b/packages/agent/src/inner-loop/call-model.ts @@ -158,18 +158,6 @@ export function callModel< }; stripToolSetSnapshotMetadata(finalRequest); - // If model is not set but models is provided, default model to models[0]. - // OpenRouter server tools and backend handlers require the primary model - // to be populated on the request. - if ( - finalRequest['model'] === undefined && - Array.isArray(finalRequest['models']) && - finalRequest['models'].length > 0 && - typeof finalRequest['models'][0] === 'string' - ) { - finalRequest['model'] = finalRequest['models'][0]; - } - if (apiTools !== undefined) { finalRequest['tools'] = apiTools; } diff --git a/packages/agent/src/lib/async-params.ts b/packages/agent/src/lib/async-params.ts index 44e7f736..f2f72e71 100644 --- a/packages/agent/src/lib/async-params.ts +++ b/packages/agent/src/lib/async-params.ts @@ -391,18 +391,7 @@ export async function resolveAsyncFunctions 0 && - typeof resolvedObj['models'][0] === 'string' - ) { - (resolvedObj as Record)['model'] = resolvedObj['models'][0]; - } - - return resolvedObj; + return buildResolvedRequest(resolvedEntries); } /** diff --git a/packages/agent/src/lib/model-result.ts b/packages/agent/src/lib/model-result.ts index 535a8fa9..eb793f4d 100644 --- a/packages/agent/src/lib/model-result.ts +++ b/packages/agent/src/lib/model-result.ts @@ -59,6 +59,7 @@ import { applyNextTurnParamsToRequest, executeNextTurnParamsFunctions, } from './next-turn-params.js'; +import { fillPrimaryModelFromModels } from './primary-model.js'; import type { StreamReplay } from './reusable-stream.js'; import { ReusableReadableStream } from './reusable-stream.js'; import { isStopConditionMet } from './stop-conditions.js'; @@ -5312,6 +5313,7 @@ export class ModelResult< private async resolveRequestForContext(context: TurnContext): Promise { if (hasAsyncFunctions(this.options.request)) { const resolved = await resolveAsyncFunctions(this.options.request, context); + fillPrimaryModelFromModels(resolved); return this.applyResolvedForcedToolChoicePolicy(resolved); } // Already resolved, extract non-function fields. @@ -5344,14 +5346,7 @@ export class ModelResult< ...rest, }; stripToolSetSnapshotMetadata(resolved); - if ( - resolved['model'] === undefined && - Array.isArray(resolved['models']) && - resolved['models'].length > 0 && - typeof resolved['models'][0] === 'string' - ) { - resolved['model'] = resolved['models'][0]; - } + fillPrimaryModelFromModels(resolved); return this.applyResolvedForcedToolChoicePolicy(resolved as ResolvedCallModelInput); } diff --git a/packages/agent/src/lib/next-turn-params.test.ts b/packages/agent/src/lib/next-turn-params.test.ts index a22550f6..c89fcdb8 100644 --- a/packages/agent/src/lib/next-turn-params.test.ts +++ b/packages/agent/src/lib/next-turn-params.test.ts @@ -230,6 +230,16 @@ describe('buildNextTurnParamsContext with models fallback', () => { 'mistralai/mistral-large-2512', ]); }); + + it('does not synthesize models from model when models was never set', () => { + const context = buildNextTurnParamsContext({ + model: 'anthropic/claude-3', + input: 'hello', + }); + + expect(context.model).toBe('anthropic/claude-3'); + expect(context.models).toEqual([]); + }); }); describe('applyNextTurnParamsToRequest with models fallback', () => { diff --git a/packages/agent/src/lib/next-turn-params.ts b/packages/agent/src/lib/next-turn-params.ts index 2fdc66d1..b360eb86 100644 --- a/packages/agent/src/lib/next-turn-params.ts +++ b/packages/agent/src/lib/next-turn-params.ts @@ -1,4 +1,5 @@ import type * as models from '@openrouter/sdk/models'; +import { fillPrimaryModelFromModels } from './primary-model.js'; import type { NextTurnParamsContext, ParsedToolCall, Tool } from './tool-types.js'; import { isClientTool } from './tool-types.js'; @@ -23,13 +24,7 @@ export function buildNextTurnParamsContext( input: request.input ?? [], toolChoice: request.toolChoice, model: request.model ?? request.models?.[0] ?? '', - models: - request.models ?? - (request.model - ? [ - request.model, - ] - : []), + models: request.models ?? [], temperature: request.temperature ?? null, maxOutputTokens: request.maxOutputTokens ?? null, topP: request.topP ?? null, @@ -173,6 +168,12 @@ function setNextTurnParam( * Apply computed nextTurnParams to the current request * Returns a new request object with updated parameters * + * `models` takes precedence over `model`: if `computedParams` changes + * `models` but not `model`, the request's `model` is reset to the new + * `models[0]`, even when the request already had an explicit `model`. + * This keeps the primary model in sync with the fallback list. To pin a + * different primary, set `model` in the same nextTurnParams update. + * * @param request - The current request * @param computedParams - Computed parameter values from nextTurnParams functions * @returns New request with updated parameters @@ -191,14 +192,8 @@ export function applyNextTurnParamsToRequest( ...request, ...sanitized, }; - if ( - (nextRequest.model === undefined || - ('models' in computedParams && !('model' in computedParams))) && - Array.isArray(nextRequest.models) && - nextRequest.models.length > 0 && - typeof nextRequest.models[0] === 'string' - ) { - nextRequest.model = nextRequest.models[0]; - } + fillPrimaryModelFromModels(nextRequest, { + overwrite: 'models' in computedParams && !('model' in computedParams), + }); return nextRequest; } diff --git a/packages/agent/src/lib/primary-model.ts b/packages/agent/src/lib/primary-model.ts new file mode 100644 index 00000000..10709840 --- /dev/null +++ b/packages/agent/src/lib/primary-model.ts @@ -0,0 +1,29 @@ +/** + * Populate the top-level `model` from the `models` fallback list (#121). + * + * OpenRouter server tools (e.g. `openrouter:datetime`) require a primary + * `model` on the request; a caller that only supplies `models` would + * otherwise send none. The primary is `models[0]`, matching the router's + * own fallback order. + * + * Mutates `request` in place. By default an existing `model` is kept; pass + * `overwrite: true` to replace it (used when `models` changed and `model` + * did not, so the two can't drift apart). + */ +export function fillPrimaryModelFromModels( + request: { + model?: unknown; + models?: unknown; + }, + options: { + overwrite?: boolean; + } = {}, +): void { + if (request.model !== undefined && !options.overwrite) { + return; + } + const primary = Array.isArray(request.models) ? request.models[0] : undefined; + if (typeof primary === 'string') { + request.model = primary; + } +} From e9f6c28af43c0e064e545b84527f892aced23cdb Mon Sep 17 00:00:00 2001 From: Luke Parke <5702154+LukasParke@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:07:32 +0000 Subject: [PATCH 3/3] refactor(agent): move fillPrimaryModelFromModels into async-params model-result.ts already imports async-params; adding a separate module pushed it over the sentrux god-file threshold and failed the structural gate. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/agent/src/lib/async-params.ts | 30 ++++++++++++++++++++++ packages/agent/src/lib/model-result.ts | 2 +- packages/agent/src/lib/next-turn-params.ts | 2 +- packages/agent/src/lib/primary-model.ts | 29 --------------------- 4 files changed, 32 insertions(+), 31 deletions(-) delete mode 100644 packages/agent/src/lib/primary-model.ts diff --git a/packages/agent/src/lib/async-params.ts b/packages/agent/src/lib/async-params.ts index f2f72e71..84d9f99f 100644 --- a/packages/agent/src/lib/async-params.ts +++ b/packages/agent/src/lib/async-params.ts @@ -406,3 +406,33 @@ export function hasAsyncFunctions(input: unknown): boolean { } return Object.values(input).some((value) => typeof value === 'function'); } + +/** + * Populate the top-level `model` from the `models` fallback list (#121). + * + * OpenRouter server tools (e.g. `openrouter:datetime`) require a primary + * `model` on the request; a caller that only supplies `models` would + * otherwise send none. The primary is `models[0]`, matching the router's + * own fallback order. + * + * Mutates `request` in place. By default an existing `model` is kept; pass + * `overwrite: true` to replace it (used when `models` changed and `model` + * did not, so the two can't drift apart). + */ +export function fillPrimaryModelFromModels( + request: { + model?: unknown; + models?: unknown; + }, + options: { + overwrite?: boolean; + } = {}, +): void { + if (request.model !== undefined && !options.overwrite) { + return; + } + const primary = Array.isArray(request.models) ? request.models[0] : undefined; + if (typeof primary === 'string') { + request.model = primary; + } +} diff --git a/packages/agent/src/lib/model-result.ts b/packages/agent/src/lib/model-result.ts index eb793f4d..364d186d 100644 --- a/packages/agent/src/lib/model-result.ts +++ b/packages/agent/src/lib/model-result.ts @@ -7,6 +7,7 @@ import * as z4 from 'zod/v4'; import type { $ZodObject, $ZodShape } from 'zod/v4/core'; import type { CallModelInput, ResolvedCallModelInput } from './async-params.js'; import { + fillPrimaryModelFromModels, hasAsyncFunctions, resolveAsyncFunctions, stripToolSetSnapshotMetadata, @@ -59,7 +60,6 @@ import { applyNextTurnParamsToRequest, executeNextTurnParamsFunctions, } from './next-turn-params.js'; -import { fillPrimaryModelFromModels } from './primary-model.js'; import type { StreamReplay } from './reusable-stream.js'; import { ReusableReadableStream } from './reusable-stream.js'; import { isStopConditionMet } from './stop-conditions.js'; diff --git a/packages/agent/src/lib/next-turn-params.ts b/packages/agent/src/lib/next-turn-params.ts index b360eb86..a883f047 100644 --- a/packages/agent/src/lib/next-turn-params.ts +++ b/packages/agent/src/lib/next-turn-params.ts @@ -1,5 +1,5 @@ import type * as models from '@openrouter/sdk/models'; -import { fillPrimaryModelFromModels } from './primary-model.js'; +import { fillPrimaryModelFromModels } from './async-params.js'; import type { NextTurnParamsContext, ParsedToolCall, Tool } from './tool-types.js'; import { isClientTool } from './tool-types.js'; diff --git a/packages/agent/src/lib/primary-model.ts b/packages/agent/src/lib/primary-model.ts deleted file mode 100644 index 10709840..00000000 --- a/packages/agent/src/lib/primary-model.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Populate the top-level `model` from the `models` fallback list (#121). - * - * OpenRouter server tools (e.g. `openrouter:datetime`) require a primary - * `model` on the request; a caller that only supplies `models` would - * otherwise send none. The primary is `models[0]`, matching the router's - * own fallback order. - * - * Mutates `request` in place. By default an existing `model` is kept; pass - * `overwrite: true` to replace it (used when `models` changed and `model` - * did not, so the two can't drift apart). - */ -export function fillPrimaryModelFromModels( - request: { - model?: unknown; - models?: unknown; - }, - options: { - overwrite?: boolean; - } = {}, -): void { - if (request.model !== undefined && !options.overwrite) { - return; - } - const primary = Array.isArray(request.models) ? request.models[0] : undefined; - if (typeof primary === 'string') { - request.model = primary; - } -}