mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 21:22:28 +03:00
- canonical executable manifest (bin/cli/cli-manifest.mjs): run/configure/completion derive targets, aliases and --model wiring from one table; drift test cross-checks manifest x cliRuntime x UI catalog (tests/unit/cli/cli-manifest-drift.test.ts) - dashboard Codex generator converged to ~/.codex/config.toml (modern Codex v0.137+, verified against codex-cli 0.147.0): conservative merge, env_key auth (key never written), refuses invalid TOML, reports legacy config.yaml as migration note - omniroute run gemini: launcher over OmniRoute's /v1beta surface via GOOGLE_GEMINI_BASE_URL + isolated GEMINI_CLI_HOME forcing gemini-api-key auth (contract proven against @google/gemini-cli 0.50.0); ACP registration kept distinct - opt-in real smoke harness for upstream CLIs (RUN_CLI_SMOKE=1, credential by env NAME, redacted output): tests/integration/upstream-cli-smoke.int.test.ts - container-guard homologation for POST /api/cli-tools/apply (422 in container, dry-run preview allowed, host write passes) + docs; guard untouched - typecheck: omniglyphAdapter union narrowing, usageTracking typed signatures (UsageLike, no any), models.ts isValidModel params — typecheck:core and typecheck:noimplicit:core now clean - relay core (prior session of this effort): omniroute run for 6 CLIs, configure picker with per-context favorites/recents, contexts with optional keychain + 0600 fallback, provider CRUD with recursive redaction, completion updates, docs
110 lines
3.7 KiB
JavaScript
110 lines
3.7 KiB
JavaScript
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
|
|
import { join, dirname } from "node:path";
|
|
import { resolveDataDir } from "./data-dir.mjs";
|
|
|
|
const PREFERENCES_VERSION = 1;
|
|
const MAX_RECENT = 12;
|
|
const MAX_FAVORITES = 32;
|
|
|
|
export function modelPreferencesPath() {
|
|
return join(resolveDataDir(), "model-preferences.json");
|
|
}
|
|
|
|
function defaultPreferences() {
|
|
return { version: PREFERENCES_VERSION, targets: {}, contexts: {} };
|
|
}
|
|
|
|
export function loadModelPreferences() {
|
|
try {
|
|
const path = modelPreferencesPath();
|
|
if (!existsSync(path)) return defaultPreferences();
|
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
return defaultPreferences();
|
|
}
|
|
return {
|
|
version: PREFERENCES_VERSION,
|
|
targets: parsed.targets && typeof parsed.targets === "object" ? parsed.targets : {},
|
|
contexts: parsed.contexts && typeof parsed.contexts === "object" ? parsed.contexts : {},
|
|
};
|
|
} catch {
|
|
return defaultPreferences();
|
|
}
|
|
}
|
|
|
|
function saveModelPreferences(preferences) {
|
|
const path = modelPreferencesPath();
|
|
mkdirSync(dirname(path), { recursive: true });
|
|
writeFileSync(path, JSON.stringify(preferences, null, 2));
|
|
try {
|
|
chmodSync(path, 0o600);
|
|
} catch {
|
|
// Best effort on platforms without POSIX modes.
|
|
}
|
|
}
|
|
|
|
function normalizeIds(values) {
|
|
return [...new Set((Array.isArray(values) ? values : []).filter((id) => typeof id === "string"))];
|
|
}
|
|
|
|
function targetState(preferences, target, contextKey) {
|
|
const raw = contextKey
|
|
? preferences.contexts?.[contextKey]?.[target] ||
|
|
(contextKey === "default" ? preferences.targets?.[target] : undefined)
|
|
: preferences.targets?.[target];
|
|
return {
|
|
favorites: normalizeIds(raw?.favorites),
|
|
recent: normalizeIds(raw?.recent),
|
|
};
|
|
}
|
|
|
|
function writeTargetState(preferences, target, contextKey) {
|
|
if (!contextKey) {
|
|
preferences.targets[target] = targetState(preferences, target);
|
|
return preferences.targets[target];
|
|
}
|
|
preferences.contexts = preferences.contexts || {};
|
|
preferences.contexts[contextKey] = preferences.contexts[contextKey] || {};
|
|
preferences.contexts[contextKey][target] = targetState(preferences, target, contextKey);
|
|
return preferences.contexts[contextKey][target];
|
|
}
|
|
|
|
/** Rank catalog IDs with favorites first, then recent choices, then catalog order. */
|
|
export function rankPreferredModels(
|
|
target,
|
|
modelIds,
|
|
preferences = loadModelPreferences(),
|
|
contextKey = ""
|
|
) {
|
|
const ids = normalizeIds(modelIds);
|
|
const state = targetState(preferences, target, contextKey);
|
|
const available = new Set(ids);
|
|
const preferred = [...state.favorites, ...state.recent].filter((id) => available.has(id));
|
|
return [...new Set([...preferred, ...ids])];
|
|
}
|
|
|
|
/** Record a successful selection without storing server URLs or credentials. */
|
|
export function recordModelPreference(target, modelId, options = {}) {
|
|
if (!target || !modelId) return loadModelPreferences();
|
|
const preferences = loadModelPreferences();
|
|
const state = writeTargetState(preferences, target, options.context || "");
|
|
state.recent = [modelId, ...state.recent.filter((id) => id !== modelId)].slice(0, MAX_RECENT);
|
|
if (options.favorite) {
|
|
state.favorites = [modelId, ...state.favorites.filter((id) => id !== modelId)].slice(
|
|
0,
|
|
MAX_FAVORITES
|
|
);
|
|
}
|
|
if (options.unfavorite) state.favorites = state.favorites.filter((id) => id !== modelId);
|
|
saveModelPreferences(preferences);
|
|
return preferences;
|
|
}
|
|
|
|
export function getModelPreferenceState(
|
|
target,
|
|
preferences = loadModelPreferences(),
|
|
contextKey = ""
|
|
) {
|
|
return targetState(preferences, target, contextKey);
|
|
}
|