Skip to content
Merged
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
98 changes: 39 additions & 59 deletions data/opencode.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,82 +9,62 @@
"apiKey": "{env:OPPER_API_KEY}"
},
"models": {
"gpt-5.5": {
"name": "GPT 5.5",
"cost": { "input": 5.00, "output": 30.00 },
"limit": { "context": 1050000, "output": 128000 },
"reasoning": true,
"variants": {
"xhigh": {
"reasoningEffort": "xhigh",
"reasoningSummary": "auto"
}
}
"claude-opus-4-7": {
"name": "Claude Opus 4.7",
"cost": { "input": 5.00, "output": 25.00 },
"limit": { "context": 1000000, "output": 128000 }
},
"claude-sonnet-4-6": {
"name": "Claude Sonnet 4.6",
"cost": { "input": 3.00, "output": 15.00 },
"limit": { "context": 1000000, "output": 64000 }
},
"claude-opus-4-7": {
"name": "Claude Opus 4.7",
"cost": { "input": 5.00, "output": 25.00 },
"limit": { "context": 1000000, "output": 128000 }
},
"claude-haiku-4-5": {
"name": "Claude Haiku 4.5",
"cost": { "input": 1.00, "output": 5.00 },
"limit": { "context": 200000, "output": 64000 }
},
"gpt-5.2": {
"name": "GPT 5.2",
"cost": { "input": 1.75, "output": 14.00 },
"limit": { "context": 400000, "output": 128000 }
},
"gpt-5-mini": {
"name": "GPT 5 Mini",
"cost": { "input": 0.25, "output": 1.00 },
"limit": { "context": 400000, "output": 128000 }
},
"gpt-5-nano": {
"name": "GPT 5 Nano",
"cost": { "input": 0.05, "output": 0.40 },
"limit": { "context": 400000, "output": 128000 }
},
"gemini-2.5-pro": {
"name": "Gemini 2.5 Pro",
"cost": { "input": 1.25, "output": 10.00 },
"limit": { "context": 1000000, "output": 65536 }
"gpt-5.5": {
"name": "GPT 5.5",
"cost": { "input": 5.00, "output": 30.00 },
"limit": { "context": 1050000, "output": 128000 },
"reasoning": true,
"variants": {
"xhigh": {
"reasoningEffort": "xhigh",
"reasoningSummary": "auto"
}
}
},
"gemini-2.5-flash": {
"name": "Gemini 2.5 Flash",
"cost": { "input": 0.30, "output": 2.50 },
"limit": { "context": 1000000, "output": 65536 }
"gemini-3.1-pro-preview": {
"name": "Gemini 3.1 Pro Preview",
"cost": { "input": 2.00, "output": 12.00 },
"limit": { "context": 1048576, "output": 65536 }
},
"gemini-2.5-flash-lite": {
"name": "Gemini 2.5 Flash Lite",
"cost": { "input": 0.10, "output": 0.40 },
"limit": { "context": 1000000, "output": 65536 }
"deepinfra/kimi-k2.6": {
"name": "Kimi K2.6",
"cost": { "input": 0.75, "output": 3.50 },
"limit": { "context": 262144, "output": 65536 }
},
"gemini-3-flash-preview": {
"name": "Gemini 3 Flash Preview",
"cost": { "input": 0.50, "output": 3.00 },
"limit": { "context": 1000000, "output": 65536 }
"deepinfra/glm-5.1": {
"name": "GLM 5.1",
"cost": { "input": 1.05, "output": 3.50 },
"limit": { "context": 202752, "output": 65536 }
},
"kimi-k2.5": {
"name": "Kimi K2.5",
"cost": { "input": 0.60, "output": 3.00 },
"limit": { "context": 256000, "output": 65536 }
"fireworks/minimax-m2p7": {
"name": "MiniMax M2.7",
"cost": { "input": 0.30, "output": 1.20 },
"limit": { "context": 196608, "output": 65536 }
},
"glm-4.7": {
"name": "GLM 4.7",
"cost": { "input": 0.60, "output": 2.20 },
"limit": { "context": 200000, "output": 128000 }
"deepinfra/deepseek-v4-pro": {
"name": "DeepSeek V4 Pro",
"cost": { "input": 1.74, "output": 3.48 },
"limit": { "context": 1048576, "output": 65536 }
},
"gpt-oss-120b": {
"name": "GPT OSS 120B",
"cost": { "input": 0.15, "output": 0.75 },
"limit": { "context": 131072, "output": 131072 }
"deepinfra/deepseek-v4-flash": {
"name": "DeepSeek V4 Flash",
"cost": { "input": 0.14, "output": 0.28 },
"limit": { "context": 1048576, "output": 65536 }
}
}
}
Expand Down
8 changes: 3 additions & 5 deletions src/agents/claude-desktop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { homedir, platform } from "node:os";
import { join, dirname } from "node:path";
import { OpperError } from "../errors.js";
import { OPPER_COMPAT_URL } from "../config/endpoints.js";
import { DEFAULT_MODELS } from "../config/models.js";
import { DEFAULT_MODELS, PICKER_MODELS } from "../config/models.js";
import { run } from "../util/run.js";
import type {
AgentAdapter,
Expand Down Expand Up @@ -240,12 +240,10 @@ async function writeGatewayProfile(
cfg.inferenceGatewayAuthScheme = "bearer";
cfg.disableDeploymentModeChooser = true;
// First entry is the picker default. Dedupe in case primaryModel
// equals one of the catalog defaults.
// equals one of the catalog ids.
const names = Array.from(new Set([
primaryModel,
DEFAULT_MODELS.opus,
DEFAULT_MODELS.sonnet,
DEFAULT_MODELS.haiku,
...PICKER_MODELS.map((m) => m.id),
]));
cfg.inferenceModels = names.map((name) => ({ name }));
await writeJson(path, cfg);
Expand Down
17 changes: 8 additions & 9 deletions src/agents/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,19 @@ import type {
} from "./types.js";

import { OPPER_COMPAT_URL } from "../config/endpoints.js";
import { DEFAULT_MODELS } from "../config/models.js";
import { PICKER_MODELS } from "../config/models.js";

const SENTINEL_OPEN = "# >>> opper-cli >>>";
const SENTINEL_CLOSE = "# <<< opper-cli <<<";
const DEFAULT_PROFILE = "opper-opus";

function buildOpperBlock(baseUrl: string): string {
const profileBlocks = PICKER_MODELS.flatMap((m) => [
"",
`[profiles.opper-${m.codexProfile}]`,
`model = "${m.id}"`,
'model_provider = "opper"',
]);
return [
SENTINEL_OPEN,
"# Managed by `opper`. Edits between these markers will be overwritten",
Expand All @@ -30,14 +36,7 @@ function buildOpperBlock(baseUrl: string): string {
`base_url = "${baseUrl}"`,
'env_key = "OPPER_API_KEY"',
'wire_api = "responses"',
"",
"[profiles.opper-opus]",
`model = "${DEFAULT_MODELS.opus}"`,
'model_provider = "opper"',
"",
"[profiles.opper-sonnet]",
`model = "${DEFAULT_MODELS.sonnet}"`,
'model_provider = "opper"',
...profileBlocks,
SENTINEL_CLOSE,
"",
].join("\n");
Expand Down
25 changes: 25 additions & 0 deletions src/agents/hermes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { which } from "../util/which.js";
import { run } from "../util/run.js";
import { opperHome } from "../auth/paths.js";
import { OpperError } from "../errors.js";
import { PICKER_MODELS } from "../config/models.js";
import type {
AgentAdapter,
DetectResult,
Expand Down Expand Up @@ -102,6 +103,30 @@ async function writeOpperConfig(routing: OpperRouting): Promise<void> {
default: routing.model,
};

// Register Opper as a named provider so Hermes' `/model` picker shows
// "Opper (N models)" alongside the built-in providers. Without this the
// picker only enumerates first-class providers (OpenRouter, Copilot,
// OpenAI…) with detected creds — our `model.provider: custom` route is
// invisible to it.
//
// We let Hermes auto-discover models from `<base_url>/v1/models` (the
// default behaviour) — same pattern Claude Code uses with our compat
// endpoint. The curated `models:` dict below is the fallback Hermes
// uses when discovery fails (network error, auth, etc.).
//
// `key_env: OPENAI_API_KEY` matches the env var we already export at
// spawn time, so no api key lands on disk.
const opperModels: Record<string, Record<string, never>> = {};
for (const m of PICKER_MODELS) opperModels[m.id] = {};
const providers = (existing.providers as Record<string, unknown> | undefined) ?? {};
providers.opper = {
name: "Opper",
base_url: routing.baseUrl,
key_env: "OPENAI_API_KEY",
models: opperModels,
};
existing.providers = providers;

await writeFile(path, stringify(existing), { mode: 0o600 });
}

Expand Down
39 changes: 12 additions & 27 deletions src/agents/openclaw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { run } from "../util/run.js";
import { OpperError } from "../errors.js";
import { npmInstallGlobal } from "./npm-install.js";
import { OPPER_COMPAT_URL } from "../config/endpoints.js";
import { DEFAULT_MODELS } from "../config/models.js";
import { DEFAULT_MODELS, pickerModelsForLaunch } from "../config/models.js";
import type {
AgentAdapter,
ConfigureOptions,
Expand Down Expand Up @@ -53,36 +53,21 @@ async function setOpperProvider(
): Promise<void> {
const cfg = await readConfig();
cfg.providers = cfg.providers ?? {};
// The launch model is reordered to index 0 — OpenClaw has no explicit
// _launch marker, so position-0 is the only signal that picks the
// active default in its picker UI.
cfg.providers[PROVIDER_KEY] = {
api: "openai-completions",
apiKey,
baseUrl,
models: [
{
id: launchModel,
name: launchModel,
api: "openai-completions",
reasoning: true,
input: ["text"],
contextWindow: 200000,
},
{
id: DEFAULT_MODELS.sonnet,
name: DEFAULT_MODELS.sonnet,
api: "openai-completions",
reasoning: true,
input: ["text"],
contextWindow: 200000,
},
{
id: DEFAULT_MODELS.haiku,
name: DEFAULT_MODELS.haiku,
api: "openai-completions",
reasoning: false,
input: ["text"],
contextWindow: 200000,
},
],
models: pickerModelsForLaunch(launchModel).map((m) => ({
id: m.id,
name: m.id,
api: "openai-completions",
reasoning: m.reasoning,
input: ["text"],
contextWindow: m.contextWindow,
})),
};
await writeConfig(cfg);
}
Expand Down
10 changes: 7 additions & 3 deletions src/agents/opencode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ async function isConfigured(): Promise<boolean> {
}

async function configure(): Promise<void> {
await configureOpenCode({ location: "global" });
// overwrite: true so a re-run pulls in the latest template (model list,
// costs, defaults). Without it, an existing `provider.opper` block from
// an older CLI version would be left in place and the new models would
// never appear in OpenCode's picker.
await configureOpenCode({ location: "global", overwrite: true });
}

async function unconfigure(): Promise<void> {
Expand Down Expand Up @@ -109,9 +113,9 @@ async function spawn(
// Explicit opt-in to writing the cwd-local config. We never silently
// mutate a project config the user didn't ask us to touch — that file
// is usually checked in.
await configureOpenCode({ location: "local" });
await configureOpenCode({ location: "local", overwrite: true });
} else {
await configureOpenCode({ location: "global" });
await configureOpenCode({ location: "global", overwrite: true });

// OpenCode reads `./opencode.json` if present and uses it instead of
// the user-level config. If one exists without an Opper provider,
Expand Down
33 changes: 11 additions & 22 deletions src/agents/pi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { run } from "../util/run.js";
import { OpperError } from "../errors.js";
import { npmInstallGlobal } from "./npm-install.js";
import { OPPER_COMPAT_URL } from "../config/endpoints.js";
import { DEFAULT_MODELS } from "../config/models.js";
import { DEFAULT_MODELS, pickerModelsForLaunch } from "../config/models.js";
import type {
AgentAdapter,
ConfigureOptions,
Expand Down Expand Up @@ -56,31 +56,20 @@ async function setOpperProvider(
): Promise<void> {
const cfg = await readConfig();
cfg.providers = cfg.providers ?? {};
// The launch model is reordered to index 0 (Pi treats the first entry as
// the active default in its picker UI). `_launch: true` is the explicit
// marker the runtime reads.
cfg.providers[PROVIDER_KEY] = {
api: "openai-completions",
apiKey,
baseUrl,
models: [
{
id: launchModel,
contextWindow: 200000,
input: ["text"],
reasoning: true,
_launch: true,
},
{
id: DEFAULT_MODELS.sonnet,
contextWindow: 200000,
input: ["text"],
reasoning: true,
},
{
id: DEFAULT_MODELS.haiku,
contextWindow: 200000,
input: ["text"],
reasoning: false,
},
],
models: pickerModelsForLaunch(launchModel).map((m) => ({
id: m.id,
contextWindow: m.contextWindow,
input: ["text"],
reasoning: m.reasoning,
...(m.id === launchModel ? { _launch: true } : {}),
})),
};
await writeConfig(cfg);
}
Expand Down
Loading