diff --git a/NOTICE b/NOTICE index cda0c48f..0ec36818 100644 --- a/NOTICE +++ b/NOTICE @@ -2,3 +2,6 @@ MonoCode is not affiliated with, endorsed by, or sponsored by the makers of the agent harnesses it can drive. Provider marks that appear in the UI (including Claude, Codex, Cursor, Grok, OpenCode, Pi, omp, and fx) are trademarks of their respective owners and are used only to identify those products. + +The Z.AI, MiMo, and OpenRouter provider SVG marks are adapted from CodexBar, +Copyright (c) 2026 Peter Steinberger, and are distributed under the MIT License. diff --git a/src/assets/providers/mimo.svg b/src/assets/providers/mimo.svg new file mode 100644 index 00000000..bd0ed759 --- /dev/null +++ b/src/assets/providers/mimo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/providers/nvidia.svg b/src/assets/providers/nvidia.svg new file mode 100644 index 00000000..ccd2b69c --- /dev/null +++ b/src/assets/providers/nvidia.svg @@ -0,0 +1 @@ + diff --git a/src/assets/providers/openrouter.svg b/src/assets/providers/openrouter.svg new file mode 100644 index 00000000..f9867e52 --- /dev/null +++ b/src/assets/providers/openrouter.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/providers/zai.svg b/src/assets/providers/zai.svg new file mode 100644 index 00000000..90265520 --- /dev/null +++ b/src/assets/providers/zai.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/chrome/HarnessIcon.tsx b/src/chrome/HarnessIcon.tsx index 73f978a9..7b3fa4b1 100644 --- a/src/chrome/HarnessIcon.tsx +++ b/src/chrome/HarnessIcon.tsx @@ -6,6 +6,10 @@ import fx from "../assets/providers/fx.svg"; import grok from "../assets/providers/grok.svg"; import omp from "../assets/providers/omp.svg"; import opencode from "../assets/providers/opencode.svg"; +import zai from "../assets/providers/zai.svg"; +import mimo from "../assets/providers/mimo.svg"; +import openrouter from "../assets/providers/openrouter.svg"; +import nvidia from "../assets/providers/nvidia.svg"; import pi from "../assets/providers/pi.svg"; import type { HarnessId } from "../lib/session"; @@ -15,6 +19,10 @@ export const HARNESS_ICONS: Record = { cursor, grok, opencode, + zai, + mimo, + openrouter, + nvidia, pi, omp, fx, @@ -25,6 +33,10 @@ export const MONOCHROME_HARNESSES = new Set([ "cursor", "grok", "opencode", + "zai", + "mimo", + "openrouter", + "nvidia", "pi", "omp", "fx", @@ -83,16 +95,6 @@ export function HarnessIcon({ ); } - if (harness === "opencode") { - return ( - - - - ); - } if (harness === "pi") { return ( @@ -117,12 +119,24 @@ export function HarnessIcon({ ); } + if (harness === "opencode") { + return ( + + + + ); + } return ( ); } diff --git a/src/index.css b/src/index.css index 1378ce63..bb9f843a 100644 --- a/src/index.css +++ b/src/index.css @@ -47,6 +47,13 @@ html.theme-light { color-scheme: light; } +/* CodexBar's monochrome provider marks are white SVGs. Keep them visible on + the light theme without altering the colored NVIDIA mark. */ +html.theme-light .provider-icon-monochrome { + /* White source mark -> the same soft dark tone used by text-content. */ + filter: invert(0.82); +} + html, body, #root { diff --git a/src/lib/harness/availability.ts b/src/lib/harness/availability.ts index f7d5df20..a1acddd2 100644 --- a/src/lib/harness/availability.ts +++ b/src/lib/harness/availability.ts @@ -27,6 +27,10 @@ const CLI: Record = { install: "curl -fsSL https://x.ai/cli/install.sh | bash", }, opencode: { name: "OpenCode CLI" }, + zai: { name: "Z.AI via OpenCode CLI" }, + mimo: { name: "MiMo via OpenCode CLI" }, + openrouter: { name: "OpenRouter via OpenCode CLI" }, + nvidia: { name: "NVIDIA via OpenCode CLI" }, pi: { name: "Pi CLI", install: "npm i -g @earendil-works/pi-coding-agent" }, omp: { name: "omp CLI", install: "curl -fsSL https://omp.sh/install | sh" }, fx: { name: "fx CLI", install: "curl -fsSL https://fx.sh/setup.sh | bash" }, @@ -38,6 +42,10 @@ let availability: HarnessAvailability = { cursor: false, grok: false, opencode: false, + zai: false, + mimo: false, + openrouter: false, + nvidia: false, pi: false, omp: false, fx: false, @@ -119,7 +127,7 @@ export function probeHarnessAvailability( return [id, false] as const; } } - if (id === "opencode") { + if (["opencode", "zai", "mimo", "openrouter", "nvidia"].includes(id)) { try { await resolveOpenCodeBinary(); return [id, true] as const; diff --git a/src/lib/harness/opencodeAdapter.ts b/src/lib/harness/opencodeAdapter.ts index 71e8fc05..4b620459 100644 --- a/src/lib/harness/opencodeAdapter.ts +++ b/src/lib/harness/opencodeAdapter.ts @@ -18,6 +18,7 @@ import { import { generateOpenCodeSessionTitle } from "./opencodeTitle"; import { warmupOpenCodeText } from "./opencodeText"; import { registerHarness, type HarnessAdapter } from "./registry"; +import type { HarnessId } from "../session"; export const openCodeAdapter: HarnessAdapter = { id: "opencode", @@ -46,3 +47,16 @@ export function ensureOpenCodeRegistered(): void { registerHarness(openCodeAdapter); registered = true; } + +/** + * OpenAI-compatible providers use the OpenCode runtime, but remain separate + * provider tabs in MonoCode. The selected model's native id tells OpenCode + * which provider/model pair to route to. + */ +export function ensureOpenCodeProviderRegistered(id: Exclude): void { + if (registeredProviders.has(id)) return; + registerHarness({ ...openCodeAdapter, id, refreshCatalog: refreshOpenCodeCatalog }); + registeredProviders.add(id); +} + +const registeredProviders = new Set(); diff --git a/src/lib/harness/opencodeCatalog.ts b/src/lib/harness/opencodeCatalog.ts index 450881d7..fef01aa9 100644 --- a/src/lib/harness/opencodeCatalog.ts +++ b/src/lib/harness/opencodeCatalog.ts @@ -1,5 +1,6 @@ import { homeDir } from "../fs"; import { + MODELS, setHarnessModels, type AgentModel, type ModelSetting, @@ -26,6 +27,16 @@ type OpenCodeModelJson = { limit?: { context?: number; input?: number; output?: number }; }; +type OpenRouterModelJson = { + id?: string; + name?: string; + context_length?: number; + architecture?: { + input_modalities?: string[]; + output_modalities?: string[]; + }; +}; + type ParsedProvider = { id: string; name: string; @@ -44,7 +55,15 @@ export function refreshOpenCodeCatalog(): Promise { if (inflight) return inflight; inflight = discoverOpenCodeModels() .then((models) => { - if (models.length > 0) setHarnessModels("opencode", models); + const byHarness = new Map(); + for (const model of models) { + const entries = byHarness.get(model.harness) ?? []; + entries.push(model); + byHarness.set(model.harness, entries); + } + for (const [harness, entries] of byHarness) { + if (entries.length > 0) setHarnessModels(harness, entries); + } }) .catch((error: unknown) => { console.debug("[monocode] opencode catalog", error); @@ -80,7 +99,40 @@ async function discoverOpenCodeModels(): Promise { } catch (error) { console.debug("[monocode] opencode agents", error); } - return flattenOpenCodeModels(parsed, agents); + const models = flattenOpenCodeModels(parsed, agents); + const openRouterModels = await discoverOpenRouterModels(); + return [...models, ...openRouterModels]; +} + +async function discoverOpenRouterModels(): Promise { + try { + const response = await fetch("https://openrouter.ai/api/v1/models"); + if (!response.ok) throw new Error(`OpenRouter models HTTP ${response.status}`); + const payload = (await response.json()) as { data?: OpenRouterModelJson[] }; + return (payload.data ?? []) + .filter((model) => { + const input = model.architecture?.input_modalities ?? ["text"]; + const output = model.architecture?.output_modalities ?? ["text"]; + return input.includes("text") && output.includes("text"); + }) + .flatMap((model) => { + const nativeId = model.id?.trim(); + if (!nativeId) return []; + return [{ + id: `openrouter:openrouter/${nativeId}`, + harness: "openrouter" as const, + name: model.name?.trim() || nativeId, + nativeId: `openrouter/${nativeId}`, + ...(model.context_length && model.context_length > 0 + ? { contextWindow: model.context_length } + : {}), + }]; + }) + .sort((left, right) => left.name.localeCompare(right.name)); + } catch (error) { + console.debug("[monocode] openrouter public catalog", error); + return []; + } } export function parseModelsCliOutput(stdout: string): { @@ -184,10 +236,12 @@ export function flattenOpenCodeModels( for (const [modelId, model] of Object.entries(provider.models)) { const name = model.name?.trim() || titleCaseSlug(modelId); const nativeId = `${provider.id}/${model.id ?? modelId}`; + if (isRetiredModel(nativeId)) continue; + const harness = providerHarness(provider.id); const contextWindow = model.limit?.context; models.push({ - id: `opencode:${nativeId}`, - harness: "opencode", + id: `${harness}:${nativeId}`, + harness, name, nativeId, settings: openCodeModelSettings(provider.id, model, primaryAgents), @@ -195,9 +249,52 @@ export function flattenOpenCodeModels( }); } } + // Keep the providers MonoCode knows about even when the local OpenCode + // installation has not emitted them yet. Once configured, the live catalog + // entries above replace these fallback entries by their complete metadata. + const seen = new Set(models.map((model) => model.nativeId)); + for (const model of MODELS) { + if ( + !["opencode", "zai", "mimo", "openrouter", "nvidia"].includes(model.harness) || + !model.nativeId || + seen.has(model.nativeId) || + isRetiredModel(model.nativeId) + ) { + continue; + } + models.push(model); + } return models.sort((left, right) => left.name.localeCompare(right.name)); } +function isRetiredModel(nativeId: string): boolean { + return new Set([ + "nvidia/deepseek-ai/deepseek-v4-pro", + "nvidia/z-ai/glm-5.2", + "nvidia/nvidia/llama-3.3-nemotron-super-49b-v1", + "nvidia/nvidia/nvidia-nemotron-nano-9b-v2", + "nvidia/nvidia/nemotron-nano-12b-v2-vl", + ]).has(nativeId); +} + +function providerHarness(providerID: string): AgentModel["harness"] { + if ( + providerID === "zai" || + providerID === "zai-coding-plan" + ) { + return "zai"; + } + if ( + providerID === "mimo" || + providerID.startsWith("xiaomi-token-plan-") + ) { + return "mimo"; + } + if (providerID === "openrouter") return "openrouter"; + if (providerID === "nvidia") return "nvidia"; + return "opencode"; +} + function openCodeModelSettings( providerID: string, model: OpenCodeModelJson, diff --git a/src/lib/harness/opencodeProtocol.test.ts b/src/lib/harness/opencodeProtocol.test.ts index f5ad7b74..6513f5d8 100644 --- a/src/lib/harness/opencodeProtocol.test.ts +++ b/src/lib/harness/opencodeProtocol.test.ts @@ -105,9 +105,17 @@ describe("OpenCode CLI inventory parsers", () => { ]); expect(models.map((model) => model.nativeId)).toEqual([ "anthropic/claude-sonnet-4-6", + "openrouter/deepseek/deepseek-chat-v3.1", "opencode/glm-5", + "zai-coding-plan/glm-5.1", + "zai-coding-plan/glm-5.2", + "zai-coding-plan/glm-5.3", + "zai-coding-plan/glm-5.3-flash", + "xiaomi-token-plan-ams/mimo-v2.5", + "xiaomi-token-plan-ams/mimo-v2.5-pro", + "nvidia/nvidia/llama-3.3-nemotron-super-49b-v1.5", ]); - expect(models[1].settings?.some((setting) => setting.id === "variant")).toBe( + expect(models[2].settings?.some((setting) => setting.id === "variant")).toBe( true, ); expect(models[0].settings?.find((setting) => setting.id === "agent")?.value).toBe( @@ -115,6 +123,23 @@ describe("OpenCode CLI inventory parsers", () => { ); }); + it("keeps the requested built-in providers in the OpenCode catalog", () => { + const models = flattenOpenCodeModels( + { providers: new Map(), connected: [] }, + [], + ); + expect(models.map((model) => model.nativeId)).toEqual([ + "openrouter/deepseek/deepseek-chat-v3.1", + "zai-coding-plan/glm-5.1", + "zai-coding-plan/glm-5.2", + "zai-coding-plan/glm-5.3", + "zai-coding-plan/glm-5.3-flash", + "xiaomi-token-plan-ams/mimo-v2.5", + "xiaomi-token-plan-ams/mimo-v2.5-pro", + "nvidia/nvidia/llama-3.3-nemotron-super-49b-v1.5", + ]); + }); + it("parses agent list headers", () => { const agents = parseAgentListCliOutput( ["build (primary)", "{}", "compaction (primary)", "{}"].join("\n"), diff --git a/src/lib/harness/register.ts b/src/lib/harness/register.ts index c8fbfe86..06a2e07c 100644 --- a/src/lib/harness/register.ts +++ b/src/lib/harness/register.ts @@ -3,7 +3,10 @@ import { ensureCodexRegistered } from "./codexAdapter"; import { ensureCursorRegistered } from "./cursorAdapter"; import { ensureFxRegistered } from "./fxAdapter"; import { ensureGrokRegistered } from "./grokAdapter"; -import { ensureOpenCodeRegistered } from "./opencodeAdapter"; +import { + ensureOpenCodeProviderRegistered, + ensureOpenCodeRegistered, +} from "./opencodeAdapter"; import { ensureOmpRegistered } from "./ompAdapter"; import { ensurePiRegistered } from "./piAdapter"; @@ -14,6 +17,10 @@ export function registerBuiltinHarnesses(): void { ensureCodexRegistered(); ensureGrokRegistered(); ensureOpenCodeRegistered(); + ensureOpenCodeProviderRegistered("zai"); + ensureOpenCodeProviderRegistered("mimo"); + ensureOpenCodeProviderRegistered("openrouter"); + ensureOpenCodeProviderRegistered("nvidia"); ensurePiRegistered(); ensureOmpRegistered(); ensureFxRegistered(); diff --git a/src/lib/models.ts b/src/lib/models.ts index a5a9ef1d..5ec5709a 100644 --- a/src/lib/models.ts +++ b/src/lib/models.ts @@ -152,6 +152,60 @@ export const MODELS: AgentModel[] = [ name: "Claude Sonnet 4.6", }, { id: "opencode:gpt-5.4", harness: "opencode", name: "GPT-5.4" }, + { + id: "zai:glm-5.2", + harness: "zai", + name: "GLM-5.2", + nativeId: "zai-coding-plan/glm-5.2", + contextWindow: 200_000, + }, + { + id: "zai:glm-5.1", + harness: "zai", + name: "GLM-5.1", + nativeId: "zai-coding-plan/glm-5.1", + contextWindow: 202_752, + }, + { + id: "zai:glm-5.3", + harness: "zai", + name: "GLM-5.3", + nativeId: "zai-coding-plan/glm-5.3", + contextWindow: 1_000_000, + }, + { + id: "zai:glm-5.3-flash", + harness: "zai", + name: "GLM-5.3 Flash", + nativeId: "zai-coding-plan/glm-5.3-flash", + contextWindow: 1_000_000, + }, + { + id: "mimo:mimo-v2.5-pro", + harness: "mimo", + name: "MiMo V2.5 Pro", + nativeId: "xiaomi-token-plan-ams/mimo-v2.5-pro", + contextWindow: 1_048_576, + }, + { + id: "mimo:mimo-v2.5", + harness: "mimo", + name: "MiMo V2.5", + nativeId: "xiaomi-token-plan-ams/mimo-v2.5", + contextWindow: 1_000_000, + }, + { + id: "openrouter:deepseek/deepseek-chat-v3.1", + harness: "openrouter", + name: "DeepSeek Chat V3.1", + nativeId: "openrouter/deepseek/deepseek-chat-v3.1", + }, + { + id: "nvidia:nvidia/llama-3.3-nemotron-super-49b-v1.5", + harness: "nvidia", + name: "NVIDIA Nemotron Super 49B v1.5", + nativeId: "nvidia/nvidia/llama-3.3-nemotron-super-49b-v1.5", + }, { id: "pi:default", harness: "pi", @@ -178,6 +232,10 @@ export const DEFAULT_MODEL_ID: Record = { cursor: "cursor:composer-2.5", grok: "grok:grok-4.6", opencode: "opencode:glm-5", + zai: "zai:glm-5.2", + mimo: "mimo:mimo-v2.5-pro", + openrouter: "openrouter:deepseek/deepseek-chat-v3.1", + nvidia: "nvidia:nvidia/nemotron-3.5-lightning-30b-a3b", pi: "pi:default", omp: "omp:default", fx: "fx:zai/glm-5.2-fast", @@ -203,6 +261,10 @@ const HARNESS_ORDER: HarnessId[] = [ "cursor", "grok", "opencode", + "zai", + "mimo", + "openrouter", + "nvidia", "pi", "omp", "fx", diff --git a/src/lib/session.ts b/src/lib/session.ts index 41e9ece3..3d6791fe 100644 --- a/src/lib/session.ts +++ b/src/lib/session.ts @@ -11,7 +11,18 @@ import { } from "./models"; export type HarnessId = - "claude" | "codex" | "cursor" | "grok" | "opencode" | "pi" | "omp" | "fx"; + | "claude" + | "codex" + | "cursor" + | "grok" + | "opencode" + | "zai" + | "mimo" + | "openrouter" + | "nvidia" + | "pi" + | "omp" + | "fx"; export const HARNESSES: HarnessId[] = [ "claude", @@ -19,6 +30,10 @@ export const HARNESSES: HarnessId[] = [ "cursor", "grok", "opencode", + "zai", + "mimo", + "openrouter", + "nvidia", "pi", "omp", "fx", @@ -261,6 +276,10 @@ export const HARNESS_LABEL: Record = { cursor: "cursor", grok: "grok", opencode: "opencode", + zai: "zai", + mimo: "mimo", + openrouter: "openrouter", + nvidia: "nvidia", pi: "pi", omp: "omp", fx: "fx", @@ -272,6 +291,10 @@ export const HARNESS_TITLE: Record = { cursor: "Cursor", grok: "Grok Build", opencode: "OpenCode", + zai: "Z.AI", + mimo: "MiMo", + openrouter: "OpenRouter", + nvidia: "NVIDIA", pi: "Pi", omp: "omp", fx: "fx",