Auto-discover OpenCode Zen and Go models, add catalog search and pagination
Add model fetchers for OpenCode Zen (https://opencode.ai/zen/v1/models) and Go (https://opencode.ai/zen/go/v1/models) APIs. Register opencode-go as a new provider, load shared credentials from auth.json, add known models with pricing, and create default agents for both tiers on first boot. Replace the manual "Add Model" form with a search bar that filters by model name/ID and paginate the catalog at 25 models per page.
This commit is contained in:
@@ -730,6 +730,7 @@ const PROVIDER_BADGE: Record<string, { short: string; bg: string; color: string;
|
||||
openrouter: { short: "OR", bg: tokens.color.infoDim, color: tokens.color.info, border: tokens.color.infoDim },
|
||||
google: { short: "GG", bg: tokens.color.infoDim, color: tokens.color.info, border: tokens.color.infoDim },
|
||||
"opencode-zen": { short: "OZ", bg: tokens.color.purpleDim, color: tokens.color.purple, border: tokens.color.purpleDim },
|
||||
"opencode-go": { short: "OG", bg: tokens.color.passDim, color: tokens.color.pass, border: tokens.color.passDim },
|
||||
};
|
||||
|
||||
function ProviderBadge({ provider }: { provider: string }) {
|
||||
@@ -1153,13 +1154,8 @@ function ModelsTab({ mobile }: { mobile: boolean }) {
|
||||
const [runtimes, setRuntimes] = useState<AgentRuntimeDisplay[]>([]);
|
||||
const [view, setView] = useState<"agents" | "catalog" | "usage">("agents");
|
||||
const [providerFilter, setProviderFilter] = useState("ALL");
|
||||
const [addingModel, setAddingModel] = useState(false);
|
||||
const [newModelId, setNewModelId] = useState("");
|
||||
const [newModelName, setNewModelName] = useState("");
|
||||
const [newModelProvider, setNewModelProvider] = useState("anthropic");
|
||||
const [newModelCtx, setNewModelCtx] = useState("");
|
||||
const [newModelCostIn, setNewModelCostIn] = useState("");
|
||||
const [newModelCostOut, setNewModelCostOut] = useState("");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [catalogPage, setCatalogPage] = useState(1);
|
||||
// Agent creation state
|
||||
const [addingAgent, setAddingAgent] = useState(false);
|
||||
const [newAgentName, setNewAgentName] = useState("");
|
||||
@@ -1196,27 +1192,19 @@ function ModelsTab({ mobile }: { mobile: boolean }) {
|
||||
loadModels();
|
||||
};
|
||||
|
||||
const addModel = async () => {
|
||||
if (!newModelId.trim() || !newModelProvider) return;
|
||||
await fetch("/api/models/curated", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
id: newModelId.trim(),
|
||||
name: newModelName.trim() || newModelId.trim(),
|
||||
provider: newModelProvider,
|
||||
contextWindow: newModelCtx ? parseInt(newModelCtx) : undefined,
|
||||
costPer1kInput: newModelCostIn ? parseFloat(newModelCostIn) : undefined,
|
||||
costPer1kOutput: newModelCostOut ? parseFloat(newModelCostOut) : undefined,
|
||||
}),
|
||||
});
|
||||
setNewModelId(""); setNewModelName(""); setNewModelCtx("");
|
||||
setNewModelCostIn(""); setNewModelCostOut(""); setAddingModel(false);
|
||||
loadModels();
|
||||
};
|
||||
|
||||
const providers = ["ALL", ...Array.from(new Set(models.map(m => m.provider)))];
|
||||
const filtered = providerFilter === "ALL" ? models : models.filter(m => m.provider === providerFilter);
|
||||
const CATALOG_PAGE_SIZE = 25;
|
||||
const filtered = models.filter(m => {
|
||||
if (providerFilter !== "ALL" && m.provider !== providerFilter) return false;
|
||||
if (searchQuery) {
|
||||
const q = searchQuery.toLowerCase();
|
||||
return m.id.toLowerCase().includes(q) || m.name.toLowerCase().includes(q);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const totalPages = Math.max(1, Math.ceil(filtered.length / CATALOG_PAGE_SIZE));
|
||||
const clampedPage = Math.min(catalogPage, totalPages);
|
||||
const paginatedModels = filtered.slice((clampedPage - 1) * CATALOG_PAGE_SIZE, clampedPage * CATALOG_PAGE_SIZE);
|
||||
|
||||
// Aggregate usage by provider
|
||||
const providerTotals = usage.summary.reduce((acc, s) => {
|
||||
@@ -1263,12 +1251,6 @@ function ModelsTab({ mobile }: { mobile: boolean }) {
|
||||
{addingAgent ? "CANCEL" : "+ NEW AGENT"}
|
||||
</Btn>
|
||||
)}
|
||||
{view === "catalog" && (
|
||||
<Btn variant="primary" onClick={() => setAddingModel(!addingModel)}
|
||||
style={{ fontSize: tokens.size.xs, minHeight: 32 }}>
|
||||
{addingModel ? "CANCEL" : "+ ADD MODEL"}
|
||||
</Btn>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ─── AGENTS VIEW ─── */}
|
||||
@@ -1321,7 +1303,7 @@ function ModelsTab({ mobile }: { mobile: boolean }) {
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: tokens.space[1] }}>
|
||||
<Label color={tokens.color.text3}>Provider</Label>
|
||||
<div style={{ display: "flex", gap: tokens.space[1] }}>
|
||||
{["anthropic", "openai", "openrouter", "google", "opencode-zen"].map(p => (
|
||||
{["anthropic", "openai", "openrouter", "google", "opencode-zen", "opencode-go"].map(p => (
|
||||
<button key={p} onClick={() => setNewAgentProvider(p)}
|
||||
style={{
|
||||
background: newAgentProvider === p ? tokens.color.bg3 : "transparent",
|
||||
@@ -1425,64 +1407,17 @@ function ModelsTab({ mobile }: { mobile: boolean }) {
|
||||
{/* ─── CATALOG VIEW ─── */}
|
||||
{view === "catalog" && (
|
||||
<>
|
||||
{/* Add model form */}
|
||||
{addingModel && (
|
||||
<Panel style={{ padding: tokens.space[3] }}>
|
||||
{/* Search + provider filter */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: tokens.space[2] }}>
|
||||
<Label color={tokens.color.accent}>ADD MODEL TO CATALOG</Label>
|
||||
<div style={{ display: "flex", flexDirection: mobile ? "column" : "row", gap: tokens.space[2] }}>
|
||||
<div style={{ flex: 2, display: "flex", flexDirection: "column", gap: tokens.space[1] }}>
|
||||
<Label color={tokens.color.text3}>Model ID</Label>
|
||||
<Input value={newModelId} onChange={e => setNewModelId(e.target.value)} placeholder="claude-sonnet-4-20250514" />
|
||||
</div>
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", gap: tokens.space[1] }}>
|
||||
<Label color={tokens.color.text3}>Display Name</Label>
|
||||
<Input value={newModelName} onChange={e => setNewModelName(e.target.value)} placeholder="Claude Sonnet 4" />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: mobile ? "column" : "row", gap: tokens.space[2] }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: tokens.space[1] }}>
|
||||
<Label color={tokens.color.text3}>Provider</Label>
|
||||
<div style={{ display: "flex", gap: tokens.space[1], flexWrap: "wrap" }}>
|
||||
{["anthropic", "openai", "openrouter", "google", "opencode-zen"].map(p => (
|
||||
<button key={p} onClick={() => setNewModelProvider(p)}
|
||||
style={{
|
||||
background: newModelProvider === p ? tokens.color.bg3 : "transparent",
|
||||
border: `1px solid ${newModelProvider === p ? tokens.color.border1 : tokens.color.border0}`,
|
||||
color: newModelProvider === p ? tokens.color.text0 : tokens.color.text2,
|
||||
fontFamily: tokens.font.mono, fontSize: tokens.size.xs,
|
||||
padding: `2px ${tokens.space[2]}px`, cursor: "pointer", borderRadius: 0,
|
||||
}}>
|
||||
{p.toUpperCase()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", gap: tokens.space[1] }}>
|
||||
<Label color={tokens.color.text3}>Context Window</Label>
|
||||
<Input value={newModelCtx} onChange={e => setNewModelCtx(e.target.value)} placeholder="200000" />
|
||||
</div>
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", gap: tokens.space[1] }}>
|
||||
<Label color={tokens.color.text3}>$/1k Input</Label>
|
||||
<Input value={newModelCostIn} onChange={e => setNewModelCostIn(e.target.value)} placeholder="0.003" />
|
||||
</div>
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", gap: tokens.space[1] }}>
|
||||
<Label color={tokens.color.text3}>$/1k Output</Label>
|
||||
<Input value={newModelCostOut} onChange={e => setNewModelCostOut(e.target.value)} placeholder="0.015" />
|
||||
</div>
|
||||
</div>
|
||||
<Btn variant="primary" onClick={addModel} disabled={!newModelId.trim()}
|
||||
style={{ alignSelf: "flex-start" }}>
|
||||
ADD TO CATALOG
|
||||
</Btn>
|
||||
</div>
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
{/* Provider filter */}
|
||||
<div style={{ display: "flex", gap: tokens.space[1], flexWrap: "wrap" }}>
|
||||
<Input
|
||||
value={searchQuery}
|
||||
onChange={e => { setSearchQuery(e.target.value); setCatalogPage(1); }}
|
||||
placeholder="Search models by name or ID..."
|
||||
style={{ maxWidth: 400 }}
|
||||
/>
|
||||
<div style={{ display: "flex", gap: tokens.space[1], flexWrap: "wrap", alignItems: "center" }}>
|
||||
{providers.map(p => (
|
||||
<button key={p} onClick={() => setProviderFilter(p)}
|
||||
<button key={p} onClick={() => { setProviderFilter(p); setCatalogPage(1); }}
|
||||
style={{
|
||||
background: providerFilter === p ? tokens.color.bg3 : "transparent",
|
||||
border: `1px solid ${providerFilter === p ? tokens.color.border1 : tokens.color.border0}`,
|
||||
@@ -1493,11 +1428,15 @@ function ModelsTab({ mobile }: { mobile: boolean }) {
|
||||
{p.toUpperCase()}
|
||||
</button>
|
||||
))}
|
||||
<Mono size={tokens.size.xs} color={tokens.color.text3} style={{ marginLeft: tokens.space[2] }}>
|
||||
{filtered.length} model{filtered.length !== 1 ? "s" : ""}
|
||||
</Mono>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model list */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: tokens.space[2] }}>
|
||||
{filtered.map(m => (
|
||||
{paginatedModels.map(m => (
|
||||
<Panel key={m.id} style={{
|
||||
borderLeft: `3px solid ${m.enabled ? tokens.color.pass : tokens.color.border0}`,
|
||||
opacity: m.enabled ? 1 : 0.6,
|
||||
@@ -1551,6 +1490,25 @@ function ModelsTab({ mobile }: { mobile: boolean }) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: tokens.space[3] }}>
|
||||
<Btn variant="default" onClick={() => setCatalogPage(p => Math.max(1, p - 1))}
|
||||
disabled={clampedPage <= 1}
|
||||
style={{ fontSize: tokens.size.xs, minHeight: 32 }}>
|
||||
PREV
|
||||
</Btn>
|
||||
<Mono size={tokens.size.sm} color={tokens.color.text2}>
|
||||
{clampedPage} / {totalPages}
|
||||
</Mono>
|
||||
<Btn variant="default" onClick={() => setCatalogPage(p => Math.min(totalPages, p + 1))}
|
||||
disabled={clampedPage >= totalPages}
|
||||
style={{ fontSize: tokens.size.xs, minHeight: 32 }}>
|
||||
NEXT
|
||||
</Btn>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -39,8 +39,8 @@ export const AGENT_RUNTIMES: Record<AgentRuntime, AgentRuntimeInfo> = {
|
||||
"opencode": {
|
||||
id: "opencode",
|
||||
name: "OpenCode",
|
||||
description: "Open-source multi-provider coding agent. Supports Anthropic, OpenAI, Google, OpenRouter.",
|
||||
defaultProviders: ["anthropic", "openai", "google", "openrouter", "opencode-zen"],
|
||||
description: "Open-source multi-provider coding agent. Supports Anthropic, OpenAI, Google, OpenRouter, OpenCode Zen/Go.",
|
||||
defaultProviders: ["anthropic", "openai", "google", "openrouter", "opencode-zen", "opencode-go"],
|
||||
cliCommand: "opencode",
|
||||
headlessFlag: "run",
|
||||
modelFlag: "--model",
|
||||
|
||||
@@ -7,17 +7,33 @@ import { upsertCuratedModel, getCuratedModels } from "./model-store";
|
||||
import { upsertAgentConfig, getAllAgentConfigs, type AgentRuntime } from "./agents";
|
||||
import { fetchAllModels } from "./model-providers";
|
||||
|
||||
// Well-known models with pricing
|
||||
// Well-known models with pricing (per 1k tokens)
|
||||
const KNOWN_MODELS: Record<string, { name: string; provider: string; contextWindow: number; costPer1kInput: number; costPer1kOutput: number }> = {
|
||||
// Anthropic (direct)
|
||||
"claude-opus-4-20250514": { name: "Claude Opus 4", provider: "anthropic", contextWindow: 200000, costPer1kInput: 0.015, costPer1kOutput: 0.075 },
|
||||
"claude-sonnet-4-20250514": { name: "Claude Sonnet 4", provider: "anthropic", contextWindow: 200000, costPer1kInput: 0.003, costPer1kOutput: 0.015 },
|
||||
"claude-haiku-4-20250514": { name: "Claude Haiku 4", provider: "anthropic", contextWindow: 200000, costPer1kInput: 0.0008, costPer1kOutput: 0.004 },
|
||||
// OpenAI (direct)
|
||||
"gpt-4o": { name: "GPT-4o", provider: "openai", contextWindow: 128000, costPer1kInput: 0.0025, costPer1kOutput: 0.01 },
|
||||
"gpt-4o-mini": { name: "GPT-4o Mini", provider: "openai", contextWindow: 128000, costPer1kInput: 0.00015, costPer1kOutput: 0.0006 },
|
||||
"o3": { name: "o3", provider: "openai", contextWindow: 200000, costPer1kInput: 0.01, costPer1kOutput: 0.04 },
|
||||
"o4-mini": { name: "o4 Mini", provider: "openai", contextWindow: 200000, costPer1kInput: 0.0011, costPer1kOutput: 0.0044 },
|
||||
// Google (direct)
|
||||
"gemini-2.5-pro": { name: "Gemini 2.5 Pro", provider: "google", contextWindow: 1048576, costPer1kInput: 0.00125, costPer1kOutput: 0.01 },
|
||||
"gemini-2.5-flash": { name: "Gemini 2.5 Flash", provider: "google", contextWindow: 1048576, costPer1kInput: 0.00015, costPer1kOutput: 0.0006 },
|
||||
// OpenCode Zen (pricing per 1M from docs, converted to per 1k)
|
||||
"claude-opus-4.6": { name: "Claude Opus 4.6", provider: "opencode-zen", contextWindow: 200000, costPer1kInput: 0.005, costPer1kOutput: 0.025 },
|
||||
"claude-sonnet-4.6": { name: "Claude Sonnet 4.6", provider: "opencode-zen", contextWindow: 200000, costPer1kInput: 0.003, costPer1kOutput: 0.015 },
|
||||
"claude-haiku-4.5": { name: "Claude Haiku 4.5", provider: "opencode-zen", contextWindow: 200000, costPer1kInput: 0.0008, costPer1kOutput: 0.004 },
|
||||
"gpt-5.4": { name: "GPT-5.4", provider: "opencode-zen", contextWindow: 200000, costPer1kInput: 0.0025, costPer1kOutput: 0.015 },
|
||||
"gpt-5.3-codex": { name: "GPT-5.3 Codex", provider: "opencode-zen", contextWindow: 200000, costPer1kInput: 0.002, costPer1kOutput: 0.008 },
|
||||
"gemini-3.1-pro": { name: "Gemini 3.1 Pro", provider: "opencode-zen", contextWindow: 1048576, costPer1kInput: 0.00125, costPer1kOutput: 0.005 },
|
||||
"gemini-3-flash": { name: "Gemini 3 Flash", provider: "opencode-zen", contextWindow: 1048576, costPer1kInput: 0.0005, costPer1kOutput: 0.003 },
|
||||
// OpenCode Go (fixed subscription, no per-token pricing)
|
||||
"glm-5": { name: "GLM-5", provider: "opencode-go", contextWindow: 128000, costPer1kInput: 0, costPer1kOutput: 0 },
|
||||
"kimi-k2.5": { name: "Kimi K2.5", provider: "opencode-go", contextWindow: 128000, costPer1kInput: 0, costPer1kOutput: 0 },
|
||||
"minimax-m2.7": { name: "MiniMax M2.7", provider: "opencode-go", contextWindow: 128000, costPer1kInput: 0, costPer1kOutput: 0 },
|
||||
"minimax-m2.5": { name: "MiniMax M2.5", provider: "opencode-go", contextWindow: 128000, costPer1kInput: 0, costPer1kOutput: 0 },
|
||||
};
|
||||
|
||||
// Default agents to create per provider when credentials are available.
|
||||
@@ -35,6 +51,12 @@ const DEFAULT_AGENTS: Record<string, { runtime: AgentRuntime; models: string[] }
|
||||
openrouter: [
|
||||
{ runtime: "opencode", models: ["claude-sonnet-4-20250514"] },
|
||||
],
|
||||
"opencode-zen": [
|
||||
{ runtime: "opencode", models: ["claude-sonnet-4.6", "gpt-5.3-codex"] },
|
||||
],
|
||||
"opencode-go": [
|
||||
{ runtime: "opencode", models: ["kimi-k2.5", "glm-5"] },
|
||||
],
|
||||
};
|
||||
|
||||
const RUNTIME_LABELS: Record<AgentRuntime, string> = {
|
||||
@@ -55,6 +77,7 @@ async function loadCredentialsFromEnv() {
|
||||
["GOOGLE_API_KEY", "google", "Google (env)"],
|
||||
["OPENROUTER_API_KEY", "openrouter", "OpenRouter (env)"],
|
||||
["OPENCODE_ZEN_API_KEY", "opencode-zen", "OpenCode Zen (env)"],
|
||||
["OPENCODE_GO_API_KEY", "opencode-go", "OpenCode Go (env)"],
|
||||
["GITHUB_TOKEN", "github", "GitHub (env)"],
|
||||
["GH_TOKEN", "github", "GitHub (env)"],
|
||||
["GITLAB_TOKEN", "gitlab", "GitLab (env)"],
|
||||
@@ -100,19 +123,20 @@ async function loadOpenCodeCredentials() {
|
||||
|
||||
try {
|
||||
const raw = JSON.parse(readFileSync(authPath, "utf-8"));
|
||||
const providerMap: Record<string, Provider> = {
|
||||
anthropic: "anthropic",
|
||||
openai: "openai",
|
||||
google: "google",
|
||||
openrouter: "openrouter",
|
||||
opencode: "opencode-zen",
|
||||
const providerMap: Record<string, Provider[]> = {
|
||||
anthropic: ["anthropic"],
|
||||
openai: ["openai"],
|
||||
google: ["google"],
|
||||
openrouter: ["openrouter"],
|
||||
opencode: ["opencode-zen", "opencode-go"], // Zen and Go share the same key
|
||||
};
|
||||
|
||||
for (const [key, entry] of Object.entries(raw)) {
|
||||
const provider = providerMap[key];
|
||||
if (!provider || typeof entry !== "object" || !entry) continue;
|
||||
const providers = providerMap[key];
|
||||
if (!providers || typeof entry !== "object" || !entry) continue;
|
||||
const token = (entry as Record<string, unknown>).key;
|
||||
if (typeof token !== "string" || !token) continue;
|
||||
for (const provider of providers) {
|
||||
const existing = await getRawCredentialsByProvider(provider);
|
||||
if (existing.length > 0) continue;
|
||||
await upsertCredential({
|
||||
@@ -122,6 +146,7 @@ async function loadOpenCodeCredentials() {
|
||||
token,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore parse errors
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ import { credentials as credentialsTable } from "@homelab/db";
|
||||
|
||||
export type Provider =
|
||||
| "github" | "gitlab"
|
||||
| "anthropic" | "openai" | "openrouter" | "google" | "opencode-zen";
|
||||
| "anthropic" | "openai" | "openrouter" | "google" | "opencode-zen" | "opencode-go";
|
||||
|
||||
export const GIT_PROVIDERS: Provider[] = ["github", "gitlab"];
|
||||
export const AI_PROVIDERS: Provider[] = ["anthropic", "openai", "openrouter", "google", "opencode-zen"];
|
||||
export const AI_PROVIDERS: Provider[] = ["anthropic", "openai", "openrouter", "google", "opencode-zen", "opencode-go"];
|
||||
|
||||
export interface Credential {
|
||||
id: string;
|
||||
|
||||
@@ -13,6 +13,8 @@ export async function fetchAllModels(): Promise<ModelInfo[]> {
|
||||
fetchOpenAIModels(),
|
||||
fetchOpenRouterModels(),
|
||||
fetchGoogleModels(),
|
||||
fetchOpenCodeZenModels(),
|
||||
fetchOpenCodeGoModels(),
|
||||
]);
|
||||
|
||||
return results.flatMap(r => r.status === "fulfilled" ? r.value : []);
|
||||
@@ -133,3 +135,67 @@ async function fetchGoogleModels(): Promise<ModelInfo[]> {
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function fetchOpenCodeZenModels(): Promise<ModelInfo[]> {
|
||||
const creds = await getRawCredentialsByProvider("opencode-zen");
|
||||
if (creds.length === 0) return [];
|
||||
|
||||
for (const cred of creds) {
|
||||
try {
|
||||
const res = await fetch("https://opencode.ai/zen/v1/models", {
|
||||
headers: { Authorization: `Bearer ${cred.token}` },
|
||||
});
|
||||
|
||||
if (!res.ok) continue;
|
||||
|
||||
const data = await res.json();
|
||||
return (data.data || data.models || []).map((m: { id: string; name?: string; context_length?: number }) => ({
|
||||
id: m.id,
|
||||
name: m.name || m.id,
|
||||
provider: "opencode-zen",
|
||||
contextWindow: m.context_length,
|
||||
}));
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function fetchOpenCodeGoModels(): Promise<ModelInfo[]> {
|
||||
// OpenCode Go shares the same API key as Zen — try zen creds, then go-specific creds
|
||||
const creds = [
|
||||
...await getRawCredentialsByProvider("opencode-go"),
|
||||
...await getRawCredentialsByProvider("opencode-zen"),
|
||||
];
|
||||
if (creds.length === 0) return [];
|
||||
|
||||
for (const cred of creds) {
|
||||
try {
|
||||
const res = await fetch("https://opencode.ai/zen/go/v1/models", {
|
||||
headers: { Authorization: `Bearer ${cred.token}` },
|
||||
});
|
||||
|
||||
if (!res.ok) continue;
|
||||
|
||||
const data = await res.json();
|
||||
return (data.data || data.models || []).map((m: { id: string; name?: string; context_length?: number }) => ({
|
||||
id: m.id,
|
||||
name: m.name || m.id,
|
||||
provider: "opencode-go",
|
||||
contextWindow: m.context_length,
|
||||
}));
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: hardcoded Go models if API unavailable but credentials exist
|
||||
return [
|
||||
{ id: "glm-5", name: "GLM-5", provider: "opencode-go" },
|
||||
{ id: "kimi-k2.5", name: "Kimi K2.5", provider: "opencode-go" },
|
||||
{ id: "minimax-m2.7", name: "MiniMax M2.7", provider: "opencode-go" },
|
||||
{ id: "minimax-m2.5", name: "MiniMax M2.5", provider: "opencode-go" },
|
||||
];
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user