From 8f3be03f26758d7908fa157936ac1f8d295b4587 Mon Sep 17 00:00:00 2001 From: maatheusgois-dd Date: Mon, 20 Jul 2026 21:34:01 -0300 Subject: [PATCH] Fix Portkey gateway models rewriting to bundled wire ids. Custom models with @-prefixed gateway ids (e.g. @modal/GLM-5-2-FP8) were fuzzy-matched to devin/glm-5-2 and inherited cross-provider effortRouting, sending glm-5-2 to Portkey without x-portkey-config. Skip @ ids in reference lookup and block cross-provider thinking inheritance. Co-authored-by: Cursor --- packages/catalog/src/identity/reference.ts | 21 +++++- .../catalog/test/gateway-reference.test.ts | 17 +++++ packages/coding-agent/CHANGELOG.md | 4 ++ .../src/config/model-discovery.ts | 5 +- .../coding-agent/src/config/model-registry.ts | 8 ++- .../issue-portkey-gateway-wire-model.test.ts | 67 +++++++++++++++++++ 6 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 packages/catalog/test/gateway-reference.test.ts create mode 100644 packages/coding-agent/test/issue-portkey-gateway-wire-model.test.ts diff --git a/packages/catalog/src/identity/reference.ts b/packages/catalog/src/identity/reference.ts index 461fc8e11ff..0dc6b7c2a57 100644 --- a/packages/catalog/src/identity/reference.ts +++ b/packages/catalog/src/identity/reference.ts @@ -8,7 +8,7 @@ * may strip `search`-style markers and prefers cache-pricing-complete * references, both of which would be wrong for canonical coalescing. */ -import type { Api, Model } from "../types"; +import type { Api, Model, ThinkingConfig } from "../types"; import { getBracketStrippedModelIdCandidates, getLongestModelLikeIdSegment, getModelLikeIdSegments } from "./id"; import { REFERENCE_TRAILING_MARKER_PATTERN } from "./markers"; @@ -137,8 +137,27 @@ function getReferenceCandidateIds(modelId: string): string[] { return [...candidates]; } +/** + * Inherit bundled reference thinking only for same-provider matches. Wire routing + * (`effortRouting`) is provider-specific; cross-provider inheritance can rewrite + * gateway ids (e.g. Portkey `@modal/GLM-5-2-FP8` → devin `glm-5-2`). + */ +export function inheritReferenceThinking( + modelThinking: ThinkingConfig | undefined, + reference: Model | undefined, + provider: string, +): ThinkingConfig | undefined { + if (modelThinking !== undefined) return modelThinking; + if (!reference?.thinking) return undefined; + if (reference.provider !== provider) return undefined; + return reference.thinking; +} + /** Resolve a (possibly proxied/affixed) model id to its bundled upstream reference. */ export function resolveModelReference(modelId: string, index: ModelReferenceIndex): Model | undefined { + // Portkey/gateway wire ids (`@provider/model`) are opaque; fuzzy matching would + // map them to unrelated bundled entries (e.g. `@modal/GLM-5-2-FP8` → devin/glm-5-2). + if (modelId.startsWith("@")) return undefined; for (const candidate of getReferenceCandidateIds(modelId)) { const key = normalizeReferenceKey(candidate); const reference = index.exact.get(key) ?? index.suffixAlias.get(key); diff --git a/packages/catalog/test/gateway-reference.test.ts b/packages/catalog/test/gateway-reference.test.ts new file mode 100644 index 00000000000..9d9ccede342 --- /dev/null +++ b/packages/catalog/test/gateway-reference.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, test } from "bun:test"; +import { getBundledModelReferenceIndex } from "../src/identity/bundled"; +import { inheritReferenceThinking, resolveModelReference } from "../src/identity/reference"; + +describe("Portkey gateway model references", () => { + test("@modal ids do not fuzzy-match bundled catalog entries", () => { + const index = getBundledModelReferenceIndex(); + expect(resolveModelReference("@modal/GLM-5-2-FP8", index)).toBeUndefined(); + }); + + test("cross-provider references do not inherit wire routing thinking", () => { + const index = getBundledModelReferenceIndex(); + const devinGlm = resolveModelReference("glm-5-2", index); + expect(devinGlm?.provider).toBe("devin"); + expect(inheritReferenceThinking(undefined, devinGlm, "doordash")).toBeUndefined(); + }); +}); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 6f32dc8851b..8edece29e5f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed Portkey/gateway custom models whose ids start with `@` (e.g. `@modal/GLM-5-2-FP8`) being rewritten to unrelated bundled wire ids (e.g. `glm-5-2`), which caused `400` responses requiring `x-portkey-config` or `x-portkey-provider`. + ## [17.0.5] - 2026-07-18 ### Added diff --git a/packages/coding-agent/src/config/model-discovery.ts b/packages/coding-agent/src/config/model-discovery.ts index d3a8d534511..2f7335c163d 100644 --- a/packages/coding-agent/src/config/model-discovery.ts +++ b/packages/coding-agent/src/config/model-discovery.ts @@ -10,6 +10,7 @@ import type { Api, Model, RemoteCompactionConfig } from "@oh-my-pi/pi-ai/types"; import { buildModel } from "@oh-my-pi/pi-catalog/build"; import { getBundledModelReferenceIndex, + inheritReferenceThinking, isQwenModelId, resolveModelReference, stripBracketedModelIdAffixes, @@ -752,7 +753,7 @@ export async function discoverOpenAIModelsList( provider: providerConfig.provider, baseUrl, reasoning: reference?.reasoning ?? false, - thinking: reference?.thinking, + thinking: inheritReferenceThinking(undefined, reference, providerConfig.provider), input: nativeMetadataForModel?.input ?? reference?.input ?? ["text"], ...(providerConfig.discovery.type === "lm-studio" ? { imageInputDecoder: "stb" as const } : {}), // Proxy/gateway pricing is provider-specific and rarely matches @@ -907,7 +908,7 @@ export async function discoverProxyModels( provider: providerConfig.provider, baseUrl, reasoning: reference?.reasoning ?? false, - thinking: reference?.thinking, + thinking: inheritReferenceThinking(undefined, reference, providerConfig.provider), input: reference?.input ?? ["text"], // Proxy pricing is provider-specific and usually does not match // upstream bundled catalogs, so keep costs local-unknown even when diff --git a/packages/coding-agent/src/config/model-registry.ts b/packages/coding-agent/src/config/model-registry.ts index 62c8492869a..bec1c4ca93a 100644 --- a/packages/coding-agent/src/config/model-registry.ts +++ b/packages/coding-agent/src/config/model-registry.ts @@ -64,7 +64,11 @@ const BUILT_IN_DISCOVERY_NON_AUTHORITATIVE_RETRY_MS = 5 * 60 * 1000; import type { ApiKeyResolver, FetchImpl } from "@oh-my-pi/pi-ai"; import { registerOAuthProvider, unregisterOAuthProviders } from "@oh-my-pi/pi-ai/oauth"; import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai/oauth/types"; -import { getBundledModelReferenceIndex, resolveModelReference } from "@oh-my-pi/pi-catalog/identity"; +import { + getBundledModelReferenceIndex, + inheritReferenceThinking, + resolveModelReference, +} from "@oh-my-pi/pi-catalog/identity"; import { isBunTestRuntime, isRecord, logger, wrapFetchForExtraCa } from "@oh-my-pi/pi-utils"; import { parseModelString, resolveProviderModelReference } from "../config/model-resolver"; import type { AuthStorage, OAuthCredential } from "../session/auth-storage"; @@ -667,7 +671,7 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil provider: resolvedModel.provider, baseUrl: resolvedModel.baseUrl, reasoning: resolvedModel.reasoning ?? reference?.reasoning ?? (options.useDefaults ? false : undefined), - thinking: resolvedModel.thinking ?? reference?.thinking, + thinking: inheritReferenceThinking(resolvedModel.thinking, reference, resolvedModel.provider), input: input as ("text" | "image")[], ...(supportsTools !== undefined ? { supportsTools } : {}), cost, diff --git a/packages/coding-agent/test/issue-portkey-gateway-wire-model.test.ts b/packages/coding-agent/test/issue-portkey-gateway-wire-model.test.ts new file mode 100644 index 00000000000..5e1db56e9d1 --- /dev/null +++ b/packages/coding-agent/test/issue-portkey-gateway-wire-model.test.ts @@ -0,0 +1,67 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { resolveWireModelId } from "@oh-my-pi/pi-catalog/model-thinking"; +import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry"; +import { resetSettingsForTest } from "@oh-my-pi/pi-coding-agent/config/settings"; +import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage"; +import { removeSyncWithRetries, Snowflake } from "@oh-my-pi/pi-utils"; + +describe("Portkey gateway custom models", () => { + let tempDir: string; + let modelsPath: string; + let authStorage: AuthStorage; + + beforeEach(async () => { + resetSettingsForTest(); + tempDir = path.join(os.tmpdir(), `pi-test-portkey-gateway-${Snowflake.next()}`); + fs.mkdirSync(tempDir, { recursive: true }); + modelsPath = path.join(tempDir, "models.yml"); + authStorage = await AuthStorage.create(":memory:"); + }); + + afterEach(() => { + resetSettingsForTest(); + authStorage.close(); + if (tempDir && fs.existsSync(tempDir)) { + removeSyncWithRetries(tempDir); + } + }); + + test("doordash @modal/GLM-5-2-FP8 keeps full wire id (not devin glm-5-2)", () => { + fs.writeFileSync( + modelsPath, + `providers: + doordash: + baseUrl: https://cybertron-service-gateway.doordash.team/v1 + api: openai-completions + apiKey: test + authHeader: true + headers: + x-portkey-api-key: test + models: + - id: "@modal/GLM-5-2-FP8" + name: glm-5p2 (modal) + reasoning: true + input: [text] + contextWindow: 1048576 + maxTokens: 131072 + compat: + thinkingFormat: openai + supportsReasoningEffort: true + reasoningEffortMap: + minimal: none + low: low + medium: medium + high: high + xhigh: max +`, + ); + const registry = new ModelRegistry(authStorage, modelsPath); + const model = registry.find("doordash", "@modal/GLM-5-2-FP8"); + expect(model).toBeDefined(); + expect(resolveWireModelId(model!, "high")).toBe("@modal/GLM-5-2-FP8"); + expect(resolveWireModelId(model!, "off")).toBe("@modal/GLM-5-2-FP8"); + }); +});