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
3 changes: 3 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 4 additions & 0 deletions src/assets/providers/mimo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/providers/nvidia.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions src/assets/providers/openrouter.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/assets/providers/zai.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 25 additions & 11 deletions src/chrome/HarnessIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -15,6 +19,10 @@ export const HARNESS_ICONS: Record<HarnessId, string> = {
cursor,
grok,
opencode,
zai,
mimo,
openrouter,
nvidia,
pi,
omp,
fx,
Expand All @@ -25,6 +33,10 @@ export const MONOCHROME_HARNESSES = new Set<HarnessId>([
"cursor",
"grok",
"opencode",
"zai",
"mimo",
"openrouter",
"nvidia",
"pi",
"omp",
"fx",
Expand Down Expand Up @@ -83,16 +95,6 @@ export function HarnessIcon({
</MonoIcon>
);
}
if (harness === "opencode") {
return (
<MonoIcon className={className}>
<path
fillRule="evenodd"
d="M20.4 5.8H8.8V23.2H20.4V5.8ZM26.2 29H3V0H26.2V29Z"
/>
</MonoIcon>
);
}
if (harness === "pi") {
return (
<MonoIcon className={className}>
Expand All @@ -117,12 +119,24 @@ export function HarnessIcon({
</MonoIcon>
);
}
if (harness === "opencode") {
return (
<MonoIcon className={className}>
<path
fillRule="evenodd"
d="M20.4 5.8H8.8V23.2H20.4V5.8ZM26.2 29H3V0H26.2V29Z"
/>
</MonoIcon>
);
}
return (
<img
src={HARNESS_ICONS[harness]}
alt=""
draggable={false}
className={`block object-contain ${className}`}
className={`block object-contain ${
MONOCHROME_HARNESSES.has(harness) ? "provider-icon-monochrome" : ""
} ${className}`}
/>
);
}
7 changes: 7 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 9 additions & 1 deletion src/lib/harness/availability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ const CLI: Record<HarnessId, { name: string; install?: string }> = {
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" },
Expand All @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
14 changes: 14 additions & 0 deletions src/lib/harness/opencodeAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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<HarnessId, "opencode">): void {
if (registeredProviders.has(id)) return;
registerHarness({ ...openCodeAdapter, id, refreshCatalog: refreshOpenCodeCatalog });
registeredProviders.add(id);
}

const registeredProviders = new Set<HarnessId>();
105 changes: 101 additions & 4 deletions src/lib/harness/opencodeCatalog.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { homeDir } from "../fs";
import {
MODELS,
setHarnessModels,
type AgentModel,
type ModelSetting,
Expand All @@ -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;
Expand All @@ -44,7 +55,15 @@ export function refreshOpenCodeCatalog(): Promise<void> {
if (inflight) return inflight;
inflight = discoverOpenCodeModels()
.then((models) => {
if (models.length > 0) setHarnessModels("opencode", models);
const byHarness = new Map<AgentModel["harness"], AgentModel[]>();
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);
Expand Down Expand Up @@ -80,7 +99,40 @@ async function discoverOpenCodeModels(): Promise<AgentModel[]> {
} 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<AgentModel[]> {
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): {
Expand Down Expand Up @@ -184,20 +236,65 @@ 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),
...(contextWindow && contextWindow > 0 ? { contextWindow } : {}),
});
}
}
// 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,
Expand Down
27 changes: 26 additions & 1 deletion src/lib/harness/opencodeProtocol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,41 @@ 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(
"build",
);
});

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"),
Expand Down
9 changes: 8 additions & 1 deletion src/lib/harness/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -14,6 +17,10 @@ export function registerBuiltinHarnesses(): void {
ensureCodexRegistered();
ensureGrokRegistered();
ensureOpenCodeRegistered();
ensureOpenCodeProviderRegistered("zai");
ensureOpenCodeProviderRegistered("mimo");
ensureOpenCodeProviderRegistered("openrouter");
ensureOpenCodeProviderRegistered("nvidia");
ensurePiRegistered();
ensureOmpRegistered();
ensureFxRegistered();
Expand Down
Loading