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:
@@ -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">
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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)),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user