fix(catalog): preserve opaque gateway model ids
Co-authored-by: Matheus Gois <matheus.gois@doordash.com>
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user