feat(FN-3227): add startup model sync for opencode-go commands
- Add shared startup model sync helper and integrate it into dashboard, serve, and daemon command startup flows - Introduce opencode-go model sync setting in core schema/types and expose it in dashboard settings UI - Add CLI regression tests covering startup model sync behavior and command wiring - Document the new startup model sync setting and related CLI behavior updates Fusion-Task-Id: FN-3227
This commit is contained in:
@@ -62,6 +62,7 @@ import { getCachedUpdateStatus, isUpdateCheckEnabled } from "../update-cache.js"
|
||||
import { resolveSelfExtension } from "./self-extension.js";
|
||||
import { ensureBundledDependencyGraphPluginInstalled } from "../plugins/bundled-plugin-install.js";
|
||||
import { registerCustomProviders, reregisterCustomProviders } from "./custom-provider-registry.js";
|
||||
import { syncStartupModels } from "./startup-model-sync.js";
|
||||
import { DashboardTUI, DashboardLogSink, isTTYAvailable, type SystemInfo, type GitStatus, type GitCommit, type GitCommitDetail, type GitBranch, type GitWorktree, type FileEntry, type FileReadResult, type TaskStep as TUITaskStep, type TaskLogEntry as TUITaskLogEntry, type TaskDetailData, type TaskEvent } from "./dashboard-tui/index.js";
|
||||
|
||||
// Re-export for backward compatibility with tests
|
||||
@@ -1351,48 +1352,6 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
||||
logSink.warn(`Failed to load custom providers from global settings: ${message}`, "custom-providers");
|
||||
}
|
||||
|
||||
// Eagerly sync OpenRouter models — the pi-openrouter-realtime extension
|
||||
// only registers providers on session_start (TUI-only event), so kick off
|
||||
// a fetch here so the dashboard model list is populated. Respects the
|
||||
// openrouterModelSync setting (defaults to true).
|
||||
(async () => {
|
||||
try {
|
||||
const settings = await store.getSettings();
|
||||
if (settings.openrouterModelSync === false) return;
|
||||
const hasOrAuth = await dashboardAuthStorage.getApiKey("openrouter");
|
||||
const headers: Record<string, string> = {};
|
||||
if (hasOrAuth) headers["Authorization"] = `Bearer ${hasOrAuth}`;
|
||||
const res = await fetch("https://openrouter.ai/api/v1/models", { headers });
|
||||
if (!res.ok) return;
|
||||
const json = await res.json() as { data?: Array<{ id: string; name: string; context_length?: number; top_provider?: { max_completion_tokens?: number }; pricing?: Record<string, string>; architecture?: { modality?: string; input_modalities?: string[] } }> };
|
||||
const orModels = (json.data || []).map((m) => {
|
||||
const id = (m.id || "").toLowerCase();
|
||||
const name = (m.name || "").toLowerCase();
|
||||
const reasoning = id.includes(":thinking") || id.includes("-r1") || id.includes("/r1") || id.includes("o1-") || id.includes("o3-") || id.includes("o4-") || id.includes("reasoner") || name.includes("thinking") || name.includes("reasoner");
|
||||
const hasVision = m.architecture?.input_modalities?.includes("image") ?? m.architecture?.modality?.includes("multimodal") ?? false;
|
||||
function parseCost(v?: string) { const n = parseFloat(v || "0"); return isNaN(n) ? 0 : n * 1_000_000; }
|
||||
return {
|
||||
id: m.id,
|
||||
name: m.name || m.id,
|
||||
reasoning,
|
||||
input: (hasVision ? ["text", "image"] : ["text"]) as ("text" | "image")[],
|
||||
cost: { input: parseCost(m.pricing?.prompt), output: parseCost(m.pricing?.completion), cacheRead: parseCost(m.pricing?.input_cache_read), cacheWrite: parseCost(m.pricing?.input_cache_write) },
|
||||
contextWindow: m.context_length || 128000,
|
||||
maxTokens: m.top_provider?.max_completion_tokens || 16384,
|
||||
};
|
||||
});
|
||||
modelRegistry.registerProvider("openrouter", {
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
apiKey: "OPENROUTER_API_KEY",
|
||||
api: "openai-completions",
|
||||
models: orModels,
|
||||
});
|
||||
logSink.log(`Synced ${orModels.length} models from OpenRouter API`, "openrouter");
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
logSink.log(`Failed to sync models: ${message}`, "openrouter");
|
||||
}
|
||||
})();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
logSink.log(`Failed to discover extensions: ${message}`, "extensions");
|
||||
@@ -1400,6 +1359,13 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
||||
modelRegistry.refresh();
|
||||
}
|
||||
|
||||
void syncStartupModels({
|
||||
getSettings: () => store.getSettings(),
|
||||
authStorage: dashboardAuthStorage,
|
||||
modelRegistry,
|
||||
log: (scope, message) => logSink.log(message, scope),
|
||||
});
|
||||
|
||||
registerHandler(store, "settings:updated", ({ settings, previous }) => {
|
||||
const currentProviders = settings.customProviders;
|
||||
const previousProviders = previous.customProviders;
|
||||
|
||||
Reference in New Issue
Block a user