From 92574de164b3f1bd46877b2af4e565bfa742d000 Mon Sep 17 00:00:00 2001 From: Syed Raheemuddin Date: Sun, 30 Aug 2026 11:21:53 +0530 Subject: [PATCH] fix(chat): preserve unstripped model string for passthrough provider routing (#11840) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Boarded with #12003/#11841/#11839 in one combined worktree: typecheck:core, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles all green; 24/24 focused tests pass. Contained fix — preserves the unstripped model string for passthrough providers (cline/kilocode) only when the combo actually redirected to a passthrough provider. Thanks for the regression coverage. --- src/sse/handlers/chat.ts | 15 ++++-- .../chat-passthrough-model-routing.test.ts | 47 +++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 tests/unit/chat-passthrough-model-routing.test.ts diff --git a/src/sse/handlers/chat.ts b/src/sse/handlers/chat.ts index d504d67086..3852ee1d46 100644 --- a/src/sse/handlers/chat.ts +++ b/src/sse/handlers/chat.ts @@ -59,6 +59,7 @@ import { getModelTargetFormat, PROVIDER_ID_TO_ALIAS, } from "@omniroute/open-sse/config/providerModels.ts"; +import { getPassthroughProviders } from "@omniroute/open-sse/config/providerRegistry.ts"; import * as log from "../utils/logger"; import { checkAndRefreshToken } from "../services/tokenRefresh"; import { createHookContext, runHooks, initPreRequestRegistry } from "@/lib/middleware/registry"; @@ -1098,7 +1099,7 @@ async function handleChatImplementation( return credentials; })(), cachedSettings: settings, - providerId: target?.providerId ?? null, + providerId: target?.providerId ?? (target as any)?.provider ?? null, correlationId: reqId, conversationId, modelPinned: (target as any)?.modelPinned ?? false, @@ -1389,7 +1390,7 @@ async function handleSingleModelChat( comboExecutionKey: null, skipUpstreamRetry: resolvedTarget?.failoverBeforeRetry === true, allowRateLimitedConnection: resolvedTarget?.allowRateLimitedConnection === true, - providerId: resolvedTarget?.providerId ?? null, + providerId: resolvedTarget?.providerId ?? (resolvedTarget as any)?.provider ?? null, correlationId: runtimeOptions?.correlationId ?? null, reasoningTransportFallback: redirectCombo.config?.reasoningTransportFallback === "skip" ? "skip" : "drop", @@ -1738,10 +1739,18 @@ async function handleSingleModelChat( // defaultModel, resolve the bare name to that real model ID before the // upstream call so the provider receives a concrete model rather than the // placeholder. A "/"-qualified model name is always left untouched. - const effectiveModel = + let effectiveModel = resolveBareModelToConnectionDefault(modelStr, model, credentials.defaultModel) ?? model; let requestBody = effectiveModel !== model ? { ...body, model: `${provider}/${effectiveModel}` } : body; + + // If the combo explicitly overrode the provider to a passthrough provider, we + // must preserve the original unstripped modelStr so that proxy providers + // (e.g., cline, kilocode) get the exact string they expect. + if (provider !== resolvedProvider && getPassthroughProviders().has(provider)) { + effectiveModel = modelStr; + requestBody = { ...body, model: modelStr }; + } if (!runtimeOptions.reasoningDecision && runtimeOptions.reasoningIntent) { const connectionRouting = await applyConnectionReasoningRule({ requestBody, diff --git a/tests/unit/chat-passthrough-model-routing.test.ts b/tests/unit/chat-passthrough-model-routing.test.ts new file mode 100644 index 0000000000..e643dca2a4 --- /dev/null +++ b/tests/unit/chat-passthrough-model-routing.test.ts @@ -0,0 +1,47 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { getPassthroughProviders } from "../../open-sse/config/providerRegistry.ts"; + +test("src/sse/handlers/chat.ts imports cleanly without module or syntax errors", async () => { + const chatHandler = await import("../../src/sse/handlers/chat.ts"); + assert.ok(chatHandler, "chat.ts handler module should export successfully"); +}); + +test("getPassthroughProviders includes expected proxy/passthrough providers", () => { + const providers = getPassthroughProviders(); + assert.ok(providers.has("cline"), "cline should be a passthrough provider"); + assert.ok(providers.has("kilocode"), "kilocode should be a passthrough provider"); +}); + +test("passthrough model routing preserves original unstripped modelStr when provider overrides to passthrough", () => { + const passthroughProviders = getPassthroughProviders(); + const provider: string = "cline"; + const resolvedProvider: string = "openai"; + const modelStr = "cline/gpt-4o-mini"; + const model = "gpt-4o-mini"; + const body = { model: "cline/gpt-4o-mini", messages: [] }; + + let effectiveModel = model; + let requestBody = { ...body, model: `cline/${effectiveModel}` }; + + if (provider !== resolvedProvider && passthroughProviders.has(provider)) { + effectiveModel = modelStr; + requestBody = { ...body, model: modelStr }; + } + + assert.equal(effectiveModel, "cline/gpt-4o-mini"); + assert.equal(requestBody.model, "cline/gpt-4o-mini"); +}); + +test("providerId falls back to target.provider when target.providerId is absent", () => { + const targetWithProviderId = { providerId: "prov_123", provider: "cline" }; + const targetWithProviderOnly = { provider: "kilocode" }; + const targetEmpty = {}; + + const resolveProviderId = (target: { providerId?: string; provider?: string }) => + target?.providerId ?? target?.provider ?? null; + + assert.equal(resolveProviderId(targetWithProviderId), "prov_123"); + assert.equal(resolveProviderId(targetWithProviderOnly), "kilocode"); + assert.equal(resolveProviderId(targetEmpty), null); +});