mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-11 06:30:25 +00:00
GUI: multi-field provider credentials and add-model family dropdown
Test button and saved pill follow the required-secret field (or the first field for cloud providers); Bedrock/Vertex add-model rows get a family selector.
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
// Add-model family dropdown for the cloud-account providers: the family choice folds
|
||||
// into the model id (`bedrock:claude/…`, `vertex:openweight/…`); plain providers keep
|
||||
// the bare add-model row.
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { ModelChecklist } from "./ModelChecklist";
|
||||
|
||||
vi.mock("../api", () => ({
|
||||
addModel: vi.fn(async (id: string) => ({ ok: true, models: [id], model: id })),
|
||||
removeModel: vi.fn(async () => ({ ok: true, models: [], model: "" })),
|
||||
setDefaultModel: vi.fn(async () => ({ ok: true })),
|
||||
getSettings: vi.fn(async () => ({ models: [], model: "" })),
|
||||
}));
|
||||
|
||||
import { addModel } from "../api";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const KNOWN = ["openai", "anthropic", "bedrock", "vertex", "openrouter"];
|
||||
|
||||
function renderList(provider: string) {
|
||||
return render(
|
||||
<ModelChecklist
|
||||
provider={provider}
|
||||
knownProviders={KNOWN}
|
||||
suggested={[]}
|
||||
curated={[]}
|
||||
defaultModel=""
|
||||
onChanged={() => {}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
function addTyped(id: string) {
|
||||
fireEvent.change(screen.getByPlaceholderText("Add another model…"), {
|
||||
target: { value: id },
|
||||
});
|
||||
fireEvent.click(screen.getByText("Add"));
|
||||
}
|
||||
|
||||
describe("ModelChecklist add-model family dropdown", () => {
|
||||
it("folds the selected vertex family into the id", async () => {
|
||||
renderList("vertex");
|
||||
fireEvent.change(screen.getByTestId("mlist-family"), {
|
||||
target: { value: "openweight" },
|
||||
});
|
||||
addTyped("meta/llama-4-maverick-17b-128e-instruct-maas");
|
||||
expect(addModel).toHaveBeenCalledWith(
|
||||
"vertex:openweight/meta/llama-4-maverick-17b-128e-instruct-maas",
|
||||
);
|
||||
});
|
||||
|
||||
it("defaults bedrock to the Claude family and keeps a typed family verbatim", async () => {
|
||||
renderList("bedrock");
|
||||
addTyped("anthropic.claude-sonnet-4-6-v1:0");
|
||||
expect(addModel).toHaveBeenCalledWith(
|
||||
"bedrock:claude/anthropic.claude-sonnet-4-6-v1:0",
|
||||
);
|
||||
addTyped("other/amazon.nova-2-pro-v1:0");
|
||||
expect(addModel).toHaveBeenLastCalledWith("bedrock:other/amazon.nova-2-pro-v1:0");
|
||||
});
|
||||
|
||||
it("shows no family dropdown for plain providers", async () => {
|
||||
renderList("openrouter");
|
||||
expect(screen.queryByTestId("mlist-family")).toBeNull();
|
||||
addTyped("z-ai/glm-5.2");
|
||||
expect(addModel).toHaveBeenCalledWith("openrouter:z-ai/glm-5.2");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,22 @@
|
||||
import { useState } from "react";
|
||||
import { addModel, getSettings, removeModel, setDefaultModel } from "../api";
|
||||
|
||||
// Cloud-account providers dispatch by a family segment baked into the model id
|
||||
// (`bedrock:claude/…`, `vertex:openweight/…`). The add-model row shows a dropdown so
|
||||
// users pick the family instead of memorizing the prefix; curated matrix ids already
|
||||
// carry theirs.
|
||||
const MODEL_FAMILIES: Record<string, { value: string; label: string }[]> = {
|
||||
bedrock: [
|
||||
{ value: "claude", label: "Claude family" },
|
||||
{ value: "other", label: "Other models" },
|
||||
],
|
||||
vertex: [
|
||||
{ value: "gemini", label: "Gemini family" },
|
||||
{ value: "claude", label: "Claude family" },
|
||||
{ value: "openweight", label: "Open-weight" },
|
||||
],
|
||||
};
|
||||
|
||||
// One provider's models as a checklist: tick = shown in the composer's model picker (the
|
||||
// curated list), the black "default" badge marks the model new sessions use, and hovering any
|
||||
// other row reveals "Make default". A free-type row below adds models by hand, so brand-new
|
||||
@@ -23,6 +39,8 @@ export function ModelChecklist({
|
||||
onChanged: (next: { models: string[]; model: string }) => void;
|
||||
}) {
|
||||
const [draft, setDraft] = useState("");
|
||||
const families = MODEL_FAMILIES[provider];
|
||||
const [family, setFamily] = useState(families?.[0]?.value || "");
|
||||
|
||||
const provOf = (id: string) => {
|
||||
const i = id.indexOf(":");
|
||||
@@ -52,8 +70,12 @@ export function ModelChecklist({
|
||||
await refresh();
|
||||
};
|
||||
const add = async () => {
|
||||
const typed = draft.trim();
|
||||
let typed = draft.trim();
|
||||
if (!typed) return;
|
||||
// Fold the family choice into the id unless the user already typed one.
|
||||
if (families && !families.some((f) => typed.startsWith(`${f.value}/`))) {
|
||||
typed = `${family}/${typed}`;
|
||||
}
|
||||
const res = await addModel(prefixed(typed));
|
||||
if (res.ok) {
|
||||
setDraft("");
|
||||
@@ -90,6 +112,20 @@ export function ModelChecklist({
|
||||
);
|
||||
})}
|
||||
<div className="mlist-add">
|
||||
{families && (
|
||||
<select
|
||||
value={family}
|
||||
onChange={(e) => setFamily(e.target.value)}
|
||||
aria-label="Model family"
|
||||
data-testid="mlist-family"
|
||||
>
|
||||
{families.map((f) => (
|
||||
<option key={f.value} value={f.value}>
|
||||
{f.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<input
|
||||
placeholder="Add another model…"
|
||||
value={draft}
|
||||
|
||||
Reference in New Issue
Block a user