fix: normalize z.ai provider ids in auth profiles
This commit is contained in:
committed by
Peter Steinberger
parent
13c1ce1f05
commit
3550dc294d
@@ -89,6 +89,67 @@ describe("resolveAuthProfileOrder", () => {
|
||||
expect(order).toEqual(["anthropic:work", "anthropic:default"]);
|
||||
});
|
||||
|
||||
it("normalizes z.ai aliases in auth.order", () => {
|
||||
const order = resolveAuthProfileOrder({
|
||||
cfg: {
|
||||
auth: {
|
||||
order: { "z.ai": ["zai:work", "zai:default"] },
|
||||
profiles: {
|
||||
"zai:default": { provider: "zai", mode: "api_key" },
|
||||
"zai:work": { provider: "zai", mode: "api_key" },
|
||||
},
|
||||
},
|
||||
},
|
||||
store: {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"zai:default": {
|
||||
type: "api_key",
|
||||
provider: "zai",
|
||||
key: "sk-default",
|
||||
},
|
||||
"zai:work": {
|
||||
type: "api_key",
|
||||
provider: "zai",
|
||||
key: "sk-work",
|
||||
},
|
||||
},
|
||||
},
|
||||
provider: "zai",
|
||||
});
|
||||
expect(order).toEqual(["zai:work", "zai:default"]);
|
||||
});
|
||||
|
||||
it("normalizes z.ai aliases in auth.profiles", () => {
|
||||
const order = resolveAuthProfileOrder({
|
||||
cfg: {
|
||||
auth: {
|
||||
profiles: {
|
||||
"zai:default": { provider: "z.ai", mode: "api_key" },
|
||||
"zai:work": { provider: "Z.AI", mode: "api_key" },
|
||||
},
|
||||
},
|
||||
},
|
||||
store: {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"zai:default": {
|
||||
type: "api_key",
|
||||
provider: "zai",
|
||||
key: "sk-default",
|
||||
},
|
||||
"zai:work": {
|
||||
type: "api_key",
|
||||
provider: "zai",
|
||||
key: "sk-work",
|
||||
},
|
||||
},
|
||||
},
|
||||
provider: "zai",
|
||||
});
|
||||
expect(order).toEqual(["zai:default", "zai:work"]);
|
||||
});
|
||||
|
||||
it("prioritizes oauth profiles when order missing", () => {
|
||||
const mixedStore: AuthProfileStore = {
|
||||
version: 1,
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { resolveOAuthPath } from "../config/paths.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { resolveClawdbotAgentDir } from "./agent-paths.js";
|
||||
import { normalizeProviderId } from "./model-selection.js";
|
||||
|
||||
const AUTH_STORE_VERSION = 1;
|
||||
const AUTH_PROFILE_FILENAME = "auth-profiles.json";
|
||||
@@ -378,8 +379,9 @@ export function listProfilesForProvider(
|
||||
store: AuthProfileStore,
|
||||
provider: string,
|
||||
): string[] {
|
||||
const providerKey = normalizeProviderId(provider);
|
||||
return Object.entries(store.profiles)
|
||||
.filter(([, cred]) => cred.provider === provider)
|
||||
.filter(([, cred]) => normalizeProviderId(cred.provider) === providerKey)
|
||||
.map(([id]) => id);
|
||||
}
|
||||
|
||||
@@ -539,17 +541,26 @@ export function resolveAuthProfileOrder(params: {
|
||||
preferredProfile?: string;
|
||||
}): string[] {
|
||||
const { cfg, store, provider, preferredProfile } = params;
|
||||
const configuredOrder = cfg?.auth?.order?.[provider];
|
||||
const providerKey = normalizeProviderId(provider);
|
||||
const configuredOrder =
|
||||
cfg?.auth?.order?.[providerKey] ??
|
||||
cfg?.auth?.order?.[provider] ??
|
||||
(providerKey === "zai"
|
||||
? (cfg?.auth?.order?.["z.ai"] ?? cfg?.auth?.order?.["z-ai"])
|
||||
: undefined);
|
||||
const explicitProfiles = cfg?.auth?.profiles
|
||||
? Object.entries(cfg.auth.profiles)
|
||||
.filter(([, profile]) => profile.provider === provider)
|
||||
.filter(
|
||||
([, profile]) =>
|
||||
normalizeProviderId(profile.provider) === providerKey,
|
||||
)
|
||||
.map(([profileId]) => profileId)
|
||||
: [];
|
||||
const baseOrder =
|
||||
configuredOrder ??
|
||||
(explicitProfiles.length > 0
|
||||
? explicitProfiles
|
||||
: listProfilesForProvider(store, provider));
|
||||
: listProfilesForProvider(store, providerKey));
|
||||
if (baseOrder.length === 0) return [];
|
||||
|
||||
const filtered = baseOrder.filter((profileId) => {
|
||||
|
||||
@@ -163,4 +163,40 @@ describe("getApiKeyForModel", () => {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("throws when ZAI API key is missing", async () => {
|
||||
const previousZai = process.env.ZAI_API_KEY;
|
||||
const previousLegacy = process.env.Z_AI_API_KEY;
|
||||
|
||||
try {
|
||||
delete process.env.ZAI_API_KEY;
|
||||
delete process.env.Z_AI_API_KEY;
|
||||
|
||||
vi.resetModules();
|
||||
const { resolveApiKeyForProvider } = await import("./model-auth.js");
|
||||
|
||||
let error: unknown = null;
|
||||
try {
|
||||
await resolveApiKeyForProvider({
|
||||
provider: "zai",
|
||||
store: { version: 1, profiles: {} },
|
||||
});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(String(error)).toContain('No API key found for provider "zai".');
|
||||
} finally {
|
||||
if (previousZai === undefined) {
|
||||
delete process.env.ZAI_API_KEY;
|
||||
} else {
|
||||
process.env.ZAI_API_KEY = previousZai;
|
||||
}
|
||||
if (previousLegacy === undefined) {
|
||||
delete process.env.Z_AI_API_KEY;
|
||||
} else {
|
||||
process.env.Z_AI_API_KEY = previousLegacy;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,10 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
|
||||
import { resolveConfiguredModelRef } from "./model-selection.js";
|
||||
import {
|
||||
normalizeProviderId,
|
||||
resolveConfiguredModelRef,
|
||||
} from "./model-selection.js";
|
||||
|
||||
describe("resolveConfiguredModelRef", () => {
|
||||
it("parses provider/model from agent.model.primary", () => {
|
||||
@@ -129,3 +132,15 @@ describe("resolveConfiguredModelRef", () => {
|
||||
expect(resolved).toEqual({ provider: "zai", model: "glm-4.7" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeProviderId", () => {
|
||||
it("normalizes z.ai aliases to canonical zai", () => {
|
||||
expect(normalizeProviderId("z.ai")).toBe("zai");
|
||||
expect(normalizeProviderId("z-ai")).toBe("zai");
|
||||
});
|
||||
|
||||
it("normalizes provider casing", () => {
|
||||
expect(normalizeProviderId("OpenAI")).toBe("openai");
|
||||
expect(normalizeProviderId("Z.AI")).toBe("zai");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ export function modelKey(provider: string, model: string) {
|
||||
return `${provider}/${model}`;
|
||||
}
|
||||
|
||||
function normalizeProvider(provider: string): string {
|
||||
export function normalizeProviderId(provider: string): string {
|
||||
const normalized = provider.trim().toLowerCase();
|
||||
if (normalized === "z.ai" || normalized === "z-ai") return "zai";
|
||||
return normalized;
|
||||
@@ -35,10 +35,10 @@ export function parseModelRef(
|
||||
if (!trimmed) return null;
|
||||
const slash = trimmed.indexOf("/");
|
||||
if (slash === -1) {
|
||||
return { provider: normalizeProvider(defaultProvider), model: trimmed };
|
||||
return { provider: normalizeProviderId(defaultProvider), model: trimmed };
|
||||
}
|
||||
const providerRaw = trimmed.slice(0, slash).trim();
|
||||
const provider = normalizeProvider(providerRaw);
|
||||
const provider = normalizeProviderId(providerRaw);
|
||||
const model = trimmed.slice(slash + 1).trim();
|
||||
if (!provider || !model) return null;
|
||||
return { provider, model };
|
||||
|
||||
@@ -156,4 +156,104 @@ describe("models list/status", () => {
|
||||
expect(payload.count).toBe(1);
|
||||
expect(payload.models[0]?.key).toBe("zai/glm-4.7");
|
||||
});
|
||||
|
||||
it("models list provider filter normalizes Z.AI alias casing", async () => {
|
||||
loadConfig.mockReturnValue({ agent: { model: "z.ai/glm-4.7" } });
|
||||
const runtime = makeRuntime();
|
||||
|
||||
const models = [
|
||||
{
|
||||
provider: "zai",
|
||||
id: "glm-4.7",
|
||||
name: "GLM-4.7",
|
||||
input: ["text"],
|
||||
baseUrl: "https://api.z.ai/v1",
|
||||
contextWindow: 128000,
|
||||
},
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-4.1-mini",
|
||||
name: "GPT-4.1 mini",
|
||||
input: ["text"],
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
contextWindow: 128000,
|
||||
},
|
||||
];
|
||||
|
||||
discoverModels.mockReturnValue({
|
||||
getAll: () => models,
|
||||
getAvailable: () => models,
|
||||
});
|
||||
|
||||
const { modelsListCommand } = await import("./models/list.js");
|
||||
await modelsListCommand({ all: true, provider: "Z.AI", json: true }, runtime);
|
||||
|
||||
expect(runtime.log).toHaveBeenCalledTimes(1);
|
||||
const payload = JSON.parse(String(runtime.log.mock.calls[0]?.[0]));
|
||||
expect(payload.count).toBe(1);
|
||||
expect(payload.models[0]?.key).toBe("zai/glm-4.7");
|
||||
});
|
||||
|
||||
it("models list provider filter normalizes z-ai alias", async () => {
|
||||
loadConfig.mockReturnValue({ agent: { model: "z.ai/glm-4.7" } });
|
||||
const runtime = makeRuntime();
|
||||
|
||||
const models = [
|
||||
{
|
||||
provider: "zai",
|
||||
id: "glm-4.7",
|
||||
name: "GLM-4.7",
|
||||
input: ["text"],
|
||||
baseUrl: "https://api.z.ai/v1",
|
||||
contextWindow: 128000,
|
||||
},
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-4.1-mini",
|
||||
name: "GPT-4.1 mini",
|
||||
input: ["text"],
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
contextWindow: 128000,
|
||||
},
|
||||
];
|
||||
|
||||
discoverModels.mockReturnValue({
|
||||
getAll: () => models,
|
||||
getAvailable: () => models,
|
||||
});
|
||||
|
||||
const { modelsListCommand } = await import("./models/list.js");
|
||||
await modelsListCommand({ all: true, provider: "z-ai", json: true }, runtime);
|
||||
|
||||
expect(runtime.log).toHaveBeenCalledTimes(1);
|
||||
const payload = JSON.parse(String(runtime.log.mock.calls[0]?.[0]));
|
||||
expect(payload.count).toBe(1);
|
||||
expect(payload.models[0]?.key).toBe("zai/glm-4.7");
|
||||
});
|
||||
|
||||
it("models list marks auth as unavailable when ZAI key is missing", async () => {
|
||||
loadConfig.mockReturnValue({ agent: { model: "z.ai/glm-4.7" } });
|
||||
const runtime = makeRuntime();
|
||||
|
||||
const model = {
|
||||
provider: "zai",
|
||||
id: "glm-4.7",
|
||||
name: "GLM-4.7",
|
||||
input: ["text"],
|
||||
baseUrl: "https://api.z.ai/v1",
|
||||
contextWindow: 128000,
|
||||
};
|
||||
|
||||
discoverModels.mockReturnValue({
|
||||
getAll: () => [model],
|
||||
getAvailable: () => [],
|
||||
});
|
||||
|
||||
const { modelsListCommand } = await import("./models/list.js");
|
||||
await modelsListCommand({ all: true, json: true }, runtime);
|
||||
|
||||
expect(runtime.log).toHaveBeenCalledTimes(1);
|
||||
const payload = JSON.parse(String(runtime.log.mock.calls[0]?.[0]));
|
||||
expect(payload.models[0]?.available).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user