mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
DefaultExecutor.buildUrl()'s "zai"/"glm-coding-apikey" case always returned the Anthropic Messages URL, ignoring a per-model targetFormat override (custom-model dropdown, #2905) that resolves to "openai" — e.g. for a vision model like glm-4.6v. chatCore/executionCredentials.ts now threads the resolved override onto providerSpecificData.targetFormat so buildUrl (via the new default/zaiFormatOverride.ts helper, extracted to respect the file-size ratchet) can route to the OpenAI-compatible endpoint instead. Separately, custom-model id lookup (lookupCustomModelMeta in src/sse/services/model.ts, getCustomModelRow in src/lib/db/models.ts) did an exact, case-sensitive match, so a model saved as "glm-4.6v" was invisible when looked up as "glm-4.6V". Both now fall back to a case-insensitive match after the exact match fails. Regression tests: tests/unit/zai-glm-target-format-override.test.ts (reused from the triage plan-file's RED probe) and tests/unit/zai-execution-credentials-target-format-7364.test.ts (production wiring in executionCredentials.ts). Gates run: check-file-size, check-complexity, check-cognitive-complexity, typecheck:core, eslint (suppressions), tests/unit/zai-glm-target-format-override.test.ts, tests/unit/zai-execution-credentials-target-format-7364.test.ts, tests/unit/executor-default-base.test.ts, tests/unit/custom-model-target-format.test.ts, tests/unit/chatcore-execution-credentials.test.ts, tests/unit/chatcore-target-format.test.ts, tests/unit/model-resolver.test.ts, tests/unit/model-alias-provider-resolution.test.ts, tests/unit/combo-custom-provider-resolution.test.ts — all green. Refs #7364
This commit is contained in:
committed by
GitHub
parent
6459dde35c
commit
265d00c0e1
1
changelog.d/fixes/7364-zai-glm-target-format.md
Normal file
1
changelog.d/fixes/7364-zai-glm-target-format.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(sse): honor per-model targetFormat override for zai/glm-coding-apikey buildUrl and make custom-model id lookup case-insensitive (#7364)
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
normalizeGigachatChatUrl,
|
||||
} from "@/lib/providers/validation/urlHelpers";
|
||||
import { forwardOpencodeClientHeaders } from "../utils/opencodeHeaders.ts";
|
||||
import { resolveZaiUrl } from "./default/zaiFormatOverride.ts";
|
||||
|
||||
import type { PoolConfig } from "../services/sessionPool/types.ts";
|
||||
|
||||
@@ -242,10 +243,9 @@ export class DefaultExecutor extends BaseExecutor {
|
||||
return normalizeOpenAIChatUrl(baseUrl);
|
||||
}
|
||||
case "zai":
|
||||
case "glm-coding-apikey": {
|
||||
const zaiBaseUrl = this.resolveBaseUrl(credentials);
|
||||
return `${zaiBaseUrl}?beta=true`;
|
||||
}
|
||||
case "glm-coding-apikey":
|
||||
// #7364: format override extracted to zaiFormatOverride.ts (file-size ratchet).
|
||||
return resolveZaiUrl(credentials, (fallback) => this.resolveBaseUrl(credentials, fallback));
|
||||
case "claude":
|
||||
case "glm":
|
||||
case "glmt":
|
||||
|
||||
25
open-sse/executors/default/zaiFormatOverride.ts
Normal file
25
open-sse/executors/default/zaiFormatOverride.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { GLM_DEFAULT_BASE_URLS } from "../../config/glmProvider.ts";
|
||||
|
||||
type ZaiCredentialsLike = {
|
||||
providerSpecificData?: { targetFormat?: unknown } | null;
|
||||
} | null;
|
||||
|
||||
/**
|
||||
* #7364: "zai"/"glm-coding-apikey" default to the Anthropic Messages wire format
|
||||
* (registry format:"claude"), but a per-model `targetFormat` override (custom-model
|
||||
* dropdown, #2905) can resolve to "openai" — e.g. for a vision model like glm-4.6v
|
||||
* that the operator wants routed through the OpenAI-compatible endpoint instead.
|
||||
* chatCore/executionCredentials.ts threads that resolved override onto
|
||||
* `providerSpecificData.targetFormat`; DefaultExecutor.buildUrl() has no other way
|
||||
* to see it, so without this check every zai/glm-coding-apikey request silently hit
|
||||
* the Claude-format endpoint regardless of the override.
|
||||
*/
|
||||
export function resolveZaiUrl(
|
||||
credentials: ZaiCredentialsLike,
|
||||
resolveBaseUrl: (fallback?: string) => string
|
||||
): string {
|
||||
if (credentials?.providerSpecificData?.targetFormat === "openai") {
|
||||
return resolveBaseUrl(GLM_DEFAULT_BASE_URLS.international);
|
||||
}
|
||||
return `${resolveBaseUrl()}?beta=true`;
|
||||
}
|
||||
@@ -55,6 +55,16 @@ export function resolveExecutionCredentials(opts: {
|
||||
providerSpecificData._omnirouteForceResponsesUpstream = true;
|
||||
}
|
||||
|
||||
// #7364: "zai"/"glm-coding-apikey" default to the Anthropic Messages wire format
|
||||
// (registry format:"claude"), but a per-model targetFormat override (custom-model
|
||||
// dropdown, #2905) can resolve targetFormat to "openai" — e.g. for a vision model
|
||||
// like glm-4.6v that the operator wants routed through the OpenAI-compatible
|
||||
// endpoint. DefaultExecutor.buildUrl()'s "zai" branch has no other way to see that
|
||||
// override, so surface it on providerSpecificData for buildUrl to read.
|
||||
if (targetFormat === FORMATS.OPENAI && (provider === "zai" || provider === "glm-coding-apikey")) {
|
||||
providerSpecificData.targetFormat = targetFormat;
|
||||
}
|
||||
|
||||
const withApiType = {
|
||||
...nextCredentials,
|
||||
providerSpecificData,
|
||||
|
||||
@@ -705,10 +705,22 @@ function getCustomModelRow(providerId: string, modelId: string): JsonRecord | nu
|
||||
try {
|
||||
const models = JSON.parse(value) as unknown;
|
||||
if (!Array.isArray(models)) return null;
|
||||
const m = models.find((x: unknown) => {
|
||||
const isIdMatch = (x: unknown, id: string): boolean => {
|
||||
if (!x || typeof x !== "object" || Array.isArray(x)) return false;
|
||||
return (x as { id?: string }).id === modelId;
|
||||
}) as JsonRecord | undefined;
|
||||
return (x as { id?: string }).id === id;
|
||||
};
|
||||
// #7364: exact match first; case-insensitive fallback so "glm-4.6V" resolves a
|
||||
// custom model saved as "glm-4.6v" (see lookupCustomModelMeta in
|
||||
// src/sse/services/model.ts for the sibling lookup this mirrors).
|
||||
const m = (models.find((x: unknown) => isIdMatch(x, modelId)) ??
|
||||
models.find(
|
||||
(x: unknown) =>
|
||||
x &&
|
||||
typeof x === "object" &&
|
||||
!Array.isArray(x) &&
|
||||
typeof (x as { id?: string }).id === "string" &&
|
||||
((x as { id: string }).id as string).toLowerCase() === modelId.toLowerCase()
|
||||
)) as JsonRecord | undefined;
|
||||
return m ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -74,7 +74,15 @@ async function lookupCustomModelMeta(
|
||||
try {
|
||||
const models = await getCustomModels(providerId);
|
||||
if (!Array.isArray(models)) return {};
|
||||
const match = models.find((m: any) => m.id === modelId);
|
||||
// #7364: exact match first (preserves existing behavior/perf); fall back to a
|
||||
// case-insensitive match so a model saved as "glm-4.6v" is still found when the
|
||||
// caller (dashboard, combo target, direct call) requests "glm-4.6V" — several
|
||||
// reporters typed the uppercase "V" from Z.AI's own docs/marketing.
|
||||
const match =
|
||||
models.find((m: any) => m.id === modelId) ??
|
||||
models.find(
|
||||
(m: any) => typeof m.id === "string" && m.id.toLowerCase() === modelId.toLowerCase()
|
||||
);
|
||||
if (!match) return {};
|
||||
return {
|
||||
apiFormat: match.apiFormat === "responses" ? "responses" : undefined,
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// tests/unit/zai-execution-credentials-target-format-7364.test.ts
|
||||
// #7364 Defect A: resolveExecutionCredentials must thread a resolved "openai"
|
||||
// targetFormat onto providerSpecificData for the "zai"/"glm-coding-apikey" providers,
|
||||
// so DefaultExecutor.buildUrl()'s zai branch (open-sse/executors/default/zaiFormatOverride.ts)
|
||||
// can see the per-model custom-model targetFormat override (#2905) and route to the
|
||||
// OpenAI-compatible endpoint instead of the default Anthropic Messages URL.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { resolveExecutionCredentials } from "../../open-sse/handlers/chatCore/executionCredentials.ts";
|
||||
|
||||
const base = {
|
||||
credentials: { providerSpecificData: { foo: "bar" } } as Record<string, unknown>,
|
||||
nativeCodexPassthrough: false,
|
||||
endpointPath: "/v1/messages",
|
||||
ccSessionId: null,
|
||||
};
|
||||
|
||||
test("zai + resolved openai targetFormat threads providerSpecificData.targetFormat", () => {
|
||||
const out = resolveExecutionCredentials({
|
||||
...base,
|
||||
provider: "zai",
|
||||
targetFormat: "openai",
|
||||
}) as Record<string, unknown>;
|
||||
assert.deepEqual(out.providerSpecificData, { foo: "bar", targetFormat: "openai" });
|
||||
});
|
||||
|
||||
test("glm-coding-apikey + resolved openai targetFormat threads providerSpecificData.targetFormat", () => {
|
||||
const out = resolveExecutionCredentials({
|
||||
...base,
|
||||
provider: "glm-coding-apikey",
|
||||
targetFormat: "openai",
|
||||
}) as Record<string, unknown>;
|
||||
assert.deepEqual(out.providerSpecificData, { foo: "bar", targetFormat: "openai" });
|
||||
});
|
||||
|
||||
test("zai + default claude targetFormat does NOT inject a targetFormat override", () => {
|
||||
const out = resolveExecutionCredentials({
|
||||
...base,
|
||||
provider: "zai",
|
||||
targetFormat: "claude",
|
||||
}) as Record<string, unknown>;
|
||||
assert.deepEqual(out.providerSpecificData, { foo: "bar" });
|
||||
});
|
||||
|
||||
test("unrelated provider (openai) with targetFormat=openai is untouched by the zai branch", () => {
|
||||
const out = resolveExecutionCredentials({
|
||||
...base,
|
||||
provider: "openai",
|
||||
targetFormat: "openai",
|
||||
}) as Record<string, unknown>;
|
||||
assert.deepEqual(out.providerSpecificData, { foo: "bar" });
|
||||
});
|
||||
54
tests/unit/zai-glm-target-format-override.test.ts
Normal file
54
tests/unit/zai-glm-target-format-override.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-7364-zai-target-format-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const modelsDb = await import("../../src/lib/db/models.ts");
|
||||
const { getModelInfo } = await import("../../src/sse/services/model.ts");
|
||||
const { DefaultExecutor } = await import("../../open-sse/executors/default.ts");
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("#7364 Defect A (URL): DefaultExecutor.buildUrl('zai', ...) ignores a per-model targetFormat:'openai' override and still returns the Anthropic Messages URL", () => {
|
||||
const executor = new DefaultExecutor("zai");
|
||||
const credentialsWithOpenAIOverride = {
|
||||
apiKey: "test-key",
|
||||
providerSpecificData: { targetFormat: "openai" },
|
||||
};
|
||||
const url = executor.buildUrl("glm-4.6v", false, 0, credentialsWithOpenAIOverride);
|
||||
assert.notEqual(
|
||||
url,
|
||||
"https://api.z.ai/api/anthropic/v1/messages?beta=true",
|
||||
"BUG #7364 Defect A: an 'openai' targetFormat override must not hit the Anthropic Messages URL, but it does"
|
||||
);
|
||||
});
|
||||
|
||||
test("#7364 Defect A (case-sensitivity): a custom model saved as 'glm-4.6v' is not found when looked up as 'glm-4.6V'", async () => {
|
||||
await modelsDb.addCustomModel(
|
||||
"zai",
|
||||
"glm-4.6v",
|
||||
"GLM 4.6V (vision)",
|
||||
"manual",
|
||||
"chat-completions",
|
||||
["chat"],
|
||||
"openai" // explicit targetFormat override, mirroring the dashboard dropdown
|
||||
);
|
||||
|
||||
const exact = (await getModelInfo("zai/glm-4.6v")) as { targetFormat?: string };
|
||||
assert.equal(exact.targetFormat, "openai", "sanity check: exact-case lookup must surface the saved targetFormat");
|
||||
|
||||
const mixedCase = (await getModelInfo("zai/glm-4.6V")) as { targetFormat?: string };
|
||||
assert.equal(
|
||||
mixedCase.targetFormat,
|
||||
"openai",
|
||||
"BUG #7364 Defect A: case-mismatched lookup ('glm-4.6V' vs stored 'glm-4.6v') must still surface the targetFormat override, but it doesn't"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user