feat: model lists and prices that follow each AI tool, a Settings tab per tool, live lane status (#40)
Pricing: the LiteLLM sync now adds every current Claude / GPT (>= 5) /
Gemini (>= 3) chat model before it is used and keeps its own rows
(model_pricing.source = 'sync', the Auto badge) at the published rate. A
user's row (any PUT) and a deleted pattern (model_pricing_dismissed) are
never touched; a deleted built-in row is not re-seeded at the next start; a
reset re-applies the list at once; retired Claude models are priced only
once used.
Models: GET /api/ai-tools/:id/models reads each tool's own list (Codex's
models_cache.json, Claude's aliases named after the newest version plus
priced versions, a catalog tool's vendor rows plus the models its sessions
used). A catalog `model: {flag, vendor?}` field passes `<flag> <id>`
(validated: never a flag or shell text); a per-tool default model starts
each run, and a resume keeps the session's own model.
Settings: every installed, enabled catalog tool without its own tab gets
one (AiToolConfig.tsx): path, homepage, Update, sessions/cost/lanes,
default model, vendor pricing, and the config files its release catalog
lists, edited over the opened sha, syntax-checked, backed up, written
atomically; credential-looking names refused. Antigravity keeps its hooks
card there.
Lanes: a just-started run no longer reads "dead" (age counts the run's
start while the old session is still bound); a second dashboard on the
same database re-broadcasts the lanes the other changed (PRAGMA
data_version, every 2 s); a lane start with a bad model answers 400.
DB: every migration runs under a cross-process lock (<db>-migrate.lock), so
two dashboards started at once on a fresh database both come up.
This commit is contained in:
+53
-15
@@ -1990,12 +1990,31 @@ export const api = {
|
||||
list: () => request<AiToolsView>("/ai-tools"),
|
||||
/** POST /api/ai-tools/refresh - re-read the login shell's PATH and re-detect. */
|
||||
refresh: () => request<AiToolsView>("/ai-tools/refresh", { method: "POST" }),
|
||||
/** PUT /api/ai-tools/:id - enable/disable a tool, or make it the default. */
|
||||
update: (id: string, body: { enabled?: boolean; default?: true }) =>
|
||||
/** PUT /api/ai-tools/:id - enable/disable a tool, make it the default, or set its default model ("" clears). */
|
||||
update: (id: string, body: AiToolUpdateBody) =>
|
||||
request<AiToolsView>(`/ai-tools/${encodeURIComponent(id)}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
/** GET /api/ai-tools/:id/models - the models the tool itself offers. */
|
||||
models: (id: string) => request<AiToolModels>(`/ai-tools/${encodeURIComponent(id)}/models`),
|
||||
/** GET /api/ai-tools/:id/usage - how many sessions the tool ran, and their cost. */
|
||||
usage: (id: string) =>
|
||||
request<{ sessions: number; cost: number }>(`/ai-tools/${encodeURIComponent(id)}/usage`),
|
||||
/** GET /api/ai-tools/:id/config - the config files the tool's catalog file lists. */
|
||||
config: (id: string) =>
|
||||
request<{ files: AiToolConfigFile[] }>(`/ai-tools/${encodeURIComponent(id)}/config`),
|
||||
/** GET /api/ai-tools/:id/config/:index - one config file's text and sha. */
|
||||
readConfig: (id: string, index: number) =>
|
||||
request<{ path: string; format: AiToolConfigFile["format"]; text: string; sha: string }>(
|
||||
`/ai-tools/${encodeURIComponent(id)}/config/${index}`
|
||||
),
|
||||
/** PUT /api/ai-tools/:id/config/:index - save over `baseSha` (409 ESTALE otherwise). */
|
||||
saveConfig: (id: string, index: number, text: string, baseSha: string) =>
|
||||
request<{ sha: string; backup: string }>(
|
||||
`/ai-tools/${encodeURIComponent(id)}/config/${index}`,
|
||||
{ method: "PUT", body: JSON.stringify({ text, baseSha }) }
|
||||
),
|
||||
/** POST /api/ai-tools/:id/install - start the catalog's install job. */
|
||||
install: (id: string) =>
|
||||
request<{ job: AiToolJob }>(`/ai-tools/${encodeURIComponent(id)}/install`, {
|
||||
@@ -3066,6 +3085,36 @@ export interface AiTool {
|
||||
install: { kind: "npm"; package: string } | { kind: "script"; url: string } | null;
|
||||
/** What Update runs; `self` is the tool's own updater with these args. */
|
||||
update: { kind: "npm" } | { kind: "script" } | { kind: "self"; args: string[] } | null;
|
||||
/** The CLI takes a model flag (Claude, Codex, or a catalog `model` field). */
|
||||
modelFlag: boolean;
|
||||
/** The model a new run starts with; null inherits the CLI's own. */
|
||||
defaultModel: string | null;
|
||||
/** Whose priced models the tool offers (its Settings tab's pricing table). */
|
||||
modelVendor: "claude" | "openai" | "google" | null;
|
||||
/** How many config files its catalog file lists for the Settings tab. */
|
||||
configFiles: number;
|
||||
}
|
||||
|
||||
/** One config file a catalog tool's Settings tab may edit. */
|
||||
export interface AiToolConfigFile {
|
||||
index: number;
|
||||
path: string;
|
||||
format: "json" | "jsonc" | "text";
|
||||
exists: boolean;
|
||||
size: number | null;
|
||||
}
|
||||
|
||||
export interface AiToolUpdateBody {
|
||||
enabled?: boolean;
|
||||
default?: true;
|
||||
defaultModel?: string | null;
|
||||
}
|
||||
|
||||
/** GET /api/ai-tools/:id/models - read from the tool's own source. */
|
||||
export interface AiToolModels {
|
||||
flag: boolean;
|
||||
models: ModelChoice[];
|
||||
defaultModel: string | null;
|
||||
}
|
||||
|
||||
export interface AiToolsView {
|
||||
@@ -3890,9 +3939,8 @@ export const RUN_EFFORT_CHOICES: EffortChoice[] = [
|
||||
{ id: "max", label: "Max", hint: "All-out - slowest, most tokens" },
|
||||
];
|
||||
|
||||
// Curated model list. "" means "inherit from settings.json" - no --model flag.
|
||||
// Static UI data for the Run page's model picker; the first entry inherits the
|
||||
// configured default and the rest map to concrete `--model` values.
|
||||
// Claude's fallback model list, used only when GET /api/ai-tools/claude/models
|
||||
// cannot answer. "" means "inherit from settings.json" - no --model flag.
|
||||
export const RUN_MODEL_CHOICES: ModelChoice[] = [
|
||||
{ id: "", label: "Inherit from settings", hint: "Use whatever your settings.json model is" },
|
||||
// Aliases, never pinned ids: `claude --model` documents `fable`/`opus`/
|
||||
@@ -3908,16 +3956,6 @@ export const RUN_MODEL_CHOICES: ModelChoice[] = [
|
||||
{ id: "haiku", label: "Haiku (latest)", hint: "Fastest, lightest" },
|
||||
];
|
||||
|
||||
// Curated model list for Codex lanes - same shape as RUN_MODEL_CHOICES, sent
|
||||
// via `codex -m`. "" inherits Codex's own configured default.
|
||||
export const RUN_MODEL_CHOICES_CODEX: ModelChoice[] = [
|
||||
{ id: "", label: "Inherit from settings", hint: "Use whatever your Codex config model is" },
|
||||
{ id: "gpt-5.6-sol", label: "GPT-5.6 Sol", hint: "Highest capability" },
|
||||
{ id: "gpt-5.6-terra", label: "GPT-5.6 Terra", hint: "Balanced capability and speed" },
|
||||
{ id: "gpt-5.6-luna", label: "GPT-5.6 Luna", hint: "Fastest, lightest" },
|
||||
{ id: "gpt-5.5", label: "GPT-5.5", hint: "Previous generation" },
|
||||
];
|
||||
|
||||
/** Result of a transcript import run - returned by `api.import.rescan`,
|
||||
* `scanPath`, and `upload`, and mirrored by the final `import.progress`
|
||||
* WebSocket message (`phase: "complete"`). The core counters (`imported`/
|
||||
|
||||
Reference in New Issue
Block a user