From e5af1a1bd7f35dd3cc4de62bde545e0c05f43d33 Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Thu, 23 Jul 2026 12:29:36 -0500 Subject: [PATCH 1/4] Reapply "[Improve] Gate the linked-issue fetch on the routing precheck; default routing to Gemini 3.6 Flash (#716)" (#726) This reverts commit 15c4f69507f5a9a1724fad85fbcabd77b9ae93f1. --- .../router/__tests__/router-service.test.ts | 100 +++++++++++++++--- .../src/server/router/mcp-gather.ts | 58 ++++++---- .../src/server/router/router-service.ts | 10 +- .../cloud-agents/src/server/router/types.ts | 17 +++ 4 files changed, 149 insertions(+), 36 deletions(-) diff --git a/packages/cloud-agents/src/server/router/__tests__/router-service.test.ts b/packages/cloud-agents/src/server/router/__tests__/router-service.test.ts index 0142435f1..ef15a03e9 100644 --- a/packages/cloud-agents/src/server/router/__tests__/router-service.test.ts +++ b/packages/cloud-agents/src/server/router/__tests__/router-service.test.ts @@ -80,7 +80,7 @@ describe('routeTask', () => { expect(mockGenerateTrackedNonTaskObject).toHaveBeenCalledWith( expect.objectContaining({ userId: undefined, - model: undefined, + model: 'google/gemini-3.6-flash', system: expect.stringContaining( 'You are a workspace routing assistant', ), @@ -106,20 +106,30 @@ describe('routeTask', () => { }); }); - it('uses pasted GitHub issue context when choosing a workspace', async () => { + it('fetches pasted GitHub issue context when the precheck asks for it', async () => { mockCallRouterMcpTool.mockResolvedValue({ title: 'Fix the dashboard refresh failure', body: 'The dashboard API request belongs to the web application.', }); - mockGenerateTrackedNonTaskObject.mockResolvedValue({ - object: { - workspaceValue: 'Full Stack', - reasoning: 'The linked issue describes the web application.', - confidence: 0.92, - needsExternalLookup: false, - externalReference: null, - }, - }); + mockGenerateTrackedNonTaskObject + .mockResolvedValueOnce({ + object: { + workspaceValue: 'Full Stack', + reasoning: 'The message alone does not identify the workspace.', + confidence: 0.4, + needsExternalLookup: true, + externalReference: 'acme/web#42', + }, + }) + .mockResolvedValueOnce({ + object: { + workspaceValue: 'Full Stack', + reasoning: 'The linked issue describes the web application.', + confidence: 0.92, + needsExternalLookup: false, + externalReference: null, + }, + }); const result = await routeTask( createContext({ @@ -142,7 +152,8 @@ describe('routeTask', () => { issue_number: 42, }, }); - expect(mockGenerateTrackedNonTaskObject).toHaveBeenCalledWith( + expect(mockGenerateTrackedNonTaskObject).toHaveBeenCalledTimes(2); + expect(mockGenerateTrackedNonTaskObject).toHaveBeenLastCalledWith( expect.objectContaining({ prompt: expect.stringContaining('Fix the dashboard refresh failure'), }), @@ -153,6 +164,71 @@ describe('routeTask', () => { debug: { phase: 'mcp', toolsUsed: ['github.issue_read'], + needsExternalLookup: true, + }, + }, + }); + }); + + it('skips the issue fetch when the precheck routes without external context', async () => { + mockGenerateTrackedNonTaskObject.mockResolvedValue({ + object: { + workspaceValue: 'Full Stack', + reasoning: 'The message already identifies the dashboard work.', + confidence: 0.95, + needsExternalLookup: false, + externalReference: null, + }, + }); + + const result = await routeTask( + createContext({ + taskDescription: + 'Fix the dashboard refresh bug, context: https://github.com/acme/web/issues/42', + }), + ); + + expect(mockCallRouterMcpTool).not.toHaveBeenCalled(); + expect(mockGenerateTrackedNonTaskObject).toHaveBeenCalledTimes(1); + expect(result).toMatchObject({ + status: 'routed', + result: { + debug: { + phase: 'direct', + toolsUsed: [], + needsExternalLookup: false, + }, + }, + }); + }); + + it('keeps the precheck decision when the requested fetch returns nothing', async () => { + mockCallRouterMcpTool.mockRejectedValue(new Error('Not connected')); + mockGenerateTrackedNonTaskObject.mockResolvedValue({ + object: { + workspaceValue: 'Full Stack', + reasoning: 'Best guess without the linked issue.', + confidence: 0.4, + needsExternalLookup: true, + externalReference: 'acme/web#42', + }, + }); + + const result = await routeTask( + createContext({ + taskDescription: + 'Please investigate https://github.com/acme/web/issues/42', + }), + ); + + expect(mockGenerateTrackedNonTaskObject).toHaveBeenCalledTimes(1); + expect(result).toMatchObject({ + status: 'routed', + result: { + debug: { + phase: 'direct', + toolsUsed: [], + needsExternalLookup: true, }, }, }); diff --git a/packages/cloud-agents/src/server/router/mcp-gather.ts b/packages/cloud-agents/src/server/router/mcp-gather.ts index a769eb7cb..1241e0498 100644 --- a/packages/cloud-agents/src/server/router/mcp-gather.ts +++ b/packages/cloud-agents/src/server/router/mcp-gather.ts @@ -66,28 +66,48 @@ export async function gatherContextFromConfiguredMcps< z.infer & LookupAwareRoutingResponse > > { + const generateRoutingDecision = async (messages: ModelMessage[]) => { + const { object } = await generateTrackedNonTaskObject({ + userId: context.routingActor?.userId, + surface: NON_TASK_INFERENCE_SURFACES.routerTaskRouting, + model: routingModel, + schema: submitRoutingDecisionSchema, + system: routingPrompt, + prompt: serializeContextMessages(messages), + }); + + return object as z.infer & + LookupAwareRoutingResponse; + }; + + const response = await generateRoutingDecision(contextMessages); + const needsExternalLookup = + typeof response.needsExternalLookup === 'boolean' + ? response.needsExternalLookup + : null; + + if (needsExternalLookup !== true) { + return { response, toolsUsed: [], phase: 'direct', needsExternalLookup }; + } + + // The precheck asked for the linked issue, so the fetch deadline is only + // paid when it can change the decision. Fail-open: with nothing fetched, + // the precheck decision stands. const externalIssueContext = await gatherExternalIssueContext(context); - const { object } = await generateTrackedNonTaskObject({ - userId: context.routingActor?.userId, - surface: NON_TASK_INFERENCE_SURFACES.routerTaskRouting, - model: routingModel, - schema: submitRoutingDecisionSchema, - system: routingPrompt, - prompt: serializeContextMessages([ - ...contextMessages, - ...externalIssueContext.contextMessages, - ]), - }); - const response = object as z.infer & - LookupAwareRoutingResponse; + + if (externalIssueContext.contextMessages.length === 0) { + return { response, toolsUsed: [], phase: 'direct', needsExternalLookup }; + } + + const informedResponse = await generateRoutingDecision([ + ...contextMessages, + ...externalIssueContext.contextMessages, + ]); return { - response, + response: informedResponse, toolsUsed: externalIssueContext.toolsUsed, - phase: externalIssueContext.toolsUsed.length > 0 ? 'mcp' : 'direct', - needsExternalLookup: - typeof response.needsExternalLookup === 'boolean' - ? response.needsExternalLookup - : null, + phase: 'mcp', + needsExternalLookup: true, }; } diff --git a/packages/cloud-agents/src/server/router/router-service.ts b/packages/cloud-agents/src/server/router/router-service.ts index fe0ed9f60..4d03f423e 100644 --- a/packages/cloud-agents/src/server/router/router-service.ts +++ b/packages/cloud-agents/src/server/router/router-service.ts @@ -19,7 +19,7 @@ import type { RoutingTaskModelSelection, WorkspaceResponse, } from './types'; -import { R_SMALL_MODEL_LABEL, PLATFORM_WORKSPACE_VALUE } from './types'; +import { resolveRoutingModel, PLATFORM_WORKSPACE_VALUE } from './types'; import { gatherContextFromConfiguredMcps } from './mcp-gather'; import { callRouterMcpTool } from './mcp-tool-call'; import { FOLLOWUP_PROMPT } from './prompts/followup-prompt'; @@ -331,7 +331,7 @@ async function runRoutingDecision( forceDisablePlatformWorkspace?: boolean; }, ): Promise { - const routingModel = context.routingModel?.trim() || R_SMALL_MODEL_LABEL; + const routingModel = resolveRoutingModel(context.routingModel); try { const promptContext = context; @@ -345,7 +345,7 @@ async function runRoutingDecision( const responseResult = await gatherContextFromConfiguredMcps( promptContext, - context.routingModel?.trim(), + routingModel, routingPrompt, contextMessages, workspaceResponseSchema, @@ -582,7 +582,7 @@ export async function routeTask( export async function routeGitHubTask( context: RoutingContext, ): Promise { - const routingModel = context.routingModel?.trim() || R_SMALL_MODEL_LABEL; + const routingModel = resolveRoutingModel(context.routingModel); if (context.source.type !== 'github') { return { @@ -600,7 +600,7 @@ export async function routeGitHubTask( const { object: response } = await generateTrackedNonTaskObject({ userId: context.routingActor?.userId, surface: NON_TASK_INFERENCE_SURFACES.routerGitHubRouting, - model: context.routingModel?.trim(), + model: routingModel, schema: gitHubRoutingResponseSchema, system: buildGitHubRoutingPrompt(), prompt: JSON.stringify(buildContextMessages(context), null, 2), diff --git a/packages/cloud-agents/src/server/router/types.ts b/packages/cloud-agents/src/server/router/types.ts index e1eb58df6..395b0e3cd 100644 --- a/packages/cloud-agents/src/server/router/types.ts +++ b/packages/cloud-agents/src/server/router/types.ts @@ -19,6 +19,23 @@ export const LINEAR_AUTO_CONFIRM_TIMEOUT_MS = 120_000; export const R_SMALL_MODEL_LABEL = 'roomote-small-model'; +/** + * Default model for workspace-routing inference. Routing is a + * latency-sensitive precheck that also gates the external issue fetch, so it + * needs a model that asks for a lookup only when the message alone cannot + * route. An explicit context.routingModel wins, then the R_ROUTER_MODEL + * deployment override, then this default. + */ +const DEFAULT_ROUTING_MODEL = 'google/gemini-3.6-flash'; + +export function resolveRoutingModel(explicitModel?: string): string { + return ( + explicitModel?.trim() || + process.env.R_ROUTER_MODEL?.trim() || + DEFAULT_ROUTING_MODEL + ); +} + /** * Maximum length for task descriptions to prevent excessive token usage. */ From 73476d623d1febb33232a66e72bba1e92ae80047 Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Thu, 23 Jul 2026 12:36:35 -0500 Subject: [PATCH 2/4] [Improve] Re-land the router lookup gate with no hardcoded model id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-lands #716's two-step gate (precheck without tools; fetch linked-issue context only on needsExternalLookup=true) after the #726 revert. The routing model is now resolved as context.routingModel -> R_ROUTER_MODEL -> undefined, deferring to the deployment small-model resolution. No model id is hardcoded: the correct id depends on the deployment's provider configuration (openrouter/google/... via OpenRouter vs google/... direct), which is what broke routing in production — google/gemini-3.6-flash resolved to the unauthenticated direct Google provider and every routing call fell back to the manual picker. The routing decision's model field reports the resolved id when an override is set and the roomote-small-model label otherwise. Verified through the real routeTask against the production provider path (scripts/verify-reland.ts): default -> routed on the deployment small model; R_ROUTER_MODEL=openrouter/google/gemini-3.6-flash -> routed on Gemini with the id in debug output; the incident's message routes to the right environment in both. Co-Authored-By: Claude Fable 5 --- .../cloud-agents/scripts/verify-reland.ts | 43 +++++++++++++++++++ .../router/__tests__/router-service.test.ts | 28 +++++++++++- .../src/server/router/router-service.ts | 20 ++++++--- .../cloud-agents/src/server/router/types.ts | 18 +++----- 4 files changed, 90 insertions(+), 19 deletions(-) create mode 100644 packages/cloud-agents/scripts/verify-reland.ts diff --git a/packages/cloud-agents/scripts/verify-reland.ts b/packages/cloud-agents/scripts/verify-reland.ts new file mode 100644 index 000000000..4c7cac973 --- /dev/null +++ b/packages/cloud-agents/scripts/verify-reland.ts @@ -0,0 +1,43 @@ +#!/usr/bin/env npx tsx +import { routeTask } from '../src/server/router/router-service'; +import type { RoutingContext } from '../src/server/router/types'; + +const context: RoutingContext = { + taskDescription: 'Review the README in the Roomote repo', + source: { type: 'slack', channelName: 'engineering' }, + availableEnvironments: [ + { + id: 'env-roomote', + name: 'Roomote', + description: 'The main Roomote product — web app, API server, workers', + repositoryNames: ['RooCodeInc/Roomote'], + }, + { + id: 'env-website', + name: 'Roomote Website', + description: 'Astro marketing website for Roomote', + repositoryNames: ['RooCodeInc/roomote-website'], + }, + ], +}; + +const decision = await routeTask(context); +console.log( + JSON.stringify( + { + R_ROUTER_MODEL: process.env.R_ROUTER_MODEL ?? null, + status: decision.status, + ...(decision.status === 'routed' + ? { + workspace: decision.result.workspace, + model: decision.result.debug ? undefined : undefined, + debug: decision.result.debug, + } + : {}), + ...(decision.status === 'fallback' ? { reason: decision.reason } : {}), + }, + null, + 2, + ), +); +process.exit(0); diff --git a/packages/cloud-agents/src/server/router/__tests__/router-service.test.ts b/packages/cloud-agents/src/server/router/__tests__/router-service.test.ts index ef15a03e9..3e6c171ea 100644 --- a/packages/cloud-agents/src/server/router/__tests__/router-service.test.ts +++ b/packages/cloud-agents/src/server/router/__tests__/router-service.test.ts @@ -80,7 +80,7 @@ describe('routeTask', () => { expect(mockGenerateTrackedNonTaskObject).toHaveBeenCalledWith( expect.objectContaining({ userId: undefined, - model: 'google/gemini-3.6-flash', + model: undefined, system: expect.stringContaining( 'You are a workspace routing assistant', ), @@ -106,6 +106,32 @@ describe('routeTask', () => { }); }); + it('uses the R_ROUTER_MODEL override and never a hardcoded model id', async () => { + vi.stubEnv('R_ROUTER_MODEL', 'openrouter/google/gemini-3.6-flash'); + + try { + mockGenerateTrackedNonTaskObject.mockResolvedValue({ + object: { + workspaceValue: 'Full Stack', + reasoning: 'Full Stack is the best fit.', + confidence: 0.9, + needsExternalLookup: false, + externalReference: null, + }, + }); + + await routeTask(createContext()); + + expect(mockGenerateTrackedNonTaskObject).toHaveBeenCalledWith( + expect.objectContaining({ + model: 'openrouter/google/gemini-3.6-flash', + }), + ); + } finally { + vi.unstubAllEnvs(); + } + }); + it('fetches pasted GitHub issue context when the precheck asks for it', async () => { mockCallRouterMcpTool.mockResolvedValue({ title: 'Fix the dashboard refresh failure', diff --git a/packages/cloud-agents/src/server/router/router-service.ts b/packages/cloud-agents/src/server/router/router-service.ts index 4d03f423e..cd3f942eb 100644 --- a/packages/cloud-agents/src/server/router/router-service.ts +++ b/packages/cloud-agents/src/server/router/router-service.ts @@ -19,7 +19,11 @@ import type { RoutingTaskModelSelection, WorkspaceResponse, } from './types'; -import { resolveRoutingModel, PLATFORM_WORKSPACE_VALUE } from './types'; +import { + resolveRoutingModel, + R_SMALL_MODEL_LABEL, + PLATFORM_WORKSPACE_VALUE, +} from './types'; import { gatherContextFromConfiguredMcps } from './mcp-gather'; import { callRouterMcpTool } from './mcp-tool-call'; import { FOLLOWUP_PROMPT } from './prompts/followup-prompt'; @@ -332,6 +336,7 @@ async function runRoutingDecision( }, ): Promise { const routingModel = resolveRoutingModel(context.routingModel); + const routingModelLabel = routingModel ?? R_SMALL_MODEL_LABEL; try { const promptContext = context; @@ -364,7 +369,7 @@ async function runRoutingDecision( reason: built.fallbackReason, }, phase: responseResult.phase ?? 'fallback', - model: routingModel, + model: routingModelLabel, toolsUsed: responseResult.toolsUsed, needsExternalLookup: responseResult.needsExternalLookup, confidence: built.confidence, @@ -379,7 +384,7 @@ async function runRoutingDecision( reasoning: built.reasoning, }, phase: responseResult.phase ?? 'direct', - model: routingModel, + model: routingModelLabel, toolsUsed: responseResult.toolsUsed, needsExternalLookup: responseResult.needsExternalLookup, confidence: built.confidence, @@ -393,7 +398,7 @@ async function runRoutingDecision( result: built.result, }, phase: responseResult.phase ?? 'direct', - model: routingModel, + model: routingModelLabel, toolsUsed: responseResult.toolsUsed, needsExternalLookup: responseResult.needsExternalLookup, confidence: built.confidence, @@ -410,7 +415,7 @@ async function runRoutingDecision( error instanceof Error ? error.message : 'Unknown routing error', }, phase: 'fallback', - model: routingModel, + model: routingModelLabel, toolsUsed: [], needsExternalLookup: null, confidence: null, @@ -583,6 +588,7 @@ export async function routeGitHubTask( context: RoutingContext, ): Promise { const routingModel = resolveRoutingModel(context.routingModel); + const routingModelLabel = routingModel ?? R_SMALL_MODEL_LABEL; if (context.source.type !== 'github') { return { @@ -623,7 +629,7 @@ export async function routeGitHubTask( console.info( formatSingleLineLog('[LLM Router] Routed GitHub task', { sourceType: context.source.type, - model: routingModel, + model: routingModelLabel, phase: 'direct', toolsUsed: [], needsExternalLookup: false, @@ -645,7 +651,7 @@ export async function routeGitHubTask( console.warn( formatSingleLineLog('[LLM Router] GitHub routing fallback', { sourceType: context.source.type, - model: routingModel, + model: routingModelLabel, phase: 'fallback', toolsUsed: [], needsExternalLookup: null, diff --git a/packages/cloud-agents/src/server/router/types.ts b/packages/cloud-agents/src/server/router/types.ts index 395b0e3cd..3231edfbe 100644 --- a/packages/cloud-agents/src/server/router/types.ts +++ b/packages/cloud-agents/src/server/router/types.ts @@ -20,19 +20,15 @@ export const LINEAR_AUTO_CONFIRM_TIMEOUT_MS = 120_000; export const R_SMALL_MODEL_LABEL = 'roomote-small-model'; /** - * Default model for workspace-routing inference. Routing is a - * latency-sensitive precheck that also gates the external issue fetch, so it - * needs a model that asks for a lookup only when the message alone cannot - * route. An explicit context.routingModel wins, then the R_ROUTER_MODEL - * deployment override, then this default. + * Model for workspace-routing inference: an explicit context.routingModel + * wins, then the R_ROUTER_MODEL deployment override. No model id is ever + * hardcoded here — the correct id depends on the deployment's provider + * configuration (e.g. openrouter/google/... vs google/...), so returning + * undefined defers to the deployment small-model resolution. */ -const DEFAULT_ROUTING_MODEL = 'google/gemini-3.6-flash'; - -export function resolveRoutingModel(explicitModel?: string): string { +export function resolveRoutingModel(explicitModel?: string): string | undefined { return ( - explicitModel?.trim() || - process.env.R_ROUTER_MODEL?.trim() || - DEFAULT_ROUTING_MODEL + explicitModel?.trim() || process.env.R_ROUTER_MODEL?.trim() || undefined ); } From a471457fe18716c12a83b0c9d86c1738971beedd Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Thu, 23 Jul 2026 13:44:34 -0500 Subject: [PATCH 3/4] Format types.ts per prettier Co-Authored-By: Claude Fable 5 --- packages/cloud-agents/src/server/router/types.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/cloud-agents/src/server/router/types.ts b/packages/cloud-agents/src/server/router/types.ts index 3231edfbe..e319797b8 100644 --- a/packages/cloud-agents/src/server/router/types.ts +++ b/packages/cloud-agents/src/server/router/types.ts @@ -26,7 +26,9 @@ export const R_SMALL_MODEL_LABEL = 'roomote-small-model'; * configuration (e.g. openrouter/google/... vs google/...), so returning * undefined defers to the deployment small-model resolution. */ -export function resolveRoutingModel(explicitModel?: string): string | undefined { +export function resolveRoutingModel( + explicitModel?: string, +): string | undefined { return ( explicitModel?.trim() || process.env.R_ROUTER_MODEL?.trim() || undefined ); From 0d07fb4dfda3f07e588e67ffc5e784fc38da63b7 Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Thu, 23 Jul 2026 13:57:52 -0500 Subject: [PATCH 4/4] =?UTF-8?q?Strip=20all=20model-resolution=20changes=20?= =?UTF-8?q?=E2=80=94=20gate=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review: the router model stays exactly as on develop (context.routingModel -> deployment small model). types.ts and router-service.ts are now zero-diff against develop; the PR touches only mcp-gather.ts (the two-step gate), its tests, and the verify-reland script. Co-Authored-By: Claude Fable 5 --- .../router/__tests__/router-service.test.ts | 26 ----------------- .../src/server/router/router-service.ts | 28 ++++++++----------- .../cloud-agents/src/server/router/types.ts | 15 ---------- 3 files changed, 11 insertions(+), 58 deletions(-) diff --git a/packages/cloud-agents/src/server/router/__tests__/router-service.test.ts b/packages/cloud-agents/src/server/router/__tests__/router-service.test.ts index 3e6c171ea..449a4222e 100644 --- a/packages/cloud-agents/src/server/router/__tests__/router-service.test.ts +++ b/packages/cloud-agents/src/server/router/__tests__/router-service.test.ts @@ -106,32 +106,6 @@ describe('routeTask', () => { }); }); - it('uses the R_ROUTER_MODEL override and never a hardcoded model id', async () => { - vi.stubEnv('R_ROUTER_MODEL', 'openrouter/google/gemini-3.6-flash'); - - try { - mockGenerateTrackedNonTaskObject.mockResolvedValue({ - object: { - workspaceValue: 'Full Stack', - reasoning: 'Full Stack is the best fit.', - confidence: 0.9, - needsExternalLookup: false, - externalReference: null, - }, - }); - - await routeTask(createContext()); - - expect(mockGenerateTrackedNonTaskObject).toHaveBeenCalledWith( - expect.objectContaining({ - model: 'openrouter/google/gemini-3.6-flash', - }), - ); - } finally { - vi.unstubAllEnvs(); - } - }); - it('fetches pasted GitHub issue context when the precheck asks for it', async () => { mockCallRouterMcpTool.mockResolvedValue({ title: 'Fix the dashboard refresh failure', diff --git a/packages/cloud-agents/src/server/router/router-service.ts b/packages/cloud-agents/src/server/router/router-service.ts index cd3f942eb..fe0ed9f60 100644 --- a/packages/cloud-agents/src/server/router/router-service.ts +++ b/packages/cloud-agents/src/server/router/router-service.ts @@ -19,11 +19,7 @@ import type { RoutingTaskModelSelection, WorkspaceResponse, } from './types'; -import { - resolveRoutingModel, - R_SMALL_MODEL_LABEL, - PLATFORM_WORKSPACE_VALUE, -} from './types'; +import { R_SMALL_MODEL_LABEL, PLATFORM_WORKSPACE_VALUE } from './types'; import { gatherContextFromConfiguredMcps } from './mcp-gather'; import { callRouterMcpTool } from './mcp-tool-call'; import { FOLLOWUP_PROMPT } from './prompts/followup-prompt'; @@ -335,8 +331,7 @@ async function runRoutingDecision( forceDisablePlatformWorkspace?: boolean; }, ): Promise { - const routingModel = resolveRoutingModel(context.routingModel); - const routingModelLabel = routingModel ?? R_SMALL_MODEL_LABEL; + const routingModel = context.routingModel?.trim() || R_SMALL_MODEL_LABEL; try { const promptContext = context; @@ -350,7 +345,7 @@ async function runRoutingDecision( const responseResult = await gatherContextFromConfiguredMcps( promptContext, - routingModel, + context.routingModel?.trim(), routingPrompt, contextMessages, workspaceResponseSchema, @@ -369,7 +364,7 @@ async function runRoutingDecision( reason: built.fallbackReason, }, phase: responseResult.phase ?? 'fallback', - model: routingModelLabel, + model: routingModel, toolsUsed: responseResult.toolsUsed, needsExternalLookup: responseResult.needsExternalLookup, confidence: built.confidence, @@ -384,7 +379,7 @@ async function runRoutingDecision( reasoning: built.reasoning, }, phase: responseResult.phase ?? 'direct', - model: routingModelLabel, + model: routingModel, toolsUsed: responseResult.toolsUsed, needsExternalLookup: responseResult.needsExternalLookup, confidence: built.confidence, @@ -398,7 +393,7 @@ async function runRoutingDecision( result: built.result, }, phase: responseResult.phase ?? 'direct', - model: routingModelLabel, + model: routingModel, toolsUsed: responseResult.toolsUsed, needsExternalLookup: responseResult.needsExternalLookup, confidence: built.confidence, @@ -415,7 +410,7 @@ async function runRoutingDecision( error instanceof Error ? error.message : 'Unknown routing error', }, phase: 'fallback', - model: routingModelLabel, + model: routingModel, toolsUsed: [], needsExternalLookup: null, confidence: null, @@ -587,8 +582,7 @@ export async function routeTask( export async function routeGitHubTask( context: RoutingContext, ): Promise { - const routingModel = resolveRoutingModel(context.routingModel); - const routingModelLabel = routingModel ?? R_SMALL_MODEL_LABEL; + const routingModel = context.routingModel?.trim() || R_SMALL_MODEL_LABEL; if (context.source.type !== 'github') { return { @@ -606,7 +600,7 @@ export async function routeGitHubTask( const { object: response } = await generateTrackedNonTaskObject({ userId: context.routingActor?.userId, surface: NON_TASK_INFERENCE_SURFACES.routerGitHubRouting, - model: routingModel, + model: context.routingModel?.trim(), schema: gitHubRoutingResponseSchema, system: buildGitHubRoutingPrompt(), prompt: JSON.stringify(buildContextMessages(context), null, 2), @@ -629,7 +623,7 @@ export async function routeGitHubTask( console.info( formatSingleLineLog('[LLM Router] Routed GitHub task', { sourceType: context.source.type, - model: routingModelLabel, + model: routingModel, phase: 'direct', toolsUsed: [], needsExternalLookup: false, @@ -651,7 +645,7 @@ export async function routeGitHubTask( console.warn( formatSingleLineLog('[LLM Router] GitHub routing fallback', { sourceType: context.source.type, - model: routingModelLabel, + model: routingModel, phase: 'fallback', toolsUsed: [], needsExternalLookup: null, diff --git a/packages/cloud-agents/src/server/router/types.ts b/packages/cloud-agents/src/server/router/types.ts index e319797b8..e1eb58df6 100644 --- a/packages/cloud-agents/src/server/router/types.ts +++ b/packages/cloud-agents/src/server/router/types.ts @@ -19,21 +19,6 @@ export const LINEAR_AUTO_CONFIRM_TIMEOUT_MS = 120_000; export const R_SMALL_MODEL_LABEL = 'roomote-small-model'; -/** - * Model for workspace-routing inference: an explicit context.routingModel - * wins, then the R_ROUTER_MODEL deployment override. No model id is ever - * hardcoded here — the correct id depends on the deployment's provider - * configuration (e.g. openrouter/google/... vs google/...), so returning - * undefined defers to the deployment small-model resolution. - */ -export function resolveRoutingModel( - explicitModel?: string, -): string | undefined { - return ( - explicitModel?.trim() || process.env.R_ROUTER_MODEL?.trim() || undefined - ); -} - /** * Maximum length for task descriptions to prevent excessive token usage. */