Skip to content

Commit 224db1a

Browse files
lidge-junLucinegogo
andcommitted
test(providers): cover the BigModel contract and the id-collision trap
Adds focused coverage for zhipu-bigmodel: the provider contract, the thinking-toggle request shaping (both the enabled and disabled halves, built through the real adapter), and the derived key-login/preset/bundle surfaces. Also generalizes the directory isolation rule. The existing guard only checked directory rows with supportLevel "reference", so re-registering a CONNECTABLE id against a different host stayed green — which is why #536 could propose `glm` for open.bigmodel.cn while `glm` was already bound to api.z.ai. The new case fails on exactly that shape. Mutation-verified: reverting the id to `glm` fails all four new tests plus the generalized guard, and removing thinkingToggleModels fails only the request-shaping test. Co-authored-by: Lucinegogo <103441383+Lucinegogo@users.noreply.github.com>
1 parent cf0a323 commit 224db1a

2 files changed

Lines changed: 137 additions & 0 deletions

File tree

tests/provider-registry-parity.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,23 @@ describe("free-provider directory isolation", () => {
812812
}
813813
});
814814

815+
test("an id shared by both lists must resolve to the same endpoint", () => {
816+
// The rule above only covers `reference` rows, so a CONNECTABLE directory id could be
817+
// re-registered against a different host and stay green — that is exactly what #536 proposed
818+
// for `glm` (directory: api.z.ai, proposed registry: open.bigmodel.cn). routedProviderConfig()
819+
// canonicalizes a saved provider onto the registry baseUrl, so the user's key would have gone
820+
// to the other vendor on the next request. Sharing an id is fine; disagreeing on where it
821+
// points is not.
822+
const registryById = new Map(PROVIDER_REGISTRY.map(entry => [entry.id, entry]));
823+
const shared = FREE_PROVIDER_DIRECTORY.filter(row => registryById.has(row.id));
824+
expect(shared.length).toBeGreaterThan(0);
825+
826+
for (const row of shared) {
827+
const registryEntry = registryById.get(row.id)!;
828+
expect(`${row.id} -> ${registryEntry.baseUrl}`).toBe(`${row.id} -> ${row.baseUrl}`);
829+
}
830+
});
831+
815832
test("a custom provider named after a directory entry keeps its own destination", () => {
816833
const config: OcxConfig = {
817834
port: 10100,
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { createOpenAIChatAdapter } from "../src/adapters/openai-chat";
3+
import { KEY_LOGIN_PROVIDERS } from "../src/oauth/key-providers";
4+
import { deriveJawcodeAliases, deriveProviderPresets } from "../src/providers/derive";
5+
import { FREE_PROVIDER_DIRECTORY } from "../src/providers/free-directory";
6+
import { PROVIDER_REGISTRY } from "../src/providers/registry";
7+
import { routeModel } from "../src/router";
8+
import type { OcxConfig } from "../src/types";
9+
10+
const BASE_URL = "https://open.bigmodel.cn/api/paas/v4";
11+
12+
function bigmodelConfig(): OcxConfig {
13+
return {
14+
port: 10100,
15+
defaultProvider: "zhipu-bigmodel",
16+
providers: {
17+
"zhipu-bigmodel": {
18+
adapter: "openai-chat",
19+
baseUrl: BASE_URL,
20+
apiKey: "test-key",
21+
},
22+
},
23+
};
24+
}
25+
26+
async function buildBody(modelRef: string, reasoning: string): Promise<Record<string, unknown>> {
27+
const route = routeModel(bigmodelConfig(), modelRef);
28+
const adapter = createOpenAIChatAdapter(route.provider);
29+
const request = await adapter.buildRequest({
30+
modelId: route.modelId,
31+
context: { messages: [{ role: "user", content: "hi" }] },
32+
stream: true,
33+
options: { reasoning },
34+
});
35+
return JSON.parse(request.body as string) as Record<string, unknown>;
36+
}
37+
38+
describe("Zhipu BigModel provider", () => {
39+
test("publishes the domestic pay-as-you-go contract", () => {
40+
const entry = PROVIDER_REGISTRY.find(provider => provider.id === "zhipu-bigmodel");
41+
expect(entry).toMatchObject({
42+
label: "Zhipu AI — BigModel",
43+
baseUrl: BASE_URL,
44+
adapter: "openai-chat",
45+
authKind: "key",
46+
dashboardUrl: "https://bigmodel.cn/console/usercenter/apikeys",
47+
defaultModel: "glm-4.6",
48+
jawcodeBundle: "zai",
49+
});
50+
51+
// Without this the catalog falls back to a generic 128k window and Codex compacts
52+
// ~76,800 tokens early on the default model.
53+
expect(entry?.modelContextWindows?.["glm-4.6"]).toBe(204_800);
54+
55+
// Modalities are declared per model. `noVisionModels` is deliberately unused here: in this
56+
// repository it ADDS image input to route attachments through the proxy's vision sidecar
57+
// (src/codex/catalog/provider-fetch.ts), a coverage claim nobody verified for this host.
58+
expect(entry?.modelInputModalities?.["glm-4.6"]).toEqual(["text"]);
59+
expect(entry?.modelInputModalities?.["glm-4.6v"]).toEqual(["text", "image"]);
60+
expect(entry?.noVisionModels).toBeUndefined();
61+
62+
// A live-discovery claim we have not seen answer would produce an empty picker at runtime.
63+
expect(entry?.liveModels).toBeUndefined();
64+
expect(entry?.models).toContain("glm-4.6v");
65+
});
66+
67+
test("does not claim an id that already resolves somewhere else", () => {
68+
const entry = PROVIDER_REGISTRY.find(provider => provider.id === "zhipu-bigmodel");
69+
expect(entry).toBeDefined();
70+
71+
// `glm` and `glm-cn` are bound in the free-provider directory to Z.AI and to the BigModel
72+
// CODING path respectively. Registering either id here would let routedProviderConfig()
73+
// canonicalize a saved config onto this baseUrl and send its API key to another host.
74+
const directoryIds = new Set(FREE_PROVIDER_DIRECTORY.map(row => row.id));
75+
expect(directoryIds.has("glm")).toBe(true);
76+
expect(directoryIds.has("glm-cn")).toBe(true);
77+
expect(directoryIds.has("zhipu-bigmodel")).toBe(false);
78+
79+
// A saved `glm` provider keeps its own destination, unaffected by this registry entry.
80+
const config: OcxConfig = {
81+
port: 10100,
82+
defaultProvider: "glm",
83+
providers: {
84+
glm: {
85+
adapter: "openai-chat",
86+
baseUrl: "https://api.z.ai/api/coding/paas/v4",
87+
apiKey: "zai-key",
88+
},
89+
},
90+
};
91+
expect(routeModel(config, "glm/glm-4.6").provider.baseUrl).toBe("https://api.z.ai/api/coding/paas/v4");
92+
});
93+
94+
test("maps reasoning effort onto the GLM thinking toggle instead of reasoning_effort", async () => {
95+
// GLM exposes a binary thinking knob; these models reject reasoning_effort outright.
96+
const high = await buildBody("zhipu-bigmodel/glm-4.6", "high");
97+
expect(high.thinking).toEqual({ type: "enabled" });
98+
expect(high.reasoning_effort).toBeUndefined();
99+
100+
// The disabled half matters: a one-sided assertion still passes when the map is stuck.
101+
const low = await buildBody("zhipu-bigmodel/glm-4.6", "low");
102+
expect(low.thinking).toEqual({ type: "disabled" });
103+
expect(low.reasoning_effort).toBeUndefined();
104+
});
105+
106+
test("derives key login, GUI preset, and metadata bundle from the registry", () => {
107+
expect(KEY_LOGIN_PROVIDERS["zhipu-bigmodel"]).toMatchObject({
108+
label: "Zhipu AI — BigModel",
109+
adapter: "openai-chat",
110+
baseUrl: BASE_URL,
111+
defaultModel: "glm-4.6",
112+
});
113+
expect(KEY_LOGIN_PROVIDERS["zhipu-bigmodel"]).not.toHaveProperty("liveModels");
114+
expect(deriveProviderPresets().find(preset => preset.id === "zhipu-bigmodel")).toMatchObject({
115+
auth: "key",
116+
defaultModel: "glm-4.6",
117+
});
118+
expect(deriveJawcodeAliases()["zhipu-bigmodel"]).toBe("zai");
119+
});
120+
});

0 commit comments

Comments
 (0)