feat: new-provider templates in Registry Editor (design doc 7.1)

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 <noreply@anthropic.com>
This commit is contained in:
m4
2026-07-21 21:14:51 +08:00
parent 9f702e230d
commit 2bf1127af2
3 changed files with 189 additions and 27 deletions
+44 -19
View File
@@ -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<Record<string, TestOutcome>>(
{}
);
const [selectedTemplate, setSelectedTemplate] = useState<string>("zhipu-glm");
const load = useCallback(async () => {
setLoadError(null);
@@ -1077,24 +1079,47 @@ export function RegistryEditor() {
/>
))}
<Button
type="button"
variant="outline"
onClick={() =>
setDraft({
...draft,
providers: [
...draft.providers,
newProviderDraft(
data.adapter_specs,
new Set(draft.providers.map((provider) => provider.id))
),
],
})
}
>
Add provider
</Button>
<div className="flex flex-wrap items-center gap-2">
<select
className={inputClass + " max-w-56"}
value={selectedTemplate}
onChange={(event) => setSelectedTemplate(event.target.value)}
aria-label="Provider template"
>
{PROVIDER_TEMPLATES.map((template) => (
<option key={template.id} value={template.id}>
{template.label}
</option>
))}
<option value="blank">Blank provider</option>
</select>
<Button
type="button"
variant="outline"
onClick={() => {
const template =
PROVIDER_TEMPLATES.find((entry) => entry.id === selectedTemplate) ??
null;
setDraft({
...draft,
providers: [
...draft.providers,
providerDraftFromTemplate(
template,
data.adapter_specs,
new Set(draft.providers.map((provider) => provider.id))
),
],
});
}}
>
Add provider
</Button>
<span className="text-xs text-muted-foreground">
Templates only prefill the adapter, Base URL, and suggested models;
a saved provider is an ordinary profile.
</span>
</div>
{saveIssues && saveIssues.details.length > 0 && (
<div className="rounded-md border border-destructive/30 bg-destructive/5 px-4 py-3 text-xs">
+77
View File
@@ -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([]);
});
});
+68 -8
View File
@@ -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<string>
): 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)),
};
}