Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions coworker/providers/matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class ModelEntry:
"deepseek:deepseek-v4-pro": ModelEntry("DeepSeek V4 Pro · DeepSeek"),
"kimi:kimi-k2.6": ModelEntry("Kimi K2.6 · Moonshot"),
"minimax:MiniMax-M2.5": ModelEntry("MiniMax M2.5 · MiniMax"),
"nvidia:z-ai/glm-5.2": ModelEntry("GLM-5.2 · NVIDIA NIM"),
"qwen:qwen3-max": ModelEntry("Qwen3 Max · Alibaba"),
"xai:grok-4.3": ModelEntry("Grok 4.3 · xAI"),
"mistral:mistral-large-latest": ModelEntry("Mistral Large · Mistral"),
Expand Down
36 changes: 36 additions & 0 deletions coworker/providers/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,17 @@ def _build_ollama(profile: dict[str, Any], secrets: Any) -> ProviderClient:
return OpenAIProvider(api_key="ollama", base_url=base_url)


def _build_nvidia(profile: dict[str, Any], secrets: Any) -> ProviderClient:
# NVIDIA NIM is OpenAI-compatible, but it uses its own key namespace and endpoint.
api_key = ((profile or {}).get("api_key") or "").strip() or (
os.environ.get("NVIDIA_API_KEY", "").strip()
)
if not api_key:
raise RuntimeError("No NVIDIA API key configured — add it in Settings ▸ Models.")
base_url = ((profile or {}).get("base_url") or "").strip() or "https://integrate.api.nvidia.com/v1"
return OpenAIProvider(api_key=api_key, base_url=base_url)


def _openai_compat(vendor: str, default_base_url: str, env_key: Optional[str] = None):
"""Builder factory for vendors reached through their OpenAI-compatible API (Z AI, DeepSeek,
Kimi, MiniMax, Qwen, xAI, Mistral). The key is resolved from the vendor's OWN profile (or its
Expand Down Expand Up @@ -307,6 +318,31 @@ def _compat(
recommended_model="mistral-large-latest",
env_key="MISTRAL_API_KEY",
),
ProviderDescriptor(
name="nvidia",
title="NVIDIA NIM",
needs_key=True,
fields=[
ProviderField(
"api_key",
"NVIDIA API key",
secret=True,
placeholder="nvapi-…",
),
ProviderField(
"base_url",
"Endpoint",
required=False,
default="https://integrate.api.nvidia.com/v1",
placeholder="https://integrate.api.nvidia.com/v1",
help="Prefilled with NVIDIA's OpenAI-compatible NIM endpoint.",
),
],
build=_build_nvidia,
recommended_model="z-ai/glm-5.2",
env_key="NVIDIA_API_KEY",
blurb="Uses NVIDIA NIM's OpenAI-compatible API — the endpoint is prefilled, just add your key.",
),
# Resellers: many labs' models behind one key, using THEIR model namespaces (the curated
# ids + display labels live in providers/matrix.py). TODO: add Groq and OpenRouter here
# (+ their matrix rows) once the current provider surface is tested — deliberately
Expand Down
3 changes: 3 additions & 0 deletions surfaces/gui/e2e/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const SETTINGS = {
model_labels: {
"anthropic:claude-opus-4-8": "Claude Opus 4.8 · Anthropic",
"zai:glm-5.2": "GLM-5.2 · Z AI",
"nvidia:z-ai/glm-5.2": "GLM-5.2 · NVIDIA NIM",
},
};

Expand Down Expand Up @@ -331,6 +332,8 @@ const PROVIDERS = [
{ name: "anthropic", title: "Claude (Anthropic)", needs_key: true, fields: [{ key: "api_key", label: "API key", secret: true, required: true, help: "", placeholder: "sk-…" }], configured: true, values: {}, suggested_models: ["claude-opus-4-8"], key_set_at: null, last_used_at: null },
// zai: an OpenAI-compatible vendor — unconfigured, with a prefilled editable endpoint + blurb.
{ name: "zai", title: "Z AI (GLM)", needs_key: true, blurb: "Uses Z AI's OpenAI-compatible API — the endpoint is prefilled, just add your key.", fields: [{ key: "api_key", label: "Z AI API key", secret: true, required: true, help: "", placeholder: "" }, { key: "base_url", label: "Endpoint", secret: false, required: false, help: "Prefilled with Z AI's international endpoint.", placeholder: "https://api.z.ai/api/paas/v4", default: "https://api.z.ai/api/paas/v4" }], configured: false, values: {}, suggested_models: ["glm-5.2"], key_set_at: null, last_used_at: null },
// nvidia: an OpenAI-compatible vendor — unconfigured, with a prefilled editable endpoint + blurb.
{ name: "nvidia", title: "NVIDIA NIM", needs_key: true, blurb: "Uses NVIDIA NIM's OpenAI-compatible API — the endpoint is prefilled, just add your key.", fields: [{ key: "api_key", label: "NVIDIA API key", secret: true, required: true, help: "", placeholder: "nvapi-…" }, { key: "base_url", label: "Endpoint", secret: false, required: false, help: "Prefilled with NVIDIA's OpenAI-compatible NIM endpoint.", placeholder: "https://integrate.api.nvidia.com/v1", default: "https://integrate.api.nvidia.com/v1" }], configured: false, values: {}, suggested_models: ["z-ai/glm-5.2"], key_set_at: null, last_used_at: null },
// ollama: keyless local provider — "configured" without proving anything runs; the
// onboarding gallery shows "No key needed" and its form is endpoint + Detect (§39).
{ name: "ollama", title: "Ollama (local models)", needs_key: false, fields: [{ key: "base_url", label: "Endpoint", secret: false, required: false, help: "", placeholder: "http://127.0.0.1:11434", default: "http://127.0.0.1:11434" }], configured: true, values: {}, suggested_models: ["qwen3-coder:30b"], key_set_at: null, last_used_at: null },
Expand Down
14 changes: 14 additions & 0 deletions surfaces/gui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion surfaces/gui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ export function App() {
// composer's "No model connected" chip. Default true so we don't flash the chip before settings
// load; corrected by loadSettings.
const [modelReady, setModelReady] = useState(true);
const [modelsLoaded, setModelsLoaded] = useState(false);
const [surface, setSurface] = useState<
"session" | "scheduled" | "integrations" | "audit" | "inbox" | "persona" | "settings"
>("session");
Expand Down Expand Up @@ -480,9 +481,12 @@ export function App() {
setModels(s.models || []);
setModelLabels(s.model_labels || {});
setModelReady(s.model_ready);
setModelsLoaded(true);
if (s.surfaces) setSurfaces(s.surfaces);
})
.catch(() => {});
.catch(() => {
setModelsLoaded(true);
});

// Open Settings → Configure Models (from the composer's "No model connected" chip).
const openModelSetup = () => openSettings("models");
Expand Down Expand Up @@ -1509,6 +1513,7 @@ export function App() {
mode={mode}
model={model}
models={models}
modelsLoaded={modelsLoaded}
modelLabels={modelLabels}
running={running}
connected={connected}
Expand Down
3 changes: 2 additions & 1 deletion surfaces/gui/src/components/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ interface Props {
mode: string;
model: string;
models?: string[];
modelsLoaded?: boolean;
modelLabels?: Record<string, string>; // curated display names (raw id when absent)
// The model is FIXED once the session has history (§17): the picker renders ONLY on a fresh
// session; after the first turn the fact lives in the topbar subtitle (§22) — no
Expand Down Expand Up @@ -320,7 +321,7 @@ export function Composer(props: Props) {
}
};

const modelsLoaded = !!(props.models && props.models.length);
const modelsLoaded = props.modelsLoaded ?? !!(props.models && props.models.length);
const modelOptions: Option[] = Array.from(
new Set([props.model, ...(props.models || [])]),
).map((m) => ({
Expand Down
7 changes: 7 additions & 0 deletions surfaces/gui/src/components/Composer.voice.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,11 @@ describe("Composer voice input (§37)", () => {
expect((await screen.findByRole("alert")).textContent).toContain("No microphone is available.");
expect(screen.getByLabelText("Start dictation").hasAttribute("disabled")).toBe(false);
});

it("an empty loaded model list shows the dropdown instead of the loading chip", () => {
render(<Composer {...props({ models: [], modelsLoaded: true })} />);

expect(screen.queryByTestId("models-loading")).toBeNull();
expect(screen.getByRole("button", { name: /gpt-5.6-sol/i })).toBeTruthy();
});
});
3 changes: 3 additions & 0 deletions surfaces/gui/src/providers/logos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import mistral from "./logos/mistral.svg";
import qwen from "./logos/qwen.svg";
import minimax from "./logos/minimax.svg";
import xai from "./logos/xai.svg";
import nvidia from "./logos/nvidia.svg";

export const PROVIDER_LOGOS: Record<string, string> = {
anthropic,
Expand All @@ -33,6 +34,7 @@ export const PROVIDER_LOGOS: Record<string, string> = {
qwen,
minimax,
xai,
nvidia,
};

export const PROVIDER_ORDER = [
Expand All @@ -43,6 +45,7 @@ export const PROVIDER_ORDER = [
"fireworks",
"together",
"zai",
"nvidia",
"kimi",
"deepseek",
"mistral",
Expand Down
5 changes: 5 additions & 0 deletions surfaces/gui/src/providers/logos/nvidia.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.