@gentbajko/slopify 1.0.0 → 1.1.0
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 +189 -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 +20 -11
- 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/dist/web/assets/{index-CO7DnVX9.js → index-DwM-Y6nw.js} +3 -3
- package/dist/web/index.html +1 -1
- 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,189 @@
|
|
|
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
|
+
displayName: modelName,
|
|
29
|
+
supportedEffortLevels: z.array(z.string()).max(10).optional(),
|
|
30
|
+
}))
|
|
31
|
+
.min(1)
|
|
32
|
+
.max(100),
|
|
33
|
+
}),
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
const supportedEfforts = ["low", "medium", "high", "xhigh"];
|
|
37
|
+
function unavailable() {
|
|
38
|
+
return new Error("Claude Code model discovery is unavailable. Refresh the list or enter an exact model ID.");
|
|
39
|
+
}
|
|
40
|
+
function discoveryEnv() {
|
|
41
|
+
const names = [
|
|
42
|
+
"HOME",
|
|
43
|
+
"USER",
|
|
44
|
+
"LOGNAME",
|
|
45
|
+
"USERPROFILE",
|
|
46
|
+
"APPDATA",
|
|
47
|
+
"LOCALAPPDATA",
|
|
48
|
+
"XDG_CONFIG_HOME",
|
|
49
|
+
"XDG_DATA_HOME",
|
|
50
|
+
"XDG_CACHE_HOME",
|
|
51
|
+
"PATH",
|
|
52
|
+
"PATHEXT",
|
|
53
|
+
"LANG",
|
|
54
|
+
"LC_ALL",
|
|
55
|
+
"LC_CTYPE",
|
|
56
|
+
"TMPDIR",
|
|
57
|
+
"TEMP",
|
|
58
|
+
"TMP",
|
|
59
|
+
"SystemRoot",
|
|
60
|
+
"WINDIR",
|
|
61
|
+
];
|
|
62
|
+
const env = {};
|
|
63
|
+
for (const name of names) {
|
|
64
|
+
if (process.env[name] !== undefined)
|
|
65
|
+
env[name] = process.env[name];
|
|
66
|
+
}
|
|
67
|
+
return env;
|
|
68
|
+
}
|
|
69
|
+
function choices(event, requestId) {
|
|
70
|
+
const parsed = responseSchema.safeParse(event);
|
|
71
|
+
if (!parsed.success || parsed.data.response.request_id !== requestId)
|
|
72
|
+
throw unavailable();
|
|
73
|
+
const models = new Map();
|
|
74
|
+
for (const row of parsed.data.response.response.models) {
|
|
75
|
+
if (models.has(row.value))
|
|
76
|
+
continue;
|
|
77
|
+
const modes = row.supportedEffortLevels?.filter((value) => supportedEfforts.includes(value));
|
|
78
|
+
models.set(row.value, {
|
|
79
|
+
id: row.value,
|
|
80
|
+
name: row.displayName,
|
|
81
|
+
...(modes?.length ? { thinkingModes: modes } : {}),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return [...models.values()];
|
|
85
|
+
}
|
|
86
|
+
// The installed CLI has no model-list command; initialize is a no-prompt
|
|
87
|
+
// stream-JSON control exchange. Treat protocol drift as unavailable metadata.
|
|
88
|
+
export async function nodeClaudeCodeModels(binary, timeoutMs = 15_000) {
|
|
89
|
+
const directory = await mkdtemp(join(tmpdir(), "slopify-claude-models-"));
|
|
90
|
+
const requestId = randomUUID();
|
|
91
|
+
const command = cliCommand(binary);
|
|
92
|
+
const child = spawn(command.file, [
|
|
93
|
+
...command.args,
|
|
94
|
+
"-p",
|
|
95
|
+
"--input-format",
|
|
96
|
+
"stream-json",
|
|
97
|
+
"--output-format",
|
|
98
|
+
"stream-json",
|
|
99
|
+
"--verbose",
|
|
100
|
+
"--safe-mode",
|
|
101
|
+
"--tools",
|
|
102
|
+
"",
|
|
103
|
+
"--strict-mcp-config",
|
|
104
|
+
"--no-session-persistence",
|
|
105
|
+
], {
|
|
106
|
+
cwd: directory,
|
|
107
|
+
env: discoveryEnv(),
|
|
108
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
109
|
+
windowsHide: true,
|
|
110
|
+
detached: process.platform !== "win32",
|
|
111
|
+
});
|
|
112
|
+
child.on("error", () => { });
|
|
113
|
+
child.stdin.on("error", () => { });
|
|
114
|
+
child.stderr.resume();
|
|
115
|
+
const closed = new Promise((resolve) => child.once("close", () => resolve()));
|
|
116
|
+
const terminate = (force) => {
|
|
117
|
+
const signal = force ? "SIGKILL" : "SIGTERM";
|
|
118
|
+
if (process.platform !== "win32" && child.pid !== undefined) {
|
|
119
|
+
try {
|
|
120
|
+
process.kill(-child.pid, signal);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// A child that already exited has no process group to signal.
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
child.kill(signal);
|
|
128
|
+
};
|
|
129
|
+
let forceTimer;
|
|
130
|
+
const timer = setTimeout(() => {
|
|
131
|
+
terminate(false);
|
|
132
|
+
forceTimer = setTimeout(() => terminate(true), 500);
|
|
133
|
+
}, timeoutMs);
|
|
134
|
+
try {
|
|
135
|
+
child.stdin.write(`${JSON.stringify({
|
|
136
|
+
request_id: requestId,
|
|
137
|
+
type: "control_request",
|
|
138
|
+
request: { subtype: "initialize" },
|
|
139
|
+
})}\n`);
|
|
140
|
+
let output = "";
|
|
141
|
+
let bytes = 0;
|
|
142
|
+
for await (const piece of child.stdout) {
|
|
143
|
+
bytes += piece.length;
|
|
144
|
+
if (bytes > maxOutputBytes)
|
|
145
|
+
throw unavailable();
|
|
146
|
+
output += piece.toString();
|
|
147
|
+
let newline = output.indexOf("\n");
|
|
148
|
+
while (newline >= 0) {
|
|
149
|
+
const line = output.slice(0, newline).trim();
|
|
150
|
+
output = output.slice(newline + 1);
|
|
151
|
+
if (line !== "") {
|
|
152
|
+
let event;
|
|
153
|
+
try {
|
|
154
|
+
event = JSON.parse(line);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
throw unavailable();
|
|
158
|
+
}
|
|
159
|
+
if (typeof event === "object" &&
|
|
160
|
+
event !== null &&
|
|
161
|
+
"type" in event &&
|
|
162
|
+
event.type === "control_response") {
|
|
163
|
+
return choices(event, requestId);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
newline = output.indexOf("\n");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
throw unavailable();
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
throw unavailable();
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
if (forceTimer !== undefined)
|
|
177
|
+
clearTimeout(forceTimer);
|
|
178
|
+
terminate(false);
|
|
179
|
+
const grace = await Promise.race([
|
|
180
|
+
closed.then(() => true),
|
|
181
|
+
new Promise((resolve) => setTimeout(() => resolve(false), 500)),
|
|
182
|
+
]);
|
|
183
|
+
if (!grace) {
|
|
184
|
+
terminate(true);
|
|
185
|
+
await Promise.race([closed, new Promise((resolve) => setTimeout(resolve, 500))]);
|
|
186
|
+
}
|
|
187
|
+
await rm(directory, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -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,7 +23,7 @@ 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 {
|
|
@@ -45,7 +37,10 @@ export async function nodeGeminiModels(binary) {
|
|
|
45
37
|
}
|
|
46
38
|
}
|
|
47
39
|
function parseGeminiModels(source) {
|
|
48
|
-
const models = new Map(
|
|
40
|
+
const models = new Map();
|
|
41
|
+
let pro = false;
|
|
42
|
+
let flash = false;
|
|
43
|
+
let flashLite = false;
|
|
49
44
|
// Read literal exported model constants only. Never import or execute an
|
|
50
45
|
// installed package, parse comments as entries, or inspect login/settings.
|
|
51
46
|
const declarations = /^export (?:const|let) ([A-Z][A-Z0-9_]*)\s*=\s*(['"])([^'"\r\n]+)\2\s*;/gm;
|
|
@@ -62,8 +57,22 @@ function parseGeminiModels(source) {
|
|
|
62
57
|
if (!/^(?:(?:PREVIEW|DEFAULT|SECONDARY)_GEMINI_|GEMMA_)/.test(name))
|
|
63
58
|
continue;
|
|
64
59
|
models.set(id, { id, name: id });
|
|
60
|
+
if (name.includes("FLASH_LITE") || id.includes("flash-lite"))
|
|
61
|
+
flashLite = true;
|
|
62
|
+
else if (name.includes("FLASH") || id.includes("flash"))
|
|
63
|
+
flash = true;
|
|
64
|
+
else if (name.includes("PRO") || id.includes("-pro"))
|
|
65
|
+
pro = true;
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
+
if (models.size === 0)
|
|
68
|
+
return [];
|
|
69
|
+
return [
|
|
70
|
+
{ id: "auto", name: "Gemini Auto (CLI default)" },
|
|
71
|
+
...(pro ? [{ id: "pro", name: "Gemini Pro (CLI alias)" }] : []),
|
|
72
|
+
...(flash ? [{ id: "flash", name: "Gemini Flash (CLI alias)" }] : []),
|
|
73
|
+
...(flashLite ? [{ id: "flash-lite", name: "Gemini Flash-Lite (CLI alias)" }] : []),
|
|
74
|
+
...models.values(),
|
|
75
|
+
];
|
|
67
76
|
}
|
|
68
77
|
async function executablePath(binary) {
|
|
69
78
|
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)
|