fix(catalog): preserve opaque gateway model ids

Co-authored-by: Matheus Gois <matheus.gois@doordash.com>
This commit is contained in:
can1357
2026-07-21 21:46:09 +02:00
parent fe415b5471
commit 5b798685ef
6 changed files with 118 additions and 5 deletions
+20 -1
View File
@@ -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: Pick<Model<Api>, "provider" | "thinking"> | 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<Api> | 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);
@@ -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, "gateway")).toBeUndefined();
});
});
+4
View File
@@ -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.6] - 2026-07-20
- Fixed failed plan-mode exits leaving the session on the restored execution model while plan mode remained active and silently changing ambient `xd://` tool presentation; rollback now restores the plan model, thinking level, and exact top-level-versus-mounted tool partition so exit can be retried safely ([#6013](https://github.com/can1357/oh-my-pi/pull/6013)).
@@ -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
@@ -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,
@@ -0,0 +1,68 @@
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 { Effort } from "@oh-my-pi/pi-catalog/effort";
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("gateway @modal/GLM-5-2-FP8 keeps full wire id (not devin glm-5-2)", () => {
fs.writeFileSync(
modelsPath,
`providers:
gateway:
baseUrl: https://gateway.example.com/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("gateway", "@modal/GLM-5-2-FP8");
expect(model).toBeDefined();
expect(resolveWireModelId(model!, Effort.High)).toBe("@modal/GLM-5-2-FP8");
expect(resolveWireModelId(model!, undefined)).toBe("@modal/GLM-5-2-FP8");
});
});