@gentbajko/slopify 1.0.1 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter-registry.js +8 -1
- package/dist/adapters/llm/claude-code-models.js +200 -0
- package/dist/adapters/llm/claude-code.js +3 -10
- package/dist/adapters/llm/codex-models.js +12 -1
- package/dist/adapters/llm/codex.js +2 -4
- package/dist/adapters/llm/gemini-models.js +51 -13
- package/dist/adapters/llm/gemini.js +2 -3
- package/dist/assets/models.yaml +8 -129
- package/dist/catalog/registry.js +58 -0
- package/dist/catalog/runtime-models.js +16 -0
- package/dist/catalog/store.js +9 -1
- package/dist/catalog/validate.js +2 -1
- package/dist/edge/http/providers.js +2 -2
- package/dist/main.js +2 -1
- package/dist/model-catalog.js +0 -6
- package/dist/slices/control/providers.js +4 -6
- package/dist/slices/play-drafts/readiness.js +9 -3
- package/dist/slices/play-drafts/review.js +18 -0
- package/dist/slices/rebuild/service-readiness.js +12 -3
- package/dist/slices/settings/model.js +3 -0
- package/dist/slices/settings/models.js +12 -6
- package/package.json +1 -1
package/dist/adapter-registry.js
CHANGED
|
@@ -3,6 +3,7 @@ import { googleImage } from "./adapters/image/google.js";
|
|
|
3
3
|
import { openAiImage } from "./adapters/image/openai.js";
|
|
4
4
|
import { replicateImage } from "./adapters/image/replicate.js";
|
|
5
5
|
import { claudeCodeLlm } from "./adapters/llm/claude-code.js";
|
|
6
|
+
import { nodeClaudeCodeModels } from "./adapters/llm/claude-code-models.js";
|
|
6
7
|
import { codexLlm } from "./adapters/llm/codex.js";
|
|
7
8
|
import { nodeCodexModels } from "./adapters/llm/codex-models.js";
|
|
8
9
|
import { geminiLlm } from "./adapters/llm/gemini.js";
|
|
@@ -28,7 +29,13 @@ export function buildRegistry(deps) {
|
|
|
28
29
|
const llms = new Map([
|
|
29
30
|
["openrouter", openRouterLlm({ fetch: deps.fetch, key: keyOf("openrouter") })],
|
|
30
31
|
// Each CLI authenticates with its own login.
|
|
31
|
-
[
|
|
32
|
+
[
|
|
33
|
+
"claude-code",
|
|
34
|
+
claudeCodeLlm({
|
|
35
|
+
run: cliFor("claude-code"),
|
|
36
|
+
readModels: () => nodeClaudeCodeModels(cliBinary(deps.db, "claude-code")),
|
|
37
|
+
}),
|
|
38
|
+
],
|
|
32
39
|
["codex", codexLlm({ run: cliFor("codex"), readModels: () => nodeCodexModels() })],
|
|
33
40
|
[
|
|
34
41
|
"gemini",
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { cliCommand } from "../../kernel/cli-command.js";
|
|
8
|
+
const maxOutputBytes = 1024 * 1024;
|
|
9
|
+
const modelId = z
|
|
10
|
+
.string()
|
|
11
|
+
.trim()
|
|
12
|
+
.min(1)
|
|
13
|
+
.max(256)
|
|
14
|
+
.refine((value) => [...value].every((character) => {
|
|
15
|
+
const code = character.charCodeAt(0);
|
|
16
|
+
return code >= 33 && code !== 127;
|
|
17
|
+
}));
|
|
18
|
+
const modelName = z.string().trim().min(1).max(256);
|
|
19
|
+
const responseSchema = z.object({
|
|
20
|
+
type: z.literal("control_response"),
|
|
21
|
+
response: z.object({
|
|
22
|
+
request_id: z.string(),
|
|
23
|
+
subtype: z.literal("success"),
|
|
24
|
+
response: z.object({
|
|
25
|
+
models: z
|
|
26
|
+
.array(z.object({
|
|
27
|
+
value: modelId,
|
|
28
|
+
resolvedModel: modelId.optional(),
|
|
29
|
+
displayName: modelName,
|
|
30
|
+
supportedEffortLevels: z.array(z.string()).max(10).optional(),
|
|
31
|
+
}))
|
|
32
|
+
.min(1)
|
|
33
|
+
.max(100),
|
|
34
|
+
}),
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
const supportedEfforts = ["low", "medium", "high", "xhigh"];
|
|
38
|
+
function unavailable() {
|
|
39
|
+
return new Error("Claude Code model discovery is unavailable. Refresh the list or enter an exact model ID.");
|
|
40
|
+
}
|
|
41
|
+
function discoveryEnv() {
|
|
42
|
+
const names = [
|
|
43
|
+
"HOME",
|
|
44
|
+
"USER",
|
|
45
|
+
"LOGNAME",
|
|
46
|
+
"USERPROFILE",
|
|
47
|
+
"APPDATA",
|
|
48
|
+
"LOCALAPPDATA",
|
|
49
|
+
"XDG_CONFIG_HOME",
|
|
50
|
+
"XDG_DATA_HOME",
|
|
51
|
+
"XDG_CACHE_HOME",
|
|
52
|
+
"PATH",
|
|
53
|
+
"PATHEXT",
|
|
54
|
+
"LANG",
|
|
55
|
+
"LC_ALL",
|
|
56
|
+
"LC_CTYPE",
|
|
57
|
+
"TMPDIR",
|
|
58
|
+
"TEMP",
|
|
59
|
+
"TMP",
|
|
60
|
+
"SystemRoot",
|
|
61
|
+
"WINDIR",
|
|
62
|
+
];
|
|
63
|
+
const env = {};
|
|
64
|
+
for (const name of names) {
|
|
65
|
+
if (process.env[name] !== undefined)
|
|
66
|
+
env[name] = process.env[name];
|
|
67
|
+
}
|
|
68
|
+
return env;
|
|
69
|
+
}
|
|
70
|
+
function choices(event, requestId) {
|
|
71
|
+
const parsed = responseSchema.safeParse(event);
|
|
72
|
+
if (!parsed.success || parsed.data.response.request_id !== requestId)
|
|
73
|
+
throw unavailable();
|
|
74
|
+
const models = new Map();
|
|
75
|
+
for (const row of parsed.data.response.response.models) {
|
|
76
|
+
const modes = row.supportedEffortLevels?.filter((value) => supportedEfforts.includes(value));
|
|
77
|
+
const thinking = modes?.length ? { thinkingModes: modes } : {};
|
|
78
|
+
if (!models.has(row.value))
|
|
79
|
+
models.set(row.value, {
|
|
80
|
+
id: row.value,
|
|
81
|
+
name: row.resolvedModel === undefined || row.resolvedModel === row.value
|
|
82
|
+
? row.displayName
|
|
83
|
+
: `${row.displayName} → ${row.resolvedModel}`,
|
|
84
|
+
...thinking,
|
|
85
|
+
});
|
|
86
|
+
// Claude advertises stable aliases and their current concrete targets.
|
|
87
|
+
// Offer both so the user can choose a moving alias or pin a version.
|
|
88
|
+
if (row.resolvedModel !== undefined && !models.has(row.resolvedModel))
|
|
89
|
+
models.set(row.resolvedModel, {
|
|
90
|
+
id: row.resolvedModel,
|
|
91
|
+
name: `${row.resolvedModel} (exact model)`,
|
|
92
|
+
...thinking,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return [...models.values()];
|
|
96
|
+
}
|
|
97
|
+
// The installed CLI has no model-list command; initialize is a no-prompt
|
|
98
|
+
// stream-JSON control exchange. Treat protocol drift as unavailable metadata.
|
|
99
|
+
export async function nodeClaudeCodeModels(binary, timeoutMs = 15_000) {
|
|
100
|
+
const directory = await mkdtemp(join(tmpdir(), "slopify-claude-models-"));
|
|
101
|
+
const requestId = randomUUID();
|
|
102
|
+
const command = cliCommand(binary);
|
|
103
|
+
const child = spawn(command.file, [
|
|
104
|
+
...command.args,
|
|
105
|
+
"-p",
|
|
106
|
+
"--input-format",
|
|
107
|
+
"stream-json",
|
|
108
|
+
"--output-format",
|
|
109
|
+
"stream-json",
|
|
110
|
+
"--verbose",
|
|
111
|
+
"--safe-mode",
|
|
112
|
+
"--tools",
|
|
113
|
+
"",
|
|
114
|
+
"--strict-mcp-config",
|
|
115
|
+
"--no-session-persistence",
|
|
116
|
+
], {
|
|
117
|
+
cwd: directory,
|
|
118
|
+
env: discoveryEnv(),
|
|
119
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
120
|
+
windowsHide: true,
|
|
121
|
+
detached: process.platform !== "win32",
|
|
122
|
+
});
|
|
123
|
+
child.on("error", () => { });
|
|
124
|
+
child.stdin.on("error", () => { });
|
|
125
|
+
child.stderr.resume();
|
|
126
|
+
const closed = new Promise((resolve) => child.once("close", () => resolve()));
|
|
127
|
+
const terminate = (force) => {
|
|
128
|
+
const signal = force ? "SIGKILL" : "SIGTERM";
|
|
129
|
+
if (process.platform !== "win32" && child.pid !== undefined) {
|
|
130
|
+
try {
|
|
131
|
+
process.kill(-child.pid, signal);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// A child that already exited has no process group to signal.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
child.kill(signal);
|
|
139
|
+
};
|
|
140
|
+
let forceTimer;
|
|
141
|
+
const timer = setTimeout(() => {
|
|
142
|
+
terminate(false);
|
|
143
|
+
forceTimer = setTimeout(() => terminate(true), 500);
|
|
144
|
+
}, timeoutMs);
|
|
145
|
+
try {
|
|
146
|
+
child.stdin.write(`${JSON.stringify({
|
|
147
|
+
request_id: requestId,
|
|
148
|
+
type: "control_request",
|
|
149
|
+
request: { subtype: "initialize" },
|
|
150
|
+
})}\n`);
|
|
151
|
+
let output = "";
|
|
152
|
+
let bytes = 0;
|
|
153
|
+
for await (const piece of child.stdout) {
|
|
154
|
+
bytes += piece.length;
|
|
155
|
+
if (bytes > maxOutputBytes)
|
|
156
|
+
throw unavailable();
|
|
157
|
+
output += piece.toString();
|
|
158
|
+
let newline = output.indexOf("\n");
|
|
159
|
+
while (newline >= 0) {
|
|
160
|
+
const line = output.slice(0, newline).trim();
|
|
161
|
+
output = output.slice(newline + 1);
|
|
162
|
+
if (line !== "") {
|
|
163
|
+
let event;
|
|
164
|
+
try {
|
|
165
|
+
event = JSON.parse(line);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
throw unavailable();
|
|
169
|
+
}
|
|
170
|
+
if (typeof event === "object" &&
|
|
171
|
+
event !== null &&
|
|
172
|
+
"type" in event &&
|
|
173
|
+
event.type === "control_response") {
|
|
174
|
+
return choices(event, requestId);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
newline = output.indexOf("\n");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
throw unavailable();
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
throw unavailable();
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
clearTimeout(timer);
|
|
187
|
+
if (forceTimer !== undefined)
|
|
188
|
+
clearTimeout(forceTimer);
|
|
189
|
+
terminate(false);
|
|
190
|
+
const grace = await Promise.race([
|
|
191
|
+
closed.then(() => true),
|
|
192
|
+
new Promise((resolve) => setTimeout(() => resolve(false), 500)),
|
|
193
|
+
]);
|
|
194
|
+
if (!grace) {
|
|
195
|
+
terminate(true);
|
|
196
|
+
await Promise.race([closed, new Promise((resolve) => setTimeout(resolve, 500))]);
|
|
197
|
+
}
|
|
198
|
+
await rm(directory, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { redact } from "../../kernel/log.js";
|
|
3
3
|
import { providerError } from "../../kernel/ports/model.js";
|
|
4
|
+
import { nodeClaudeCodeModels } from "./claude-code-models.js";
|
|
4
5
|
import { cliEvent, cliShaped, endedWithout, promptOf, stopCliRun } from "./run-cli.js";
|
|
5
6
|
import { lines } from "./sse-lines.js";
|
|
6
7
|
// The local-agent adapter for Claude Code: spawned non-interactively, authenticated by the
|
|
@@ -8,15 +9,6 @@ import { lines } from "./sse-lines.js";
|
|
|
8
9
|
// may not import `slices/**`, and `slices/settings/cli-status.ts` already probes the binary per
|
|
9
10
|
// request. The registry `main.ts` builds is where this adapter and that probe meet.
|
|
10
11
|
export const claudeCodeBinary = "claude";
|
|
11
|
-
// Official stable family aliases resolve to the latest model available to the
|
|
12
|
-
// installed CLI/account. Full model IDs remain available through custom entry.
|
|
13
|
-
// https://code.claude.com/docs/en/model-config
|
|
14
|
-
export const claudeCodeModels = [
|
|
15
|
-
{ id: "fable", name: "Claude Fable (latest)" },
|
|
16
|
-
{ id: "opus", name: "Claude Opus (latest)" },
|
|
17
|
-
{ id: "sonnet", name: "Claude Sonnet (latest)" },
|
|
18
|
-
{ id: "haiku", name: "Claude Haiku (latest)" },
|
|
19
|
-
];
|
|
20
12
|
const writingRole = "You are the writing and research component of Slopify, a video creation app. " +
|
|
21
13
|
"Produce the text requested in the supplied conversation: articles, narration scripts, " +
|
|
22
14
|
"research notes or image prompts. Follow the requested topic, language, tone, length and format. " +
|
|
@@ -47,6 +39,7 @@ export function claudeCodeArgs(req) {
|
|
|
47
39
|
? ["--tools", "WebSearch", "--allowedTools", "WebSearch"]
|
|
48
40
|
: ["--tools", ""]),
|
|
49
41
|
...(req.model === "" ? [] : ["--model", req.model]),
|
|
42
|
+
...(req.thinking !== undefined && req.thinking !== "off" ? ["--effort", req.thinking] : []),
|
|
50
43
|
// Everything after `--` is the prompt, so a prompt opening with a dash is text and not
|
|
51
44
|
// a flag. It is one argv element: no shell sees it and nothing in it is expanded.
|
|
52
45
|
"--",
|
|
@@ -142,7 +135,7 @@ export function claudeCodeLlm(deps) {
|
|
|
142
135
|
id: "claude-code",
|
|
143
136
|
// Partial messages keep the deadline alive; complete assistant turns supply prose.
|
|
144
137
|
capabilities: { streams: true, reportsUsage: true, webSearch: true },
|
|
145
|
-
models: () =>
|
|
138
|
+
models: deps.readModels ?? (() => nodeClaudeCodeModels(binary)),
|
|
146
139
|
complete,
|
|
147
140
|
};
|
|
148
141
|
}
|
|
@@ -17,6 +17,10 @@ const model = z.object({
|
|
|
17
17
|
display_name: safeText.optional(),
|
|
18
18
|
visibility: z.literal("list"),
|
|
19
19
|
priority: z.number().finite().optional(),
|
|
20
|
+
supported_reasoning_levels: z
|
|
21
|
+
.array(z.object({ effort: z.string() }))
|
|
22
|
+
.max(20)
|
|
23
|
+
.optional(),
|
|
20
24
|
});
|
|
21
25
|
export async function nodeCodexModels(env = process.env) {
|
|
22
26
|
try {
|
|
@@ -32,7 +36,14 @@ export async function nodeCodexModels(env = process.env) {
|
|
|
32
36
|
const unique = new Map();
|
|
33
37
|
for (const item of models) {
|
|
34
38
|
if (!unique.has(item.slug)) {
|
|
35
|
-
|
|
39
|
+
const thinkingModes = item.supported_reasoning_levels
|
|
40
|
+
?.map((level) => level.effort)
|
|
41
|
+
.filter((level) => ["off", "low", "medium", "high", "xhigh"].includes(level));
|
|
42
|
+
unique.set(item.slug, {
|
|
43
|
+
id: item.slug,
|
|
44
|
+
name: item.display_name ?? item.slug,
|
|
45
|
+
...(thinkingModes?.length ? { thinkingModes } : {}),
|
|
46
|
+
});
|
|
36
47
|
}
|
|
37
48
|
}
|
|
38
49
|
if (unique.size === 0)
|
|
@@ -4,15 +4,13 @@ import { join } from "node:path";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { redact } from "../../kernel/log.js";
|
|
6
6
|
import { providerError } from "../../kernel/ports/model.js";
|
|
7
|
+
import { nodeCodexModels } from "./codex-models.js";
|
|
7
8
|
import { cliEvent, cliShaped, endedWithout, promptOf, stopCliRun } from "./run-cli.js";
|
|
8
9
|
import { lines } from "./sse-lines.js";
|
|
9
10
|
// The local-agent adapter for the Codex CLI. Same shape as Claude Code's and a different
|
|
10
11
|
// vocabulary: Codex writes a JSONL thread of `thread.started`, `item.*` and `turn.*`
|
|
11
12
|
// events. No key here either - the CLI's own login authenticates it.
|
|
12
13
|
export const codexBinary = "codex";
|
|
13
|
-
// Codex publishes its account-specific picker in models_cache.json. With no
|
|
14
|
-
// cache, offer custom entry rather than pretending a pinned model is current.
|
|
15
|
-
export const codexModels = [];
|
|
16
14
|
const writingRole = "You are the writing and research component of Slopify, a video creation app. " +
|
|
17
15
|
"Produce the text requested in the supplied conversation: articles, narration scripts, " +
|
|
18
16
|
"research notes or image prompts. Follow the requested topic, language, tone, length and format. " +
|
|
@@ -181,7 +179,7 @@ export function codexLlm(deps) {
|
|
|
181
179
|
id: "codex",
|
|
182
180
|
// Prose arrives as whole messages; other JSONL events carry activity separately.
|
|
183
181
|
capabilities: { streams: true, reportsUsage: true, webSearch: true },
|
|
184
|
-
models: deps.readModels ?? (() =>
|
|
182
|
+
models: deps.readModels ?? (() => nodeCodexModels()),
|
|
185
183
|
complete,
|
|
186
184
|
};
|
|
187
185
|
}
|
|
@@ -3,14 +3,6 @@ import { access, realpath } from "node:fs/promises";
|
|
|
3
3
|
import { delimiter, dirname, isAbsolute, join } from "node:path";
|
|
4
4
|
import { cliCommand } from "../../kernel/cli-command.js";
|
|
5
5
|
import { readCatalogueFile } from "./catalogue-files.js";
|
|
6
|
-
// Official CLI aliases follow the installed CLI's model routing and account.
|
|
7
|
-
// https://geminicli.com/docs/cli/model/
|
|
8
|
-
export const geminiModels = [
|
|
9
|
-
{ id: "auto", name: "Gemini Auto (CLI default)" },
|
|
10
|
-
{ id: "pro", name: "Gemini Pro (CLI alias)" },
|
|
11
|
-
{ id: "flash", name: "Gemini Flash (CLI alias)" },
|
|
12
|
-
{ id: "flash-lite", name: "Gemini Flash-Lite (CLI alias)" },
|
|
13
|
-
];
|
|
14
6
|
export async function nodeGeminiModels(binary) {
|
|
15
7
|
try {
|
|
16
8
|
const command = cliCommand(binary);
|
|
@@ -31,24 +23,56 @@ export async function nodeGeminiModels(binary) {
|
|
|
31
23
|
for (const candidate of candidates) {
|
|
32
24
|
try {
|
|
33
25
|
const models = parseGeminiModels(await readCatalogueFile(candidate, 256 * 1024));
|
|
34
|
-
if (models.length >
|
|
26
|
+
if (models.length > 1)
|
|
35
27
|
return models;
|
|
36
28
|
}
|
|
37
29
|
catch {
|
|
38
30
|
// A candidate is an optional package layout, not the final discovery result.
|
|
39
31
|
}
|
|
40
32
|
}
|
|
33
|
+
// Recent Gemini CLI releases ship only bundled chunks. Follow the entry
|
|
34
|
+
// point's literal chunk imports and read their model constants as text.
|
|
35
|
+
const bundled = await bundledModels(entry);
|
|
36
|
+
if (bundled.length > 1)
|
|
37
|
+
return bundled;
|
|
41
38
|
throw new Error("No installed model metadata");
|
|
42
39
|
}
|
|
43
40
|
catch {
|
|
44
41
|
throw new Error("Gemini CLI model metadata is unavailable. Use a documented CLI alias or enter a custom model ID.");
|
|
45
42
|
}
|
|
46
43
|
}
|
|
44
|
+
async function bundledModels(entry) {
|
|
45
|
+
const source = await readCatalogueFile(entry, 256 * 1024);
|
|
46
|
+
const imports = new Set();
|
|
47
|
+
for (const match of source.matchAll(/\b(?:from\s+|import\s*)["']\.\/(chunk-[A-Za-z0-9-]+\.js)["']/g)) {
|
|
48
|
+
if (match[1] !== undefined)
|
|
49
|
+
imports.add(match[1]);
|
|
50
|
+
}
|
|
51
|
+
if (imports.size === 0 || imports.size > 32)
|
|
52
|
+
return [];
|
|
53
|
+
for (const name of imports) {
|
|
54
|
+
try {
|
|
55
|
+
const source = await readCatalogueFile(join(dirname(entry), name), 24 * 1024 * 1024);
|
|
56
|
+
if (!source.includes("packages/core/dist/src/config/models.js"))
|
|
57
|
+
continue;
|
|
58
|
+
const models = parseGeminiModels(source);
|
|
59
|
+
if (models.length > 1)
|
|
60
|
+
return models;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Other chunks can be absent or too large; do not execute them.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
47
68
|
function parseGeminiModels(source) {
|
|
48
|
-
const models = new Map(
|
|
49
|
-
|
|
69
|
+
const models = new Map();
|
|
70
|
+
let pro = false;
|
|
71
|
+
let flash = false;
|
|
72
|
+
let flashLite = false;
|
|
73
|
+
// Read literal model constants only. Never import or execute an
|
|
50
74
|
// installed package, parse comments as entries, or inspect login/settings.
|
|
51
|
-
const declarations = /^export
|
|
75
|
+
const declarations = /^(?:export\s+)?(?:const|let|var)\s+([A-Z][A-Z0-9_]*)\s*=\s*(['"])([^'"\r\n]+)\2\s*;/gm;
|
|
52
76
|
const uncommented = source.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
53
77
|
for (const match of uncommented.matchAll(declarations)) {
|
|
54
78
|
const name = match[1];
|
|
@@ -62,8 +86,22 @@ function parseGeminiModels(source) {
|
|
|
62
86
|
if (!/^(?:(?:PREVIEW|DEFAULT|SECONDARY)_GEMINI_|GEMMA_)/.test(name))
|
|
63
87
|
continue;
|
|
64
88
|
models.set(id, { id, name: id });
|
|
89
|
+
if (name.includes("FLASH_LITE") || id.includes("flash-lite"))
|
|
90
|
+
flashLite = true;
|
|
91
|
+
else if (name.includes("FLASH") || id.includes("flash"))
|
|
92
|
+
flash = true;
|
|
93
|
+
else if (name.includes("PRO") || id.includes("-pro"))
|
|
94
|
+
pro = true;
|
|
65
95
|
}
|
|
66
|
-
|
|
96
|
+
if (models.size === 0)
|
|
97
|
+
return [];
|
|
98
|
+
return [
|
|
99
|
+
{ id: "auto", name: "Gemini Auto (CLI default)" },
|
|
100
|
+
...(pro ? [{ id: "pro", name: "Gemini Pro (CLI alias)" }] : []),
|
|
101
|
+
...(flash ? [{ id: "flash", name: "Gemini Flash (CLI alias)" }] : []),
|
|
102
|
+
...(flashLite ? [{ id: "flash-lite", name: "Gemini Flash-Lite (CLI alias)" }] : []),
|
|
103
|
+
...models.values(),
|
|
104
|
+
];
|
|
67
105
|
}
|
|
68
106
|
async function executablePath(binary) {
|
|
69
107
|
if (isAbsolute(binary) || /[\\/]/.test(binary))
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { redact } from "../../kernel/log.js";
|
|
3
3
|
import { providerError } from "../../kernel/ports/model.js";
|
|
4
|
-
import {
|
|
4
|
+
import { nodeGeminiModels } from "./gemini-models.js";
|
|
5
5
|
import { geminiWorkspace } from "./gemini-workspace.js";
|
|
6
6
|
import { cliEvent, cliShaped, endedWithout, promptOf, stopCliRun } from "./run-cli.js";
|
|
7
7
|
import { lines } from "./sse-lines.js";
|
|
8
8
|
export const geminiBinary = "gemini";
|
|
9
|
-
export { geminiModels } from "./gemini-models.js";
|
|
10
9
|
// Verified against installed Gemini CLI 0.16.0's config/nonInteractiveCli sources.
|
|
11
10
|
// Its --allowed-tools flag bypasses approval; tools.core in the isolated settings
|
|
12
11
|
// actually restricts discovery. Never request yolo/auto_edit.
|
|
@@ -103,7 +102,7 @@ export function geminiLlm(deps) {
|
|
|
103
102
|
return {
|
|
104
103
|
id: "gemini",
|
|
105
104
|
capabilities: { streams: true, reportsUsage: true, webSearch: true },
|
|
106
|
-
models: deps.readModels ?? (() =>
|
|
105
|
+
models: deps.readModels ?? (() => nodeGeminiModels(binary)),
|
|
107
106
|
complete,
|
|
108
107
|
};
|
|
109
108
|
}
|
package/dist/assets/models.yaml
CHANGED
|
@@ -2,16 +2,10 @@
|
|
|
2
2
|
# A private copy in the Slopify data directory reloads automatically.
|
|
3
3
|
# Omitted pricing means unknown, never free. Provider concurrency is capped at five.
|
|
4
4
|
schemaVersion: 1
|
|
5
|
-
updatedAt: 2026-09-
|
|
5
|
+
updatedAt: 2026-09-24
|
|
6
6
|
providers:
|
|
7
7
|
openrouter:
|
|
8
8
|
maxConcurrent: 3
|
|
9
|
-
claude-code:
|
|
10
|
-
maxConcurrent: 3
|
|
11
|
-
codex:
|
|
12
|
-
maxConcurrent: 3
|
|
13
|
-
gemini:
|
|
14
|
-
maxConcurrent: 3
|
|
15
9
|
elevenlabs:
|
|
16
10
|
maxConcurrent: 3
|
|
17
11
|
openai-tts:
|
|
@@ -29,127 +23,6 @@ providers:
|
|
|
29
23
|
google-image:
|
|
30
24
|
maxConcurrent: 3
|
|
31
25
|
llm:
|
|
32
|
-
- provider: claude-code
|
|
33
|
-
id: opus
|
|
34
|
-
name: Claude Opus (latest)
|
|
35
|
-
source: https://code.claude.com/docs/en/model-config
|
|
36
|
-
keywords:
|
|
37
|
-
- llm
|
|
38
|
-
pricing: &a1
|
|
39
|
-
note: Uses your CLI login and account plan. No per-run price is available; paid
|
|
40
|
-
overage may apply.
|
|
41
|
-
llm:
|
|
42
|
-
webSearch: true
|
|
43
|
-
- provider: claude-code
|
|
44
|
-
id: sonnet
|
|
45
|
-
name: Claude Sonnet (latest)
|
|
46
|
-
source: https://code.claude.com/docs/en/model-config
|
|
47
|
-
keywords:
|
|
48
|
-
- llm
|
|
49
|
-
pricing: *a1
|
|
50
|
-
llm:
|
|
51
|
-
webSearch: true
|
|
52
|
-
- provider: claude-code
|
|
53
|
-
id: haiku
|
|
54
|
-
name: Claude Haiku (latest)
|
|
55
|
-
source: https://code.claude.com/docs/en/model-config
|
|
56
|
-
keywords:
|
|
57
|
-
- llm
|
|
58
|
-
pricing: *a1
|
|
59
|
-
llm:
|
|
60
|
-
webSearch: true
|
|
61
|
-
- provider: codex
|
|
62
|
-
id: gpt-6-astra
|
|
63
|
-
name: GPT-6-Astra
|
|
64
|
-
source: https://developers.openai.com/codex/models
|
|
65
|
-
keywords:
|
|
66
|
-
- llm
|
|
67
|
-
pricing: *a1
|
|
68
|
-
llm:
|
|
69
|
-
webSearch: true
|
|
70
|
-
thinking: &a2
|
|
71
|
-
low:
|
|
72
|
-
effort: low
|
|
73
|
-
medium:
|
|
74
|
-
effort: medium
|
|
75
|
-
high:
|
|
76
|
-
effort: high
|
|
77
|
-
- provider: codex
|
|
78
|
-
id: gpt-5.6-sol
|
|
79
|
-
name: GPT-5.6-Sol
|
|
80
|
-
source: https://developers.openai.com/codex/models
|
|
81
|
-
keywords:
|
|
82
|
-
- llm
|
|
83
|
-
pricing: *a1
|
|
84
|
-
llm:
|
|
85
|
-
webSearch: true
|
|
86
|
-
thinking: *a2
|
|
87
|
-
- provider: codex
|
|
88
|
-
id: gpt-5.6-terra
|
|
89
|
-
name: GPT-5.6-Terra
|
|
90
|
-
source: https://developers.openai.com/codex/models
|
|
91
|
-
keywords:
|
|
92
|
-
- llm
|
|
93
|
-
pricing: *a1
|
|
94
|
-
llm:
|
|
95
|
-
webSearch: true
|
|
96
|
-
thinking: *a2
|
|
97
|
-
- provider: codex
|
|
98
|
-
id: gpt-5.6-luna
|
|
99
|
-
name: GPT-5.6-Luna
|
|
100
|
-
source: https://developers.openai.com/codex/models
|
|
101
|
-
keywords:
|
|
102
|
-
- llm
|
|
103
|
-
pricing: *a1
|
|
104
|
-
llm:
|
|
105
|
-
webSearch: true
|
|
106
|
-
thinking: *a2
|
|
107
|
-
- provider: codex
|
|
108
|
-
id: gpt-5.5
|
|
109
|
-
name: GPT-5.5
|
|
110
|
-
source: https://developers.openai.com/codex/models
|
|
111
|
-
keywords:
|
|
112
|
-
- llm
|
|
113
|
-
pricing: *a1
|
|
114
|
-
llm:
|
|
115
|
-
webSearch: true
|
|
116
|
-
thinking: *a2
|
|
117
|
-
- provider: codex
|
|
118
|
-
id: gpt-5.3-codex-spark
|
|
119
|
-
name: GPT-5.3-Codex-Spark
|
|
120
|
-
source: https://developers.openai.com/codex/models
|
|
121
|
-
keywords:
|
|
122
|
-
- llm
|
|
123
|
-
pricing: *a1
|
|
124
|
-
llm:
|
|
125
|
-
webSearch: true
|
|
126
|
-
thinking: *a2
|
|
127
|
-
- provider: gemini
|
|
128
|
-
id: gemini-3.8-flash
|
|
129
|
-
name: Gemini 3.8 Flash
|
|
130
|
-
source: https://ai.google.dev/gemini-api/docs/models
|
|
131
|
-
keywords:
|
|
132
|
-
- llm
|
|
133
|
-
pricing: *a1
|
|
134
|
-
llm:
|
|
135
|
-
webSearch: true
|
|
136
|
-
thinking: &a3
|
|
137
|
-
low:
|
|
138
|
-
level: low
|
|
139
|
-
medium:
|
|
140
|
-
level: medium
|
|
141
|
-
high:
|
|
142
|
-
level: high
|
|
143
|
-
- provider: gemini
|
|
144
|
-
id: gemini-3.1-pro-preview
|
|
145
|
-
name: Gemini 3.1 Pro (preview)
|
|
146
|
-
source: https://ai.google.dev/gemini-api/docs/models
|
|
147
|
-
keywords:
|
|
148
|
-
- llm
|
|
149
|
-
pricing: *a1
|
|
150
|
-
llm:
|
|
151
|
-
webSearch: true
|
|
152
|
-
thinking: *a3
|
|
153
26
|
- provider: openrouter
|
|
154
27
|
id: google/gemini-3.8-flash
|
|
155
28
|
name: "Google: Gemini 3.8 Flash"
|
|
@@ -164,7 +37,13 @@ llm:
|
|
|
164
37
|
llm:
|
|
165
38
|
webSearch: true
|
|
166
39
|
contextTokens: 1048576
|
|
167
|
-
thinking:
|
|
40
|
+
thinking: &a2
|
|
41
|
+
low:
|
|
42
|
+
effort: low
|
|
43
|
+
medium:
|
|
44
|
+
effort: medium
|
|
45
|
+
high:
|
|
46
|
+
effort: high
|
|
168
47
|
- provider: openrouter
|
|
169
48
|
id: google/gemini-3.1-pro-preview
|
|
170
49
|
name: "Google: Gemini 3.1 Pro Preview"
|
package/dist/catalog/registry.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { providerError } from "../kernel/ports/model.js";
|
|
2
|
+
import { isLocalCliProvider } from "../slices/settings/model.js";
|
|
2
3
|
export function curateRegistry(registry, catalogue) {
|
|
3
4
|
const requireModel = (provider, family, id) => {
|
|
4
5
|
const models = catalogue.models(provider, family);
|
|
@@ -14,6 +15,49 @@ export function curateRegistry(registry, catalogue) {
|
|
|
14
15
|
list: registry.list,
|
|
15
16
|
llm: (id) => {
|
|
16
17
|
const port = registry.llm(id);
|
|
18
|
+
if (isLocalCliProvider(id)) {
|
|
19
|
+
return {
|
|
20
|
+
...port,
|
|
21
|
+
models: () => port.models(),
|
|
22
|
+
complete: async function* (request) {
|
|
23
|
+
if (request.model.trim() === "")
|
|
24
|
+
throw providerError({ kind: "unsupported", message: "Choose a model ID." });
|
|
25
|
+
let models;
|
|
26
|
+
try {
|
|
27
|
+
models = await port.models();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// A failed metadata read permits an exact manual ID; generation still
|
|
31
|
+
// decides whether the installed CLI/account accepts it.
|
|
32
|
+
}
|
|
33
|
+
const selected = models?.find((model) => model.id === request.model);
|
|
34
|
+
if (models !== undefined && selected === undefined)
|
|
35
|
+
throw providerError({
|
|
36
|
+
kind: "unsupported",
|
|
37
|
+
message: "This model is not available in the installed CLI.",
|
|
38
|
+
});
|
|
39
|
+
if (request.thinking !== undefined &&
|
|
40
|
+
selected !== undefined &&
|
|
41
|
+
!selected.thinkingModes?.includes(request.thinking))
|
|
42
|
+
throw providerError({
|
|
43
|
+
kind: "unsupported",
|
|
44
|
+
message: "This model does not support the selected thinking setting.",
|
|
45
|
+
});
|
|
46
|
+
if (request.webSearch && !port.capabilities.webSearch)
|
|
47
|
+
throw providerError({
|
|
48
|
+
kind: "unsupported",
|
|
49
|
+
message: "This CLI does not support research web search.",
|
|
50
|
+
});
|
|
51
|
+
const thinkingConfig = id === "codex" && request.thinking !== undefined
|
|
52
|
+
? { effort: request.thinking === "off" ? "none" : request.thinking }
|
|
53
|
+
: undefined;
|
|
54
|
+
yield* port.complete({
|
|
55
|
+
...request,
|
|
56
|
+
...(request.thinkingConfig === undefined && thinkingConfig ? { thinkingConfig } : {}),
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
17
61
|
return {
|
|
18
62
|
...port,
|
|
19
63
|
models: async () => catalogue.models(id, "llm").map((m) => ({
|
|
@@ -48,6 +92,20 @@ export function curateRegistry(registry, catalogue) {
|
|
|
48
92
|
},
|
|
49
93
|
image: (id) => {
|
|
50
94
|
const port = registry.image(id);
|
|
95
|
+
if (id === "codex-image") {
|
|
96
|
+
return {
|
|
97
|
+
...port,
|
|
98
|
+
models: () => port.models(),
|
|
99
|
+
generate: async (request) => {
|
|
100
|
+
if (!(await port.models()).some((model) => model.id === request.model))
|
|
101
|
+
throw providerError({
|
|
102
|
+
kind: "unsupported",
|
|
103
|
+
message: "The Codex image capability is unavailable.",
|
|
104
|
+
});
|
|
105
|
+
return port.generate(request);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
51
109
|
return {
|
|
52
110
|
...port,
|
|
53
111
|
models: async () => catalogue.models(id, "image"),
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { isLocalCliProvider } from "../slices/settings/model.js";
|
|
2
|
+
export async function checkRuntimeModel(modelsFor, provider, family, id, thinking) {
|
|
3
|
+
if (id.trim() === "")
|
|
4
|
+
return "missing";
|
|
5
|
+
try {
|
|
6
|
+
const model = (await modelsFor(provider, family)).find((row) => row.id === id);
|
|
7
|
+
if (model === undefined)
|
|
8
|
+
return "missing";
|
|
9
|
+
if (family === "llm" && thinking !== undefined && !model.thinkingModes?.includes(thinking))
|
|
10
|
+
return "thinking";
|
|
11
|
+
return "available";
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return isLocalCliProvider(provider) && family === "llm" ? "manual" : "missing";
|
|
15
|
+
}
|
|
16
|
+
}
|
package/dist/catalog/store.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { parse } from "yaml";
|
|
4
|
+
import { isLocalCliProvider } from "../slices/settings/model.js";
|
|
4
5
|
import { catalogueSchema } from "./schema.js";
|
|
5
6
|
export const catalogueSource = "https://raw.githubusercontent.com/GentBajko/slopify/main/packages/app/src/assets/models.yaml";
|
|
6
7
|
export function parseCatalogue(text) {
|
|
7
8
|
if (Buffer.byteLength(text) > 1024 * 1024)
|
|
8
9
|
throw new Error("Model catalogue exceeds 1 MB");
|
|
9
|
-
|
|
10
|
+
const parsed = catalogueSchema.parse(parse(text, { maxAliasCount: 20, uniqueKeys: true }));
|
|
11
|
+
return {
|
|
12
|
+
...parsed,
|
|
13
|
+
providers: Object.fromEntries(Object.entries(parsed.providers).filter(([id]) => !isLocalCliProvider(id))),
|
|
14
|
+
llm: parsed.llm.filter((row) => !isLocalCliProvider(row.provider)),
|
|
15
|
+
image: parsed.image.filter((row) => !isLocalCliProvider(row.provider)),
|
|
16
|
+
tts: parsed.tts.filter((row) => !isLocalCliProvider(row.provider)),
|
|
17
|
+
};
|
|
10
18
|
}
|
|
11
19
|
export function createCatalogueStore(deps) {
|
|
12
20
|
const path = join(deps.dataDir, "models.yaml");
|
package/dist/catalog/validate.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isLocalCliProvider } from "../slices/settings/model.js";
|
|
1
2
|
export function modelFields(draft, catalogue) {
|
|
2
3
|
if (!catalogue)
|
|
3
4
|
return [];
|
|
@@ -24,7 +25,7 @@ export function modelFields(draft, catalogue) {
|
|
|
24
25
|
},
|
|
25
26
|
];
|
|
26
27
|
for (const { field, family, choice, needed } of checks) {
|
|
27
|
-
if (!needed || !choice)
|
|
28
|
+
if (!needed || !choice || isLocalCliProvider(choice.provider))
|
|
28
29
|
continue;
|
|
29
30
|
const model = catalogue.models(choice.provider, family).find((m) => m.id === choice.model);
|
|
30
31
|
if (!model)
|
|
@@ -3,7 +3,7 @@ import { Hono } from "hono";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { cliPathMaxLength, saveCliPath } from "../../slices/settings/cli-paths.js";
|
|
5
5
|
import { keyStatus, removeProviderKey, saveProviderKey } from "../../slices/settings/keys.js";
|
|
6
|
-
import { providerById, providerIds } from "../../slices/settings/model.js";
|
|
6
|
+
import { isLocalCliProvider, providerById, providerIds } from "../../slices/settings/model.js";
|
|
7
7
|
import { createModelCatalog } from "../../slices/settings/models.js";
|
|
8
8
|
import { providerStatuses } from "../../slices/settings/readiness.js";
|
|
9
9
|
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
@@ -46,7 +46,7 @@ export function providerRoutes(deps) {
|
|
|
46
46
|
.get("/", async (c) => c.json({ providers: await providerStatuses(readiness) }))
|
|
47
47
|
.get("/:id/models", zValidator("param", providerParam, onInvalid), async (c) => {
|
|
48
48
|
const { id } = c.req.valid("param");
|
|
49
|
-
if (deps.catalogue)
|
|
49
|
+
if (deps.catalogue && !isLocalCliProvider(id))
|
|
50
50
|
return c.json({
|
|
51
51
|
models: deps.catalogue.models(id, providerById(id).family).map((m) => ({
|
|
52
52
|
...m,
|
package/dist/main.js
CHANGED
|
@@ -43,6 +43,7 @@ import { executionStages, executionStandings, invocationReady, projectStandings,
|
|
|
43
43
|
import { settleTerminalScheduleRuns } from "./slices/schedules/repo.js";
|
|
44
44
|
import { createScheduleRunner } from "./slices/schedules/scheduler.js";
|
|
45
45
|
import { nodeCliProbe } from "./slices/settings/cli-status.js";
|
|
46
|
+
import { isLocalCliProvider } from "./slices/settings/model.js";
|
|
46
47
|
import { providerStatuses } from "./slices/settings/readiness.js";
|
|
47
48
|
import { reconcileStorage } from "./slices/storage/reconcile.js";
|
|
48
49
|
import { collectorEndpoint, httpPostEvents } from "./slices/telemetry/collector-client.js";
|
|
@@ -407,7 +408,7 @@ export function wireRunner({ audioPreviews, db, paths, clock, ids, log, hub, tel
|
|
|
407
408
|
attempts: sqliteAttempts(db, ids),
|
|
408
409
|
clock,
|
|
409
410
|
log,
|
|
410
|
-
queue: createProviderQueue((provider) => catalogue.read().providers[provider]?.maxConcurrent ?? 1),
|
|
411
|
+
queue: createProviderQueue((provider) => isLocalCliProvider(provider) ? 3 : (catalogue.read().providers[provider]?.maxConcurrent ?? 1)),
|
|
411
412
|
};
|
|
412
413
|
const checkpoints = createCheckpointAuthority({
|
|
413
414
|
decide: (work) => checkpointDecisionForWork(execution, work),
|
package/dist/model-catalog.js
CHANGED
|
@@ -2,9 +2,6 @@ import { falModels } from "./adapters/image/fal.js";
|
|
|
2
2
|
import { googleImageModels } from "./adapters/image/google.js";
|
|
3
3
|
import { openAiImageModels } from "./adapters/image/openai.js";
|
|
4
4
|
import { replicateModels } from "./adapters/image/replicate.js";
|
|
5
|
-
import { claudeCodeModels } from "./adapters/llm/claude-code.js";
|
|
6
|
-
import { codexModels } from "./adapters/llm/codex.js";
|
|
7
|
-
import { geminiModels } from "./adapters/llm/gemini.js";
|
|
8
5
|
import { cartesiaModels } from "./adapters/tts/cartesia.js";
|
|
9
6
|
import { elevenLabsModels } from "./adapters/tts/elevenlabs.js";
|
|
10
7
|
import { inworldModels } from "./adapters/tts/inworld.js";
|
|
@@ -12,9 +9,6 @@ import { openAiTtsModels } from "./adapters/tts/openai.js";
|
|
|
12
9
|
// Composition keeps provider-specific fallback data out of the HTTP and settings layers.
|
|
13
10
|
export function modelSources(registry) {
|
|
14
11
|
const fallbacks = {
|
|
15
|
-
"claude-code": claudeCodeModels,
|
|
16
|
-
codex: codexModels,
|
|
17
|
-
gemini: geminiModels,
|
|
18
12
|
elevenlabs: elevenLabsModels,
|
|
19
13
|
"openai-tts": openAiTtsModels,
|
|
20
14
|
cartesia: cartesiaModels,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { checkRuntimeModel } from "../../catalog/runtime-models.js";
|
|
2
3
|
import { thinkingModes } from "../../kernel/ports/llm.js";
|
|
3
4
|
import { readinessIsUsable, } from "../../kernel/ports/model.js";
|
|
4
5
|
import { chunkModes } from "../narration/chunk.js";
|
|
@@ -64,14 +65,11 @@ export async function validateProviderChanges(changes, current) {
|
|
|
64
65
|
continue;
|
|
65
66
|
if (current.allowsCustomModels?.(picked.provider) === true)
|
|
66
67
|
continue;
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
if (!model) {
|
|
68
|
+
const result = await checkRuntimeModel(current.modelsFor, picked.provider, families[key], picked.model, picked.thinking);
|
|
69
|
+
if (result === "missing") {
|
|
70
70
|
fields.push({ field: `${key}.model`, message: "Choose a model supported by this provider." });
|
|
71
71
|
}
|
|
72
|
-
else if (
|
|
73
|
-
picked.thinking &&
|
|
74
|
-
!model.thinkingModes?.includes(picked.thinking)) {
|
|
72
|
+
else if (result === "thinking") {
|
|
75
73
|
fields.push({ field: `${key}.thinking`, message: "Choose a supported thinking setting." });
|
|
76
74
|
}
|
|
77
75
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { checkRuntimeModel } from "../../catalog/runtime-models.js";
|
|
1
2
|
import { modelFields } from "../../catalog/validate.js";
|
|
2
3
|
import { readinessIsUsable } from "../../kernel/ports/model.js";
|
|
3
4
|
import { cliPathStatus } from "../settings/cli-paths.js";
|
|
4
5
|
import { hasKey, listVoices } from "../settings/repo.js";
|
|
5
|
-
function choices(runs) {
|
|
6
|
+
export function choices(runs) {
|
|
6
7
|
const selected = runs
|
|
7
8
|
.flatMap(({ draft: d }) => [
|
|
8
9
|
{
|
|
@@ -49,12 +50,17 @@ export async function checkDraftReadiness(deps, runs) {
|
|
|
49
50
|
});
|
|
50
51
|
continue;
|
|
51
52
|
}
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
53
|
+
const result = await checkRuntimeModel(deps.modelsFor, c.provider, c.family, c.model, c.thinking);
|
|
54
|
+
if (result === "missing")
|
|
54
55
|
fields.push({
|
|
55
56
|
field: `${c.field}.model`,
|
|
56
57
|
message: "Choose an available model before starting.",
|
|
57
58
|
});
|
|
59
|
+
else if (result === "thinking")
|
|
60
|
+
fields.push({
|
|
61
|
+
field: `${c.field}.thinking`,
|
|
62
|
+
message: "Choose a supported thinking setting.",
|
|
63
|
+
});
|
|
58
64
|
}
|
|
59
65
|
return { fields: [...fields, ...localDraftReadiness(deps, runs, providers)], providers };
|
|
60
66
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { checkRuntimeModel } from "../../catalog/runtime-models.js";
|
|
2
3
|
import { transact } from "../../kernel/db/tx.js";
|
|
3
4
|
import { isMissingFont } from "../fonts/index.js";
|
|
5
|
+
import { choices } from "./readiness.js";
|
|
4
6
|
import { draftRow } from "./repo.js";
|
|
5
7
|
import { resolveReviewInputs, reviewBinding, reviewRefusal } from "./review-inputs.js";
|
|
6
8
|
import { draftViewSchema, playReviewSchema } from "./schema.js";
|
|
@@ -33,6 +35,22 @@ export async function resolvePlayReview(deps, view) {
|
|
|
33
35
|
const after = resolveReviewInputs(deps, parsed.data, font);
|
|
34
36
|
if (!after.ok)
|
|
35
37
|
return after;
|
|
38
|
+
const fields = [];
|
|
39
|
+
for (const choice of choices(after.value.runs)) {
|
|
40
|
+
const result = await checkRuntimeModel(deps.modelsFor, choice.provider, choice.family, choice.model, choice.thinking);
|
|
41
|
+
if (result === "missing")
|
|
42
|
+
fields.push({
|
|
43
|
+
field: `${choice.field}.model`,
|
|
44
|
+
message: "Choose an available model before reviewing.",
|
|
45
|
+
});
|
|
46
|
+
else if (result === "thinking")
|
|
47
|
+
fields.push({
|
|
48
|
+
field: `${choice.field}.thinking`,
|
|
49
|
+
message: "Choose a supported thinking setting.",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (fields.length)
|
|
53
|
+
return reviewRefusal(view, fields);
|
|
36
54
|
return reviewBinding(before.value) === reviewBinding(after.value)
|
|
37
55
|
? after
|
|
38
56
|
: reviewRefusal(view, [], "stale-review");
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { checkRuntimeModel } from "../../catalog/runtime-models.js";
|
|
1
2
|
import { readinessIsUsable } from "../../kernel/ports/model.js";
|
|
2
3
|
import { cliPathStatus } from "../settings/cli-paths.js";
|
|
4
|
+
import { isLocalCliProvider } from "../settings/model.js";
|
|
3
5
|
import { hasKey, listVoices } from "../settings/repo.js";
|
|
4
6
|
import { requiresNewSubmission } from "./preview-retained.js";
|
|
5
7
|
import { recipeProviderChoice } from "./recipe-provider-choice.js";
|
|
@@ -36,12 +38,17 @@ export async function checkReadiness(deps, snapshot, view) {
|
|
|
36
38
|
});
|
|
37
39
|
continue;
|
|
38
40
|
}
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
41
|
+
const result = await checkRuntimeModel(deps.modelsFor, choice.provider, choice.family, choice.model, choice.family === "llm" ? (choice.thinking ?? undefined) : undefined);
|
|
42
|
+
if (result === "missing")
|
|
41
43
|
fields.push({
|
|
42
44
|
field: `${choice.family}.model`,
|
|
43
45
|
message: "Choose an available model before rebuilding.",
|
|
44
46
|
});
|
|
47
|
+
else if (result === "thinking")
|
|
48
|
+
fields.push({
|
|
49
|
+
field: `${choice.family}.thinking`,
|
|
50
|
+
message: "Choose a supported thinking setting.",
|
|
51
|
+
});
|
|
45
52
|
}
|
|
46
53
|
return { fields, providers };
|
|
47
54
|
}
|
|
@@ -52,7 +59,7 @@ export function localReadiness(deps, snapshot, view, providers, catalogue) {
|
|
|
52
59
|
row.id === choice.model &&
|
|
53
60
|
row.enabled &&
|
|
54
61
|
!row.deprecated);
|
|
55
|
-
if (model === undefined)
|
|
62
|
+
if (model === undefined && !isLocalCliProvider(choice.provider))
|
|
56
63
|
fields.push({
|
|
57
64
|
field: `${choice.family}.model`,
|
|
58
65
|
message: "The selected model is no longer available.",
|
|
@@ -85,6 +92,8 @@ export function localReadiness(deps, snapshot, view, providers, catalogue) {
|
|
|
85
92
|
});
|
|
86
93
|
}
|
|
87
94
|
else if (input.kind === "llm") {
|
|
95
|
+
if (isLocalCliProvider(input.provider))
|
|
96
|
+
continue;
|
|
88
97
|
const model = catalogue.llm.find((row) => row.provider === input.provider && row.id === input.model);
|
|
89
98
|
if (input.thinking !== null && model?.llm.thinking?.[input.thinking] === undefined)
|
|
90
99
|
fields.push({
|
|
@@ -18,6 +18,9 @@ export const providerIds = [
|
|
|
18
18
|
"openai-image",
|
|
19
19
|
"google-image",
|
|
20
20
|
];
|
|
21
|
+
export function isLocalCliProvider(id) {
|
|
22
|
+
return id === "claude-code" || id === "codex" || id === "gemini" || id === "codex-image";
|
|
23
|
+
}
|
|
21
24
|
export const providers = [
|
|
22
25
|
{ id: "openrouter", family: "llm", displayName: "OpenRouter", auth: "key" },
|
|
23
26
|
{
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { isLocalCliProvider } from "./model.js";
|
|
1
2
|
// These marketplaces host models with different input schemas. Discovery alone cannot
|
|
2
3
|
// make an arbitrary endpoint compatible with our image request.
|
|
3
4
|
export function allowsCustomModel(provider) {
|
|
4
|
-
return provider !== "fal" && provider !== "replicate";
|
|
5
|
+
return provider !== "fal" && provider !== "replicate" && provider !== "codex-image";
|
|
5
6
|
}
|
|
6
7
|
const ttlMs = 5 * 60_000;
|
|
7
8
|
const retryMs = 30_000;
|
|
@@ -33,14 +34,19 @@ export function createModelCatalog(deps) {
|
|
|
33
34
|
// A catalogue is metadata, not generation. Never expose an upstream response
|
|
34
35
|
// here: it can quote credentials that belong only inside its adapter.
|
|
35
36
|
deps.report(provider);
|
|
36
|
-
const
|
|
37
|
+
const local = isLocalCliProvider(provider);
|
|
38
|
+
const saved = local ? undefined : previous?.value.models;
|
|
37
39
|
value = {
|
|
38
|
-
models: saved ?? deps.fallback(provider),
|
|
40
|
+
models: local ? [] : (saved ?? deps.fallback(provider)),
|
|
39
41
|
...catalogNotice(provider),
|
|
40
42
|
allowsCustom: allowsCustomModel(provider),
|
|
41
|
-
warning:
|
|
42
|
-
?
|
|
43
|
-
|
|
43
|
+
warning: local
|
|
44
|
+
? provider === "codex-image"
|
|
45
|
+
? "Could not check the Codex image capability. Refresh after checking the CLI."
|
|
46
|
+
: "Could not discover models from this CLI. Enter an exact model ID manually or refresh."
|
|
47
|
+
: saved === undefined
|
|
48
|
+
? "Could not load the model list. Showing bundled choices; refresh to try again."
|
|
49
|
+
: "Could not refresh the model list. Showing the last loaded choices.",
|
|
44
50
|
};
|
|
45
51
|
lifetime = retryMs;
|
|
46
52
|
}
|