From bb71d8e0b3cc28bc541f0e272d82ec67c154692c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Brand=C3=A3o?= Date: Sat, 7 Nov 2026 17:03:59 -0300 Subject: [PATCH 1/2] fix(proxy): a connection base URL no longer forwards the catalog prefix (opencode-go/glm-5.3) to the upstream With an editable base URL the native endpoint becomes an override that ProviderClient treats as custom, so the model went upstream with Thinker's catalog prefix and OpenCode answered "Model is unavailable". The resolver now strips the prefix with the same rule the native endpoint uses. Co-Authored-By: Claude Opus 5.5 --- .../proxy/forward-endpoint-resolver.spec.ts | 19 ++++++++++ .../proxy/forward-endpoint-resolver.ts | 11 +++--- .../backend/src/routing/proxy/model-prefix.ts | 36 +++++++++++++++++++ .../src/routing/proxy/provider-client.ts | 36 +------------------ 4 files changed, 63 insertions(+), 39 deletions(-) create mode 100644 packages/backend/src/routing/proxy/model-prefix.ts diff --git a/packages/backend/src/routing/proxy/forward-endpoint-resolver.spec.ts b/packages/backend/src/routing/proxy/forward-endpoint-resolver.spec.ts index 847c9c89..fb1cd0a8 100644 --- a/packages/backend/src/routing/proxy/forward-endpoint-resolver.spec.ts +++ b/packages/backend/src/routing/proxy/forward-endpoint-resolver.spec.ts @@ -347,6 +347,25 @@ describe('resolveForwardEndpoint', () => { expect(account.customEndpoint?.allowPrivateDestination).toBe(false); }); + it('strips the catalog prefix when a connection base URL overrides a native endpoint', () => { + const go = resolveForwardEndpoint({ + provider: 'opencode-go', + authType: 'api_key', + model: 'opencode-go/glm-5.3', + providerBaseUrl: 'http://wwz:8000/opencode/inference/go/openai', + }); + expect(go.customEndpoint).toBeDefined(); + expect(go.forwardModel).toBe('glm-5.3'); + // OpenRouter ids keep their vendor segment, as on the native endpoint. + const or = resolveForwardEndpoint({ + provider: 'openrouter', + authType: 'api_key', + model: 'white-wizard/wwz-fast', + providerBaseUrl: 'http://wwz:8000/openrouter', + }); + expect(or.forwardModel).toBe('white-wizard/wwz-fast'); + }); + it('sets no override for a custom provider whose row is missing', () => { const out = resolveForwardEndpoint({ provider: 'custom:gone', diff --git a/packages/backend/src/routing/proxy/forward-endpoint-resolver.ts b/packages/backend/src/routing/proxy/forward-endpoint-resolver.ts index 22c22b19..1d757484 100644 --- a/packages/backend/src/routing/proxy/forward-endpoint-resolver.ts +++ b/packages/backend/src/routing/proxy/forward-endpoint-resolver.ts @@ -20,6 +20,7 @@ import { type ProviderEndpoint, } from './provider-endpoints'; import { CustomProviderService } from '../custom-provider/custom-provider.service'; +import { stripModelPrefix } from './model-prefix'; import { normalizeMinimaxSubscriptionBaseUrl } from '../provider-base-url'; import { getBedrockMantleBaseUrl, @@ -160,10 +161,12 @@ export function resolveForwardEndpoint( } } else if (providerBaseUrl && authType === 'api_key' && resolveEndpointKey(provider)) { const key = resolveEndpointKey(provider)!; - customEndpoint = buildEndpointOverride( - providerBaseUrl, - key === 'bedrock' ? resolveBedrockEndpointKey(model, apiMode) : key, - ); + const overrideKey = key === 'bedrock' ? resolveBedrockEndpointKey(model, apiMode) : key; + customEndpoint = buildEndpointOverride(providerBaseUrl, overrideKey); + // The override reaches ProviderClient as a custom endpoint, which forwards + // the model untouched; strip the catalog prefix (opencode-go/glm-5.3) here, + // exactly as the native endpoint would have. + forwardModel = stripModelPrefix(forwardModel, overrideKey); customEndpoint.allowPrivateDestination = allowsPrivateDestination(providerBaseUrlAllowsPrivate); } else if (resolveEndpointKey(provider) === 'bedrock' && isBedrockRegion(providerRegion)) { const bedrockEndpointKey = resolveBedrockEndpointKey(model, apiMode); diff --git a/packages/backend/src/routing/proxy/model-prefix.ts b/packages/backend/src/routing/proxy/model-prefix.ts new file mode 100644 index 00000000..77822aaf --- /dev/null +++ b/packages/backend/src/routing/proxy/model-prefix.ts @@ -0,0 +1,36 @@ +import { stripVendorPrefix } from '../../common/constants/openai-models'; + +/** + * Strip vendor prefix from model name (e.g. "anthropic/claude-sonnet-4" → "claude-sonnet-4"). + * Models synced from OpenRouter use vendor prefixes, but native APIs expect bare names. + */ +export function stripModelPrefix(model: string, endpointKey: string): string { + // OpenRouter expects vendor prefixes. + if (endpointKey === 'openrouter') return model; + if (endpointKey === 'commandcode' || endpointKey === 'commandcode-anthropic') { + return model.startsWith('commandcode/') ? model.slice('commandcode/'.length) : model; + } + // Custom providers, Fireworks, Groq, Hugging Face, Kilo, Nous, NVIDIA NIM, Ollama, Pioneer, and ClinePass: model IDs from these APIs contain + // legitimate slash segments (e.g. "accounts/fireworks/models/deepseek-v3p1", + // "MiniMaxAI/MiniMax-2.7", "meta-llama/llama-guard-4-12b", "anthropic/claude-sonnet-4.5", + // "cline-pass/deepseek-v4-flash"). Stripping would mangle the name the upstream API expects. + if ( + endpointKey === 'custom' || + endpointKey === 'fireworks' || + endpointKey === 'groq' || + endpointKey === 'huggingface' || + endpointKey === 'kilo' || + endpointKey === 'nous' || + endpointKey === 'nvidia' || + endpointKey === 'cline-pass' || + endpointKey === 'ollama' || + endpointKey === 'ollama-cloud' || + // LM Studio serves hub ids such as `qwen/qwen3-8b`; llama-server echoes + // whatever `--alias` or file path it was started with. + endpointKey === 'lmstudio' || + endpointKey === 'llamacpp' || + endpointKey === 'pioneer' + ) + return model; + return stripVendorPrefix(model); +} diff --git a/packages/backend/src/routing/proxy/provider-client.ts b/packages/backend/src/routing/proxy/provider-client.ts index 2ba8631c..2615337d 100644 --- a/packages/backend/src/routing/proxy/provider-client.ts +++ b/packages/backend/src/routing/proxy/provider-client.ts @@ -32,6 +32,7 @@ import { } from './proxy-transport'; import { resolveSubscriptionEndpointKey } from './provider-hooks'; import { isAnthropicHost, mergeAnthropicBeta } from './anthropic-beta'; +import { stripModelPrefix } from './model-prefix'; import { mergeExtraHeaders } from './extra-headers'; import { injectOpenAiMessageCacheControl, injectOpenRouterCacheControl } from './cache-injection'; import { @@ -363,41 +364,6 @@ function openRouterCacheMode(model: string): 'anthropic' | 'message' | null { return null; } -/** - * Strip vendor prefix from model name (e.g. "anthropic/claude-sonnet-4" → "claude-sonnet-4"). - * Models synced from OpenRouter use vendor prefixes, but native APIs expect bare names. - */ -function stripModelPrefix(model: string, endpointKey: string): string { - // OpenRouter expects vendor prefixes. - if (endpointKey === 'openrouter') return model; - if (endpointKey === 'commandcode' || endpointKey === 'commandcode-anthropic') { - return model.startsWith('commandcode/') ? model.slice('commandcode/'.length) : model; - } - // Custom providers, Fireworks, Groq, Hugging Face, Kilo, Nous, NVIDIA NIM, Ollama, Pioneer, and ClinePass: model IDs from these APIs contain - // legitimate slash segments (e.g. "accounts/fireworks/models/deepseek-v3p1", - // "MiniMaxAI/MiniMax-2.7", "meta-llama/llama-guard-4-12b", "anthropic/claude-sonnet-4.5", - // "cline-pass/deepseek-v4-flash"). Stripping would mangle the name the upstream API expects. - if ( - endpointKey === 'custom' || - endpointKey === 'fireworks' || - endpointKey === 'groq' || - endpointKey === 'huggingface' || - endpointKey === 'kilo' || - endpointKey === 'nous' || - endpointKey === 'nvidia' || - endpointKey === 'cline-pass' || - endpointKey === 'ollama' || - endpointKey === 'ollama-cloud' || - // LM Studio serves hub ids such as `qwen/qwen3-8b`; llama-server echoes - // whatever `--alias` or file path it was started with. - endpointKey === 'lmstudio' || - endpointKey === 'llamacpp' || - endpointKey === 'pioneer' - ) - return model; - return stripVendorPrefix(model); -} - @Injectable() export class ProviderClient { private readonly logger = new Logger(ProviderClient.name); From a66f5bd6a92f37949c7df21cadb982ebec35b861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Brand=C3=A3o?= Date: Sat, 7 Nov 2026 17:04:31 -0300 Subject: [PATCH 2/2] chore(release): version 0.31.5 --- docs/releases/0.31.5.md | 9 +++++++++ package-lock.json | 2 +- packages/thinker/package.json | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 docs/releases/0.31.5.md diff --git a/docs/releases/0.31.5.md b/docs/releases/0.31.5.md new file mode 100644 index 00000000..e9e38f2a --- /dev/null +++ b/docs/releases/0.31.5.md @@ -0,0 +1,9 @@ +# Thinker 0.31.5 + +```bash +docker pull tmsoftbrasil/thinker:0.31.5 +``` + +## Mudanças + +- fix(proxy): a connection base URL no longer forwards the catalog prefix (opencode-go/glm-5.3) to the upstream diff --git a/package-lock.json b/package-lock.json index 5f225311..47a2580e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14768,7 +14768,7 @@ } }, "packages/thinker": { - "version": "0.31.4" + "version": "0.31.5" } } } diff --git a/packages/thinker/package.json b/packages/thinker/package.json index 9141d501..4604d56d 100644 --- a/packages/thinker/package.json +++ b/packages/thinker/package.json @@ -1,6 +1,6 @@ { "name": "thinker", - "version": "0.31.4", + "version": "0.31.5", "private": true, "description": "Canonical release version for the Thinker Docker image. This package holds no code — the real backend and frontend live in ../backend and ../frontend. See README.md for details." }