Skip to content
Closed

Testing #6141

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion packages/catalog/src/identity/reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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<TApi extends Api>(
modelThinking: ThinkingConfig | undefined,
reference: Model<TApi> | 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);
Expand Down
17 changes: 17 additions & 0 deletions packages/catalog/test/gateway-reference.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
4 changes: 4 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions packages/coding-agent/src/config/model-discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions packages/coding-agent/src/config/model-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
});
});