chat: surface OpenCode models in the picker (Phase 2)
list-models.ts now merges the OpenCode catalog (from `opencode models`, cached; ids are providerID/modelID) with the static Claude tiers, so /chat/models returns both. invalidateModelCache clears the OpenCode cache for real now. Adds the 'opencode' → 'OpenCode Zen' provider label in the /models response and the ModelSelector's PROVIDER_DISPLAY. The existing useModels visibility gates already pass non-claude-code providers through, so no gate changes are needed — Super Admin sees all models. Selecting any non-claude-code model routes the turn to the OpenCode harness (Phase 1). Default model stays 'claude-code'. 58 OpenCode Zen models currently list; curating to a flagship subset is an easy follow-up if the full catalog is unwieldy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -62,7 +62,7 @@ chatRouter.patch('/sessions/:id/title', async (ctx) => {
|
|||||||
chatRouter.get('/models', async (ctx: Context) => {
|
chatRouter.get('/models', async (ctx: Context) => {
|
||||||
try {
|
try {
|
||||||
const models = await listChatModels();
|
const models = await listChatModels();
|
||||||
const providerNames: Record<string, string> = { 'claude-code': 'Claude Code' };
|
const providerNames: Record<string, string> = { 'claude-code': 'Claude Code', opencode: 'OpenCode Zen' };
|
||||||
return ctx.json({ models, providerNames, hostHome: process.env.HOME ?? '' });
|
return ctx.json({ models, providerNames, hostHome: process.env.HOME ?? '' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Failed to list models', { error: String(err) });
|
logger.error('Failed to list models', { error: String(err) });
|
||||||
|
|||||||
@@ -1,16 +1,60 @@
|
|||||||
import type { ModelInfo } from './types';
|
import type { ModelInfo } from './types';
|
||||||
|
import { homedir } from 'os';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
// Claude-only. The runner is the `claude` CLI, so the model list is a fixed set of Claude tiers.
|
// The Claude harness runs the `claude` CLI, so its tiers are a fixed set.
|
||||||
const CLAUDE_CODE_MODELS: ModelInfo[] = [
|
const CLAUDE_CODE_MODELS: ModelInfo[] = [
|
||||||
{ id: 'claude-code/opus', name: 'opus', provider: 'claude-code', contextWindow: 200000, maxTokens: 16000, reasoning: true, images: true },
|
{ id: 'claude-code/opus', name: 'opus', provider: 'claude-code', contextWindow: 200000, maxTokens: 16000, reasoning: true, images: true },
|
||||||
{ id: 'claude-code/sonnet', name: 'sonnet', provider: 'claude-code', contextWindow: 200000, maxTokens: 16000, reasoning: true, images: true },
|
{ id: 'claude-code/sonnet', name: 'sonnet', provider: 'claude-code', contextWindow: 200000, maxTokens: 16000, reasoning: true, images: true },
|
||||||
{ id: 'claude-code/haiku', name: 'haiku', provider: 'claude-code', contextWindow: 200000, maxTokens: 8192, reasoning: false, images: true },
|
{ id: 'claude-code/haiku', name: 'haiku', provider: 'claude-code', contextWindow: 200000, maxTokens: 8192, reasoning: false, images: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const OPENCODE_BIN = process.env.OPENCODE_BIN || join(homedir(), '.opencode', 'bin', 'opencode');
|
||||||
|
|
||||||
|
// Cache the OpenCode catalog; it's stable for a session and `opencode models` costs a subprocess.
|
||||||
|
let openCodeCache: ModelInfo[] | null = null;
|
||||||
|
|
||||||
export function invalidateModelCache(): void {
|
export function invalidateModelCache(): void {
|
||||||
// No-op: the Claude model list is static. Kept for call-site compatibility.
|
openCodeCache = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enumerate OpenCode models via `opencode models` (ids are `providerID/modelID`, e.g.
|
||||||
|
// `opencode/claude-opus-4-8`). Metadata (context/tokens/modalities) is left at neutral defaults for
|
||||||
|
// now — it can be enriched later from a serve's GET /config/providers.
|
||||||
|
async function listOpenCodeModels(): Promise<ModelInfo[]> {
|
||||||
|
if (openCodeCache) return openCodeCache;
|
||||||
|
try {
|
||||||
|
const proc = Bun.spawn([OPENCODE_BIN, 'models'], { stdout: 'pipe', stderr: 'ignore' });
|
||||||
|
const out = await new Response(proc.stdout).text();
|
||||||
|
await proc.exited;
|
||||||
|
|
||||||
|
const models: ModelInfo[] = out
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
.replace(/\x1b\[[0-9;]*m/g, '')
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line.includes('/'))
|
||||||
|
.map((full) => {
|
||||||
|
const slash = full.indexOf('/');
|
||||||
|
return {
|
||||||
|
id: full,
|
||||||
|
name: full.slice(slash + 1),
|
||||||
|
provider: full.slice(0, slash),
|
||||||
|
contextWindow: 200000,
|
||||||
|
maxTokens: 8192,
|
||||||
|
reasoning: false,
|
||||||
|
images: true,
|
||||||
|
} satisfies ModelInfo;
|
||||||
|
});
|
||||||
|
|
||||||
|
openCodeCache = models;
|
||||||
|
return models;
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listChatModels(): Promise<ModelInfo[]> {
|
export async function listChatModels(): Promise<ModelInfo[]> {
|
||||||
return [...CLAUDE_CODE_MODELS];
|
const openCode = await listOpenCodeModels();
|
||||||
|
return [...CLAUDE_CODE_MODELS, ...openCode];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const PROVIDER_DISPLAY: Record<string, string> = {
|
|||||||
'google-vertex': 'Google Vertex AI',
|
'google-vertex': 'Google Vertex AI',
|
||||||
'azure-openai': 'Azure OpenAI',
|
'azure-openai': 'Azure OpenAI',
|
||||||
'claude-code': 'Claude Code',
|
'claude-code': 'Claude Code',
|
||||||
|
opencode: 'OpenCode Zen',
|
||||||
};
|
};
|
||||||
|
|
||||||
type ModelSelectorProps = {
|
type ModelSelectorProps = {
|
||||||
|
|||||||
Reference in New Issue
Block a user