From 2bf1127af2bd5ab5be67ec8a258e25e25d6c4179 Mon Sep 17 00:00:00 2001 From: m4 Date: Tue, 21 Jul 2026 21:14:51 +0800 Subject: [PATCH] feat: new-provider templates in Registry Editor (design doc 7.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the six phase-1 templates (智谱 GLM, OpenAI, Anthropic, OpenAI-compatible, Anthropic-compatible, Ollama) that prefill adapter, Base URL, auth mode, and suggested models; saved providers remain ordinary profiles. Verified in browser against the live dev server. Co-Authored-By: Claude Opus 4.7 --- src/app/components/RegistryEditor.tsx | 63 +++++++++++++++------- src/lib/registryDraft.test.ts | 77 +++++++++++++++++++++++++++ src/lib/registryDraft.ts | 76 +++++++++++++++++++++++--- 3 files changed, 189 insertions(+), 27 deletions(-) create mode 100644 src/lib/registryDraft.test.ts diff --git a/src/app/components/RegistryEditor.tsx b/src/app/components/RegistryEditor.tsx index 8eecfde..19d7fe1 100644 --- a/src/app/components/RegistryEditor.tsx +++ b/src/app/components/RegistryEditor.tsx @@ -24,7 +24,8 @@ import { ID_PATTERN, modelRefKey, newModelDraft, - newProviderDraft, + PROVIDER_TEMPLATES, + providerDraftFromTemplate, registryDirty, supportedAuthModes, } from "@/lib/registryDraft"; @@ -788,6 +789,7 @@ export function RegistryEditor() { const [testOutcomes, setTestOutcomes] = useState>( {} ); + const [selectedTemplate, setSelectedTemplate] = useState("zhipu-glm"); const load = useCallback(async () => { setLoadError(null); @@ -1077,24 +1079,47 @@ export function RegistryEditor() { /> ))} - +
+ + + + Templates only prefill the adapter, Base URL, and suggested models; + a saved provider is an ordinary profile. + +
{saveIssues && saveIssues.details.length > 0 && (
diff --git a/src/lib/registryDraft.test.ts b/src/lib/registryDraft.test.ts new file mode 100644 index 0000000..eafad35 --- /dev/null +++ b/src/lib/registryDraft.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from "vitest"; +import type { AdapterParameterSpec } from "@/lib/modelRegistry"; +import { + PROVIDER_TEMPLATES, + providerDraftFromTemplate, +} from "@/lib/registryDraft"; + +function spec(adapterId: string, authModes: string[]): AdapterParameterSpec { + return { + adapter_id: adapterId, + spec_revision: 1, + model_selector: "*", + auth_specs: Object.fromEntries( + authModes.map((mode) => [ + mode, + { + credential_required: mode !== "none", + credential_kind: mode, + target: "client_option", + target_name: "api_key", + }, + ]) + ), + parameters: {}, + protocol_capabilities: { + tools: true, + vision: false, + structured_output: true, + }, + } as unknown as AdapterParameterSpec; +} + +const SPECS = [ + spec("openai", ["api_key"]), + spec("openai-compatible", ["api_key", "none"]), + spec("anthropic", ["api_key"]), + spec("anthropic-compatible", ["api_key"]), + spec("ollama", ["none"]), +]; + +describe("providerDraftFromTemplate", () => { + it("prefills the Zhipu GLM template with adapter, base URL, and glm-5.2", () => { + const template = PROVIDER_TEMPLATES.find((t) => t.id === "zhipu-glm")!; + const draft = providerDraftFromTemplate(template, SPECS, new Set()); + expect(draft.adapter).toBe("openai-compatible"); + expect(draft.base_url).toBe("https://open.bigmodel.cn/api/paas/v4"); + expect(draft.auth.mode).toBe("api_key"); + expect(draft.auth.credential_id).toBe("zhipu-glm-credential"); + expect(draft.models.map((model) => model.key)).toEqual(["glm-5.2"]); + expect(draft.models[0].enabled).toBe(false); + }); + + it("uses mode=none and no credential for the Ollama template", () => { + const template = PROVIDER_TEMPLATES.find((t) => t.id === "ollama")!; + const draft = providerDraftFromTemplate(template, SPECS, new Set()); + expect(draft.auth).toEqual({ mode: "none", credential_id: null }); + expect(draft.base_url).toBe("http://localhost:11434"); + }); + + it("deduplicates the provider id against existing providers", () => { + const template = PROVIDER_TEMPLATES.find((t) => t.id === "openai")!; + const draft = providerDraftFromTemplate( + template, + SPECS, + new Set(["openai", "openai-2"]) + ); + expect(draft.id).toBe("openai-3"); + expect(draft.auth.credential_id).toBe("openai-3-credential"); + }); + + it("falls back to a blank draft when no template is given", () => { + const draft = providerDraftFromTemplate(null, SPECS, new Set()); + expect(draft.id).toBe("provider-1"); + expect(draft.base_url).toBe(""); + expect(draft.models).toEqual([]); + }); +}); diff --git a/src/lib/registryDraft.ts b/src/lib/registryDraft.ts index 604aa12..f576deb 100644 --- a/src/lib/registryDraft.ts +++ b/src/lib/registryDraft.ts @@ -81,24 +81,84 @@ export function enabledModelRefs(registry: RegistryV4): Array<{ return out; } -export function newProviderDraft( +/** + * New-provider templates (design doc 7.1 item 2). Templates only prefill the + * adapter, Base URL, auth mode, and suggested models/parameters — once saved, + * the result is an ordinary profile; there is no built-in management page. + */ +export interface ProviderTemplate { + id: string; + label: string; + adapter: string; + baseUrl: string; + suggestedModels: string[]; +} + +export const PROVIDER_TEMPLATES: ProviderTemplate[] = [ + { + id: "zhipu-glm", + label: "智谱 GLM", + adapter: "openai-compatible", + baseUrl: "https://open.bigmodel.cn/api/paas/v4", + suggestedModels: ["glm-5.2"], + }, + { + id: "openai", + label: "OpenAI", + adapter: "openai", + baseUrl: "https://api.openai.com/v1", + suggestedModels: [], + }, + { + id: "anthropic", + label: "Anthropic", + adapter: "anthropic", + baseUrl: "https://api.anthropic.com", + suggestedModels: [], + }, + { + id: "openai-compatible", + label: "OpenAI-compatible", + adapter: "openai-compatible", + baseUrl: "", + suggestedModels: [], + }, + { + id: "anthropic-compatible", + label: "Anthropic-compatible", + adapter: "anthropic-compatible", + baseUrl: "", + suggestedModels: [], + }, + { + id: "ollama", + label: "Ollama", + adapter: "ollama", + baseUrl: "http://localhost:11434", + suggestedModels: [], + }, +]; + +export function providerDraftFromTemplate( + template: ProviderTemplate | null, specs: AdapterParameterSpec[], existingIds: Set ): ProviderConfig { - let index = existingIds.size + 1; - let id = `provider-${index}`; + const baseId = template?.id ?? "provider"; + let index = template ? 1 : existingIds.size + 1; + let id = template ? baseId : `${baseId}-${index}`; while (existingIds.has(id)) { index += 1; - id = `provider-${index}`; + id = `${baseId}-${index}`; } - const adapter = specs[0]?.adapter_id ?? "openai-compatible"; + const adapter = template?.adapter ?? specs[0]?.adapter_id ?? "openai-compatible"; const spec = adapterSpecFor(specs, adapter); const authMode = supportedAuthModes(spec)[0] ?? "none"; return { id, - name: id, + name: template?.label ?? id, adapter, - base_url: "", + base_url: template?.baseUrl ?? "", auth: { mode: authMode, credential_id: authMode === "none" ? null : `${id}-credential`, @@ -111,7 +171,7 @@ export function newProviderDraft( default_top_p: null, default_reasoning_effort: "auto", }, - models: [], + models: (template?.suggestedModels ?? []).map((key) => newModelDraft(key)), }; }