@gentbajko/slopify 1.2.0 → 1.2.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/README.md +9 -0
- package/dist/adapter-registry.js +7 -1
- package/dist/adapters/llm/claude-code-models.js +24 -13
- package/dist/adapters/llm/codex-models.js +12 -5
- package/dist/adapters/llm/codex-server-models.js +150 -0
- package/dist/adapters/llm/codex.js +1 -1
- package/dist/adapters/llm/gemini-models.js +23 -19
- package/dist/edge/cli.js +17 -0
- package/dist/slices/settings/models.js +3 -3
- package/dist/web/assets/{index-DwM-Y6nw.js → index-Cq2O1JRf.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +2 -1
- package/scripts/docker-run.sh +113 -0
package/README.md
CHANGED
|
@@ -30,6 +30,14 @@ slopify
|
|
|
30
30
|
|
|
31
31
|
Both commands launch the same local app.
|
|
32
32
|
|
|
33
|
+
For Docker on Linux, use `slopify --docker` or
|
|
34
|
+
`npx @gentbajko/slopify@latest --docker`. The launcher detects installed Codex,
|
|
35
|
+
Claude Code, and Gemini CLIs and mounts them read-only. It runs in the background,
|
|
36
|
+
restarts with Docker, and keeps your data in `slopify-data`. Run it again after
|
|
37
|
+
upgrading a CLI to refresh its mounts. Add `--port 7070` to change the default port.
|
|
38
|
+
CLI credentials stay separate; sign in once inside the container. See the
|
|
39
|
+
[Docker setup guide](https://github.com/GentBajko/slopify#docker) for login commands.
|
|
40
|
+
|
|
33
41
|
## How to use it
|
|
34
42
|
|
|
35
43
|
1. Add provider keys and voices in Settings. They stay in your local Slopify database.
|
|
@@ -72,6 +80,7 @@ for account limits. Both model IDs are bundled; Inworld's LLM catalogue does not
|
|
|
72
80
|
| `--host` | `SLOPIFY_HOST` | `127.0.0.1` |
|
|
73
81
|
| `--data-dir` | `SLOPIFY_DATA_DIR` | `~/.slopify` |
|
|
74
82
|
| `--no-open` | `SLOPIFY_NO_OPEN` | the browser opens |
|
|
83
|
+
| `--docker` | — | run locally; with the flag, launch Docker with host CLI detection |
|
|
75
84
|
| — | `SLOPIFY_FFMPEG` | the bundled binary |
|
|
76
85
|
|
|
77
86
|
There is no login. Binding to anything but `127.0.0.1` hands the app and every key in
|
package/dist/adapter-registry.js
CHANGED
|
@@ -36,7 +36,13 @@ export function buildRegistry(deps) {
|
|
|
36
36
|
readModels: () => nodeClaudeCodeModels(cliBinary(deps.db, "claude-code")),
|
|
37
37
|
}),
|
|
38
38
|
],
|
|
39
|
-
[
|
|
39
|
+
[
|
|
40
|
+
"codex",
|
|
41
|
+
codexLlm({
|
|
42
|
+
run: cliFor("codex"),
|
|
43
|
+
readModels: () => nodeCodexModels(process.env, cliBinary(deps.db, "codex")),
|
|
44
|
+
}),
|
|
45
|
+
],
|
|
40
46
|
[
|
|
41
47
|
"gemini",
|
|
42
48
|
geminiLlm({
|
|
@@ -27,6 +27,8 @@ const responseSchema = z.object({
|
|
|
27
27
|
value: modelId,
|
|
28
28
|
resolvedModel: modelId.optional(),
|
|
29
29
|
displayName: modelName,
|
|
30
|
+
description: z.string().max(4096).optional(),
|
|
31
|
+
disabled: z.boolean().optional(),
|
|
30
32
|
supportedEffortLevels: z.array(z.string()).max(10).optional(),
|
|
31
33
|
}))
|
|
32
34
|
.min(1)
|
|
@@ -73,27 +75,36 @@ function choices(event, requestId) {
|
|
|
73
75
|
throw unavailable();
|
|
74
76
|
const models = new Map();
|
|
75
77
|
for (const row of parsed.data.response.response.models) {
|
|
78
|
+
if (row.disabled)
|
|
79
|
+
continue;
|
|
76
80
|
const modes = row.supportedEffortLevels?.filter((value) => supportedEfforts.includes(value));
|
|
77
81
|
const thinking = modes?.length ? { thinkingModes: modes } : {};
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
const target = row.resolvedModel ?? row.value;
|
|
83
|
+
const context = /\[([^\]]+)\]/.exec(row.value)?.[1] ?? /\[([^\]]+)\]/.exec(target)?.[1];
|
|
84
|
+
const key = target.replace(/\[[^\]]+\]/g, "") + (context ? `[${context}]` : "");
|
|
85
|
+
// Default and Opus can resolve to the same model. Keep the named CLI choice,
|
|
86
|
+
// preserving its accepted value (including context modifiers) for generation.
|
|
87
|
+
if (!models.has(key) || models.get(key)?.id === "default")
|
|
88
|
+
models.set(key, {
|
|
80
89
|
id: row.value,
|
|
81
|
-
name: row.
|
|
82
|
-
?
|
|
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)`,
|
|
90
|
+
name: claudeModelName(target, row.description, row.displayName) +
|
|
91
|
+
(context ? ` (${context.toUpperCase()} context)` : ""),
|
|
92
92
|
...thinking,
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
|
+
if (models.size === 0)
|
|
96
|
+
throw unavailable();
|
|
95
97
|
return [...models.values()];
|
|
96
98
|
}
|
|
99
|
+
function claudeModelName(target, description, fallback) {
|
|
100
|
+
const version = /^claude-(opus|sonnet|haiku|fable)-(\d+)(?:-(\d{1,2})(?=-|\[|$))?/i.exec(target);
|
|
101
|
+
if (version) {
|
|
102
|
+
const family = version[1] ?? "";
|
|
103
|
+
return `${family[0]?.toUpperCase()}${family.slice(1)} ${version[2]}${version[3] ? `.${version[3]}` : ""}`;
|
|
104
|
+
}
|
|
105
|
+
const advertised = /\b(?:Opus|Sonnet|Haiku|Fable) \d+(?:\.\d+)?\b/i.exec(description ?? "");
|
|
106
|
+
return advertised?.[0] ?? fallback.replace(/\s*\([^)]*context\)/i, "");
|
|
107
|
+
}
|
|
97
108
|
// The installed CLI has no model-list command; initialize is a no-prompt
|
|
98
109
|
// stream-JSON control exchange. Treat protocol drift as unavailable metadata.
|
|
99
110
|
export async function nodeClaudeCodeModels(binary, timeoutMs = 15_000) {
|
|
@@ -2,6 +2,7 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { readCatalogueFile } from "./catalogue-files.js";
|
|
5
|
+
import { codexModelName, nodeCodexServerModels } from "./codex-server-models.js";
|
|
5
6
|
const safeText = z
|
|
6
7
|
.string()
|
|
7
8
|
.trim()
|
|
@@ -22,10 +23,11 @@ const model = z.object({
|
|
|
22
23
|
.max(20)
|
|
23
24
|
.optional(),
|
|
24
25
|
});
|
|
25
|
-
export async function nodeCodexModels(env = process.env) {
|
|
26
|
+
export async function nodeCodexModels(env = process.env, binary = "codex", timeoutMs = 15_000) {
|
|
26
27
|
try {
|
|
27
28
|
const directory = env.CODEX_HOME?.trim() || join(homedir(), ".codex");
|
|
28
|
-
const
|
|
29
|
+
const path = env.SLOPIFY_CODEX_MODELS_FILE?.trim() || join(directory, "models_cache.json");
|
|
30
|
+
const source = await readCatalogueFile(path, 8 * 1024 * 1024);
|
|
29
31
|
const parsed = cache.parse(JSON.parse(source));
|
|
30
32
|
const models = parsed.models
|
|
31
33
|
.flatMap((entry) => {
|
|
@@ -41,7 +43,7 @@ export async function nodeCodexModels(env = process.env) {
|
|
|
41
43
|
.filter((level) => ["off", "low", "medium", "high", "xhigh"].includes(level));
|
|
42
44
|
unique.set(item.slug, {
|
|
43
45
|
id: item.slug,
|
|
44
|
-
name: item.display_name ?? item.slug,
|
|
46
|
+
name: codexModelName(item.display_name ?? item.slug),
|
|
45
47
|
...(thinkingModes?.length ? { thinkingModes } : {}),
|
|
46
48
|
});
|
|
47
49
|
}
|
|
@@ -51,7 +53,12 @@ export async function nodeCodexModels(env = process.env) {
|
|
|
51
53
|
return [...unique.values()];
|
|
52
54
|
}
|
|
53
55
|
catch {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
try {
|
|
57
|
+
return await nodeCodexServerModels(binary, env, timeoutMs);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Do not expose cache contents, home paths or raw JSON errors to the browser.
|
|
61
|
+
throw new Error("Codex model metadata is unavailable. Check the CLI and refresh, or enter a custom model ID.");
|
|
62
|
+
}
|
|
56
63
|
}
|
|
57
64
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { cliCommand } from "../../kernel/cli-command.js";
|
|
7
|
+
const text = z
|
|
8
|
+
.string()
|
|
9
|
+
.trim()
|
|
10
|
+
.min(1)
|
|
11
|
+
.max(256)
|
|
12
|
+
.refine((value) => !/[\p{Cc}]/u.test(value));
|
|
13
|
+
const pageSchema = z.object({
|
|
14
|
+
data: z
|
|
15
|
+
.array(z.object({
|
|
16
|
+
model: text.regex(/^\S+$/),
|
|
17
|
+
displayName: text,
|
|
18
|
+
hidden: z.boolean().optional(),
|
|
19
|
+
supportedReasoningEfforts: z
|
|
20
|
+
.array(z.object({ reasoningEffort: z.string() }))
|
|
21
|
+
.max(20)
|
|
22
|
+
.optional(),
|
|
23
|
+
}))
|
|
24
|
+
.max(1000),
|
|
25
|
+
nextCursor: z.string().max(4096).nullish(),
|
|
26
|
+
});
|
|
27
|
+
// model/list is a metadata-only app-server exchange: no thread, turn or tool is started.
|
|
28
|
+
export async function nodeCodexServerModels(binary, env, timeoutMs = 15_000) {
|
|
29
|
+
const command = cliCommand(binary);
|
|
30
|
+
const directory = await mkdtemp(join(tmpdir(), "slopify-codex-models-"));
|
|
31
|
+
const child = spawn(command.file, [...command.args, "app-server", "--listen", "stdio://"], {
|
|
32
|
+
cwd: directory,
|
|
33
|
+
env: { ...env, PWD: directory },
|
|
34
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
35
|
+
windowsHide: true,
|
|
36
|
+
detached: process.platform !== "win32",
|
|
37
|
+
});
|
|
38
|
+
child.on("error", () => { });
|
|
39
|
+
child.stdin.on("error", () => { });
|
|
40
|
+
child.stderr.resume();
|
|
41
|
+
const closed = new Promise((resolve) => child.once("close", () => resolve()));
|
|
42
|
+
function terminate(signal) {
|
|
43
|
+
if (process.platform !== "win32" && child.pid !== undefined) {
|
|
44
|
+
try {
|
|
45
|
+
process.kill(-child.pid, signal);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
/* The process may already have exited. */
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
child.kill(signal);
|
|
53
|
+
}
|
|
54
|
+
const timer = setTimeout(() => terminate("SIGKILL"), timeoutMs);
|
|
55
|
+
const send = (message) => child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
56
|
+
let expectedId = 1;
|
|
57
|
+
let pages = 0;
|
|
58
|
+
let bytes = 0;
|
|
59
|
+
let buffer = "";
|
|
60
|
+
const models = new Map();
|
|
61
|
+
const cursors = new Set();
|
|
62
|
+
try {
|
|
63
|
+
send({
|
|
64
|
+
id: expectedId,
|
|
65
|
+
method: "initialize",
|
|
66
|
+
params: {
|
|
67
|
+
clientInfo: { name: "slopify_model_discovery", version: "1.0" },
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
child.stdout.setEncoding("utf8");
|
|
71
|
+
for await (const piece of child.stdout) {
|
|
72
|
+
bytes += Buffer.byteLength(piece);
|
|
73
|
+
if (bytes > 4 * 1024 * 1024)
|
|
74
|
+
throw new Error("Oversized metadata");
|
|
75
|
+
buffer += piece;
|
|
76
|
+
let newline = buffer.indexOf("\n");
|
|
77
|
+
while (newline >= 0) {
|
|
78
|
+
const line = buffer.slice(0, newline).trim();
|
|
79
|
+
buffer = buffer.slice(newline + 1);
|
|
80
|
+
newline = buffer.indexOf("\n");
|
|
81
|
+
if (line === "")
|
|
82
|
+
continue;
|
|
83
|
+
const event = z
|
|
84
|
+
.object({
|
|
85
|
+
id: z.unknown().optional(),
|
|
86
|
+
result: z.unknown().optional(),
|
|
87
|
+
error: z.unknown().optional(),
|
|
88
|
+
})
|
|
89
|
+
.parse(JSON.parse(line));
|
|
90
|
+
if (event.id !== expectedId)
|
|
91
|
+
continue;
|
|
92
|
+
if (event.error !== undefined)
|
|
93
|
+
throw new Error("Discovery refused");
|
|
94
|
+
let cursor;
|
|
95
|
+
if (expectedId === 1) {
|
|
96
|
+
send({ method: "initialized" });
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const page = pageSchema.parse(event.result);
|
|
100
|
+
for (const row of page.data) {
|
|
101
|
+
if (row.hidden === true || models.has(row.model))
|
|
102
|
+
continue;
|
|
103
|
+
const thinkingModes = row.supportedReasoningEfforts
|
|
104
|
+
?.map((item) => item.reasoningEffort)
|
|
105
|
+
.filter((effort) => ["off", "low", "medium", "high", "xhigh"].includes(effort));
|
|
106
|
+
models.set(row.model, {
|
|
107
|
+
id: row.model,
|
|
108
|
+
name: codexModelName(row.displayName),
|
|
109
|
+
...(thinkingModes?.length ? { thinkingModes } : {}),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
cursor = page.nextCursor;
|
|
113
|
+
if (!cursor) {
|
|
114
|
+
if (models.size === 0)
|
|
115
|
+
throw new Error("No visible models");
|
|
116
|
+
return [...models.values()];
|
|
117
|
+
}
|
|
118
|
+
if (cursors.has(cursor) || ++pages >= 20)
|
|
119
|
+
throw new Error("Invalid pagination");
|
|
120
|
+
cursors.add(cursor);
|
|
121
|
+
}
|
|
122
|
+
send({
|
|
123
|
+
id: ++expectedId,
|
|
124
|
+
method: "model/list",
|
|
125
|
+
params: { limit: 100, includeHidden: false, ...(cursor ? { cursor } : {}) },
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
throw new Error("Discovery ended before its reply");
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
terminate("SIGTERM");
|
|
134
|
+
let grace;
|
|
135
|
+
await Promise.race([
|
|
136
|
+
closed,
|
|
137
|
+
new Promise((resolve) => {
|
|
138
|
+
grace = setTimeout(() => {
|
|
139
|
+
terminate("SIGKILL");
|
|
140
|
+
resolve();
|
|
141
|
+
}, 500);
|
|
142
|
+
}),
|
|
143
|
+
]);
|
|
144
|
+
clearTimeout(grace);
|
|
145
|
+
await rm(directory, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export function codexModelName(name) {
|
|
149
|
+
return name.replace(/^(GPT-\d+(?:\.\d+)?)-/i, "$1 ");
|
|
150
|
+
}
|
|
@@ -179,7 +179,7 @@ export function codexLlm(deps) {
|
|
|
179
179
|
id: "codex",
|
|
180
180
|
// Prose arrives as whole messages; other JSONL events carry activity separately.
|
|
181
181
|
capabilities: { streams: true, reportsUsage: true, webSearch: true },
|
|
182
|
-
models: deps.readModels ?? (() => nodeCodexModels()),
|
|
182
|
+
models: deps.readModels ?? (() => nodeCodexModels(process.env, binary)),
|
|
183
183
|
complete,
|
|
184
184
|
};
|
|
185
185
|
}
|
|
@@ -67,9 +67,6 @@ async function bundledModels(entry) {
|
|
|
67
67
|
}
|
|
68
68
|
function parseGeminiModels(source) {
|
|
69
69
|
const models = new Map();
|
|
70
|
-
let pro = false;
|
|
71
|
-
let flash = false;
|
|
72
|
-
let flashLite = false;
|
|
73
70
|
// Read literal model constants only. Never import or execute an
|
|
74
71
|
// installed package, parse comments as entries, or inspect login/settings.
|
|
75
72
|
const declarations = /^(?:export\s+)?(?:const|let|var)\s+([A-Z][A-Z0-9_]*)\s*=\s*(['"])([^'"\r\n]+)\2\s*;/gm;
|
|
@@ -81,27 +78,34 @@ function parseGeminiModels(source) {
|
|
|
81
78
|
id === undefined ||
|
|
82
79
|
!name.includes("MODEL") ||
|
|
83
80
|
name.includes("EMBEDDING") ||
|
|
84
|
-
!/^(?:
|
|
81
|
+
!/^gemini-\d+(?:\.\d+)?-(?:pro|flash|flash-lite)(?:-preview(?:-\d{2}-\d{2})?)?$/.test(id))
|
|
85
82
|
continue;
|
|
86
|
-
|
|
83
|
+
// SECONDARY and CUSTOM_TOOLS constants are routing aliases, not separate
|
|
84
|
+
// picker choices. Gemma and embedding models use different availability rules.
|
|
85
|
+
// In 0.61+, DEFAULT_FLASH points at a BASE constant while new releases
|
|
86
|
+
// use LATEST constants. Read both literal sources without evaluating JS.
|
|
87
|
+
if (!/^(?:PREVIEW|DEFAULT|BASE|LATEST)_GEMINI_/.test(name) || name.includes("CUSTOM_TOOLS"))
|
|
87
88
|
continue;
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
const parts = /^gemini-(\d+(?:\.\d+)?)-(pro|flash-lite|flash)/.exec(id);
|
|
90
|
+
if (!parts)
|
|
91
|
+
continue;
|
|
92
|
+
const tier = parts[2] === "pro" ? "Pro" : parts[2] === "flash-lite" ? "Flash-Lite" : "Flash";
|
|
93
|
+
models.set(id, {
|
|
94
|
+
id,
|
|
95
|
+
name: `Gemini ${parts[1]} ${tier}${id.includes("-preview") ? " (Preview)" : ""}`,
|
|
96
|
+
});
|
|
95
97
|
}
|
|
96
98
|
if (models.size === 0)
|
|
97
99
|
return [];
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
const sorted = [...models.values()].sort((a, b) => b.id.localeCompare(a.id, "en", { numeric: true }));
|
|
101
|
+
const tiers = new Set();
|
|
102
|
+
const choices = sorted.map((model) => {
|
|
103
|
+
const tier = /-(pro|flash-lite|flash)(?:-|$)/.exec(model.id)?.[1] ?? model.id;
|
|
104
|
+
const group = tiers.has(tier) ? "Other versions" : "Latest in installed CLI";
|
|
105
|
+
tiers.add(tier);
|
|
106
|
+
return { ...model, group };
|
|
107
|
+
});
|
|
108
|
+
return [{ id: "auto", name: "Automatic (CLI default)" }, ...choices];
|
|
105
109
|
}
|
|
106
110
|
async function executablePath(binary) {
|
|
107
111
|
if (isAbsolute(binary) || /[\\/]/.test(binary))
|
package/dist/edge/cli.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
2
4
|
import { parseArgs } from "node:util";
|
|
3
5
|
import { configFrom } from "../kernel/config/index.js";
|
|
4
6
|
import { readVersion } from "../kernel/version.js";
|
|
@@ -12,9 +14,24 @@ const { values } = parseArgs({
|
|
|
12
14
|
host: { type: "string" },
|
|
13
15
|
"data-dir": { type: "string" },
|
|
14
16
|
"no-open": { type: "boolean" },
|
|
17
|
+
docker: { type: "boolean" },
|
|
15
18
|
},
|
|
16
19
|
});
|
|
17
20
|
try {
|
|
21
|
+
if (values.docker) {
|
|
22
|
+
if (values.host !== undefined || values["data-dir"] !== undefined)
|
|
23
|
+
throw new Error("Docker stores data in the slopify-data volume and binds to localhost. Use --port to change its port.");
|
|
24
|
+
const result = spawnSync("bash", [fileURLToPath(new URL("../../scripts/docker-run.sh", import.meta.url))], {
|
|
25
|
+
stdio: "inherit",
|
|
26
|
+
env: {
|
|
27
|
+
...process.env,
|
|
28
|
+
...(values.port === undefined ? {} : { SLOPIFY_DOCKER_HOST_PORT: values.port }),
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
if (result.error)
|
|
32
|
+
throw result.error;
|
|
33
|
+
process.exit(result.status ?? 1);
|
|
34
|
+
}
|
|
18
35
|
const config = configFrom(values, process.env);
|
|
19
36
|
const forwarded = await forwardManagedUpdate(config.dataDir, readVersion(), process.argv.slice(2));
|
|
20
37
|
if (forwarded !== undefined)
|
|
@@ -82,9 +82,9 @@ function uniqueModels(models) {
|
|
|
82
82
|
}
|
|
83
83
|
function catalogNotice(provider) {
|
|
84
84
|
const notices = {
|
|
85
|
-
codex: "Models come from Codex
|
|
86
|
-
gemini: "Models
|
|
87
|
-
"claude-code": "
|
|
85
|
+
codex: "Models come from your Codex installation. Refresh to check again.",
|
|
86
|
+
gemini: "Models come from your installed Gemini CLI, not an account access check. Missing a new model? Update Gemini CLI, rerun the Docker launcher if applicable, then refresh this list.",
|
|
87
|
+
"claude-code": "Models come from Claude Code. Missing a new model? Update Claude Code, rerun the Docker launcher if applicable, then refresh this list. You can also enter an exact model ID.",
|
|
88
88
|
inworld: "These are Inworld’s documented TTS models. You can also enter a compatible model ID.",
|
|
89
89
|
cartesia: "Cartesia has no model-list API. These are bundled compatible choices; you can enter a newer model ID.",
|
|
90
90
|
fal: "These models have input formats supported by Slopify's fal adapter.",
|