Generate chat, task and learning models from agent-models.json. Use WaterNetwork fast/expert models with 272k context and validate upstream names before readiness. Add an isolated model check command with signal-safe cleanup and plugin, Skill and MCP management docs. Verified in isolation: type checks, 26 focused tests and both model catalog checks; no model inference requests.
95 lines
5.7 KiB
TypeScript
95 lines
5.7 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
|
|
import {
|
|
agentModelOptions,
|
|
isSupportedModel,
|
|
resolveDefaultModel,
|
|
supportedModels,
|
|
} from "../../src/chat/models.js";
|
|
import { compileOpencodeModels, getAgentModelOptions, parseAgentModelConfig, parseModelReference } from "../../src/chat/modelConfig.js";
|
|
import { serviceOpencodeConfig } from "../../src/runtime/opencode2-config.js";
|
|
import { modelCheckTargets } from "../../src/runtime/model-check.js";
|
|
|
|
describe("agent model config", () => {
|
|
it("keeps every exposed option in the supported model list", () => {
|
|
expect(agentModelOptions.map((model) => model.id)).toEqual(supportedModels);
|
|
expect(
|
|
agentModelOptions.every(
|
|
(model) => model.label && model.description && model.icon,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("validates supported model ids", () => {
|
|
expect(isSupportedModel(supportedModels[0]!)).toBe(true);
|
|
expect(isSupportedModel("unknown/model")).toBe(false);
|
|
});
|
|
|
|
it("rejects unsupported default models", () => {
|
|
expect(resolveDefaultModel(supportedModels[0]!)).toBe(supportedModels[0]!);
|
|
expect(() => resolveDefaultModel("unknown/model")).toThrow(
|
|
"unsupported default agent model",
|
|
);
|
|
});
|
|
|
|
it("generates native providers and the public catalog from one custom provider definition", () => {
|
|
const configuration = parseAgentModelConfig({
|
|
default_model: "custom/chat", task_model: "custom/org/model", learning_gate_model: "custom/chat",
|
|
providers: { custom: { name: "自定义服务", api_url: "https://example.com/v1", api_key: "{env:CUSTOM_API_KEY}", models: {
|
|
chat: { name: "自定义", description: "自定义模型", icon: "bolt", model_id: "wire-chat" },
|
|
"org/model": {}, hidden: { selectable: false }, image: { disabled: true, capabilities: { tools: false, output: ["image"] } },
|
|
} } },
|
|
});
|
|
const options = getAgentModelOptions(configuration);
|
|
expect(options.map((model) => model.id)).toEqual(["custom/chat", "custom/org/model"]);
|
|
expect(options[0]).toEqual({ id: "custom/chat", label: "自定义", description: "自定义模型", icon: "bolt" });
|
|
const native = compileOpencodeModels(configuration);
|
|
expect(native.providers.custom).toMatchObject({
|
|
env: ["CUSTOM_API_KEY"], package: "@opencode/ai/providers/openai-compatible",
|
|
settings: { baseURL: "https://example.com/v1", apiKey: "{env:CUSTOM_API_KEY}" },
|
|
models: { chat: { modelID: "wire-chat", capabilities: { tools: true } }, image: { disabled: true } },
|
|
});
|
|
expect(JSON.stringify(options)).not.toContain("CUSTOM_API_KEY");
|
|
expect(JSON.stringify(options)).not.toContain("example.com");
|
|
expect(modelCheckTargets(configuration).models.map((model) => model.id)).toEqual(["custom/chat", "custom/org/model", "custom/hidden"]);
|
|
expect(modelCheckTargets(configuration).models[0]!.wire_id).toBe("wire-chat");
|
|
const service = serviceOpencodeConfig({ model: "stale/model", providers: { stale: {} }, agents: { instruction: { model: "stale/model" } } }, configuration);
|
|
expect(service.model).toBe("custom/chat");
|
|
expect(service.agents.instruction.model).toBe("custom/chat");
|
|
expect(Object.keys(service.providers)).toEqual(["custom"]);
|
|
});
|
|
|
|
it("rejects unknown, disabled or non-tool role models and invalid endpoints without echoing keys", () => {
|
|
const base = { default_model: "custom/chat", providers: { custom: { name: "Custom", api_url: "https://example.com/v1", api_key: "TEST_ONLY_SECRET", models: { chat: {} } } } };
|
|
for (const role of ["default_model", "task_model", "learning_gate_model"]) expect(() => parseAgentModelConfig({ ...base, [role]: "custom/missing" })).toThrow("enabled configured text model");
|
|
for (const model of [{ disabled: true }, { capabilities: { tools: false } }, { capabilities: { output: ["image"] } }]) {
|
|
expect(() => parseAgentModelConfig({ ...base, providers: { custom: { ...base.providers.custom, models: { chat: model } } } })).toThrow();
|
|
}
|
|
for (const url of ["file:///secret", "https://user:TEST_ONLY_SECRET@example.com/v1", "https://example.com/v1?key=TEST_ONLY_SECRET"]) {
|
|
try { parseAgentModelConfig({ ...base, providers: { custom: { ...base.providers.custom, api_url: url } } }); throw new Error("accepted invalid URL"); }
|
|
catch (error) { expect(String(error)).not.toContain("TEST_ONLY_SECRET"); expect(String(error)).toContain("api_url"); }
|
|
}
|
|
});
|
|
|
|
it("keeps namespaced model IDs intact when splitting runtime references", () => {
|
|
expect(parseModelReference("custom/org/model")).toEqual({ providerID: "custom", modelID: "org/model" });
|
|
for (const invalid of ["model", "/model", "provider/", "provider/model name", "provider/model#variant"]) expect(() => parseModelReference(invalid)).toThrow();
|
|
});
|
|
|
|
it("exposes only the two configured gateway models with their roles, icons and context limits", () => {
|
|
expect(supportedModels).toEqual(["waternetwork/gpt-6-luna", "waternetwork/gpt-6.1-sol"]);
|
|
expect(agentModelOptions.map(({ id, label, icon }) => ({ id, label, icon }))).toEqual([
|
|
{ id: "waternetwork/gpt-6-luna", label: "快速", icon: "bolt" },
|
|
{ id: "waternetwork/gpt-6.1-sol", label: "专家", icon: "sparkle" },
|
|
]);
|
|
expect(isSupportedModel("deepseek/deepseek-flash")).toBe(false);
|
|
expect(isSupportedModel("waternetwork/deepseek-flash")).toBe(false);
|
|
expect(isSupportedModel("waternetwork/gpt-image-2.5")).toBe(false);
|
|
expect(isSupportedModel("waternetwork/codex-auto-review")).toBe(false);
|
|
const native = serviceOpencodeConfig({});
|
|
expect(Object.keys(native.providers)).toEqual(["waternetwork"]);
|
|
expect(native.model).toBe("waternetwork/gpt-6-luna");
|
|
for (const model of Object.values(native.providers.waternetwork.models)) expect(model.limit?.context).toBe(272000);
|
|
});
|
|
});
|