okengine 0.9.0 → 0.10.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/AGENTS.md +1 -1
- package/manifest.v1.schema.json +9 -1
- package/package.json +3 -3
- package/site/content/docs/console/runs.mdx +10 -0
- package/site/content/docs/console/traces.mdx +4 -0
- package/site/content/docs/elements/ai.mdx +54 -32
- package/site/content/docs/elements/channel.mdx +105 -14
- package/site/content/docs/elements/clock.mdx +1 -0
- package/site/content/docs/elements/flow.mdx +42 -8
- package/site/content/docs/get-started/basic-usage.mdx +5 -1
- package/site/content/docs/get-started/installation.mdx +9 -5
- package/site/content/docs/meta.json +2 -0
- package/site/content/docs/plugins/otp.mdx +58 -46
- package/site/content/docs/providers/azure-redis.mdx +85 -0
- package/site/content/docs/providers/cockroachdb.mdx +86 -0
- package/site/content/docs/providers/digitalocean-caching.mdx +88 -0
- package/site/content/docs/providers/dragonfly-cloud.mdx +82 -0
- package/site/content/docs/providers/elasticache.mdx +92 -0
- package/site/content/docs/providers/index.mdx +101 -0
- package/site/content/docs/providers/memorystore.mdx +89 -0
- package/site/content/docs/providers/meta.json +20 -0
- package/site/content/docs/providers/neon.mdx +115 -0
- package/site/content/docs/providers/redis-cloud.mdx +93 -0
- package/site/content/docs/providers/supabase.mdx +117 -0
- package/site/content/docs/providers/upstash.mdx +91 -0
- package/site/content/docs/providers/yugabytedb.mdx +95 -0
- package/site/content/docs/recipes/caddy.mdx +128 -0
- package/site/content/docs/recipes/cockroachdb.mdx +127 -0
- package/site/content/docs/recipes/dragonfly.mdx +125 -0
- package/site/content/docs/recipes/index.mdx +146 -0
- package/site/content/docs/recipes/llama-cpp.mdx +153 -0
- package/site/content/docs/recipes/mailpit.mdx +136 -0
- package/site/content/docs/recipes/meilisearch.mdx +139 -0
- package/site/content/docs/recipes/meta.json +31 -0
- package/site/content/docs/recipes/ollama.mdx +142 -0
- package/site/content/docs/recipes/openbao.mdx +141 -0
- package/site/content/docs/recipes/pgdog.mdx +137 -0
- package/site/content/docs/recipes/postgres.mdx +136 -0
- package/site/content/docs/recipes/redis.mdx +130 -0
- package/site/content/docs/recipes/rustfs.mdx +148 -0
- package/site/content/docs/recipes/sglang.mdx +105 -0
- package/site/content/docs/recipes/supabase-docker.mdx +145 -0
- package/site/content/docs/recipes/timescale.mdx +129 -0
- package/site/content/docs/recipes/traefik.mdx +136 -0
- package/site/content/docs/recipes/valkey.mdx +121 -0
- package/site/content/docs/recipes/vllm.mdx +106 -0
- package/site/content/docs/recipes/yugabytedb.mdx +135 -0
- package/site/content/docs/reference/cli.md +42 -6
- package/site/content/docs/reference/configuration.mdx +5 -3
- package/site/content/docs/reference/environment-variables.mdx +12 -12
- package/site/content/docs/reference/fx.mdx +38 -4
- package/src/auth/otp-capability.ts +14 -14
- package/src/cli/ai-setup/ai-setup.test.ts +32 -12
- package/src/cli/ai-setup/apply.ts +65 -23
- package/src/cli/ai-setup/catalog.ts +1316 -35
- package/src/cli/ai-setup/detect-ollama.ts +47 -0
- package/src/cli/ai-setup/index.ts +44 -7
- package/src/cli/ai-setup/prompts.ts +430 -486
- package/src/cli/ai-setup/recommend.test.ts +150 -50
- package/src/cli/ai-setup/recommend.ts +118 -101
- package/src/cli/ask-seed.test.ts +68 -0
- package/src/cli/ask-seed.ts +79 -0
- package/src/cli/db-seed-redact.test.ts +25 -0
- package/src/cli/db-seed.ts +36 -7
- package/src/cli/db.test.ts +1 -1
- package/src/cli/db.ts +2 -2
- package/src/cli/dev-controls.test.ts +107 -0
- package/src/cli/dev-controls.ts +354 -0
- package/src/cli/dev-schema-sync.ts +12 -4
- package/src/cli/dev.test.ts +50 -30
- package/src/cli/dev.ts +607 -66
- package/src/cli/drizzle-env.test.ts +40 -6
- package/src/cli/drizzle-env.ts +22 -1
- package/src/cli/ensure-drizzle-config.ts +6 -4
- package/src/cli/hero-meta.test.ts +13 -1
- package/src/cli/hero-meta.ts +28 -6
- package/src/cli/index.ts +5 -0
- package/src/cli/registry.ts +24 -0
- package/src/cli/replay.test.ts +105 -0
- package/src/cli/replay.ts +206 -0
- package/src/compiler/extract.ts +3 -3
- package/src/config/index.ts +3 -3
- package/src/console/server/claim.ts +3 -1
- package/src/console/ui/dist/assets/{index-ClX0ZCe5.js → index-D-9KFce5.js} +2 -2
- package/src/console/ui/dist/assets/panel-overview-CW0CQEc2.js +1 -0
- package/src/console/ui/dist/assets/panel-runs-BxuyDI3x.js +1 -0
- package/src/console/ui/dist/assets/{panel-signals-Dsxdu_AR.js → panel-signals-BXofbj7v.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-hy7O8HOs.js → panel-store-B8mH-arU.js} +1 -1
- package/src/console/ui/dist/assets/{panel-traces-CBiAe5go.js → panel-traces-DSA-ZWnf.js} +1 -1
- package/src/console/ui/dist/index.html +1 -1
- package/src/console/ui/overview/slo.ts +31 -3
- package/src/console/ui/overview/types.ts +6 -0
- package/src/console/ui/runs/errors.test.ts +53 -0
- package/src/console/ui/runs/errors.ts +88 -0
- package/src/console/ui/runs/index.ts +8 -0
- package/src/console/ui/runs/search.ts +16 -0
- package/src/console/ui/shell/panels/runs/RunsPanel.tsx +72 -1
- package/src/docker/ai-model-status.test.ts +101 -0
- package/src/docker/ai-model-status.ts +276 -0
- package/src/docker/compose-health.test.ts +115 -0
- package/src/docker/compose-health.ts +234 -0
- package/src/docker/compose.ts +24 -8
- package/src/docker/derive.ts +35 -9
- package/src/docker/docker.test.ts +304 -12
- package/src/docker/helpers.ts +56 -0
- package/src/docker/index.ts +52 -0
- package/src/docker/ollama-pull.ts +232 -0
- package/src/docker/recipes/cockroach.ts +24 -0
- package/src/docker/recipes/index.ts +32 -0
- package/src/docker/recipes/llama-cpp.ts +273 -0
- package/src/docker/recipes/ollama.ts +19 -23
- package/src/docker/recipes/pgdog.ts +4 -1
- package/src/docker/recipes/postgres.ts +5 -18
- package/src/docker/recipes/sglang.ts +55 -0
- package/src/docker/recipes/supabase.ts +21 -0
- package/src/docker/recipes/timescale.ts +18 -0
- package/src/docker/recipes/vllm.ts +44 -0
- package/src/docker/recipes/yugabyte.ts +23 -0
- package/src/docker/stack-id.test.ts +22 -0
- package/src/docker/stack-id.ts +38 -4
- package/src/docker/types.ts +15 -0
- package/src/drivers/ai-ollama.integration.test.ts +7 -4
- package/src/drivers/signal-engine.ts +10 -0
- package/src/drivers/signal-postgres.ts +24 -2
- package/src/drivers/signal-types.ts +10 -0
- package/src/elements/channel/fallback.test.ts +91 -0
- package/src/elements/channel/hard-bounce.test.ts +82 -0
- package/src/elements/channel/injection.test.ts +153 -0
- package/src/elements/channel/locale-ar.test.ts +102 -0
- package/src/elements/channel/otp-delivery.ts +5 -2
- package/src/elements/channel/receipts-ledger.test.ts +100 -0
- package/src/elements/channel/runtime.ts +24 -1
- package/src/elements/channel/test-helpers.ts +60 -0
- package/src/elements/channel/whatsapp-compliance.test.ts +124 -0
- package/src/elements/store/cache.ts +1 -1
- package/src/elements/store/emit-drizzle.ts +43 -9
- package/src/elements/store/runtime.ts +1 -1
- package/src/elements/store/schema-decl.test.ts +2 -0
- package/src/elements/store/schema-decl.ts +15 -0
- package/src/elements/store/seed.ts +1 -1
- package/src/index.ts +1 -0
- package/src/kernel/app.ts +118 -11
- package/src/kernel/boot-bind/signal.ts +6 -2
- package/src/kernel/boot-bind/store.ts +2 -1
- package/src/kernel/boot.test.ts +6 -5
- package/src/kernel/boot.ts +8 -3
- package/src/kernel/compensate.test.ts +131 -0
- package/src/kernel/correlation.test.ts +108 -0
- package/src/kernel/effect-timing.test.ts +82 -0
- package/src/kernel/flow.ts +38 -0
- package/src/kernel/fx-runs.test.ts +49 -0
- package/src/kernel/fx.test.ts +14 -0
- package/src/kernel/fx.ts +111 -1
- package/src/manifest/types.ts +8 -2
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
- package/src/plugins/auth-methods.security.test.ts +3 -3
- package/src/plugins/auth-methods.test.ts +1 -1
- package/src/plugins/index.ts +2 -2
- package/src/plugins/otp.test.ts +21 -19
- package/src/plugins/otp.ts +36 -34
- package/src/plugins/taqnyat.live.test.ts +3 -3
- package/src/runs/collect.ts +3 -0
- package/src/runs/export-otlp.ts +64 -0
- package/src/runs/index.ts +16 -0
- package/src/runs/parquet.ts +9 -0
- package/src/runs/types.ts +5 -0
- package/src/runs/window.test.ts +68 -0
- package/src/runs/window.ts +148 -0
- package/src/runtime/boot-warn.test.ts +45 -0
- package/src/runtime/boot-warn.ts +17 -0
- package/src/runtime/dev-request-log.ts +5 -5
- package/src/term.test.ts +95 -10
- package/src/term.ts +369 -14
- package/src/console/ui/dist/assets/panel-overview-DONDzBd2.js +0 -1
- package/src/console/ui/dist/assets/panel-runs-D5zf-D9c.js +0 -1
|
@@ -1,43 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Interactive Clack prompts for `oke ai setup
|
|
2
|
+
* Interactive Clack prompts for `oke ai setup` / create-oke AI wizard.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { isCancel, note, select, text } from "@clack/prompts";
|
|
5
|
+
import { isCancel, note, password, select, text } from "@clack/prompts";
|
|
6
6
|
import {
|
|
7
|
-
CHAT_MODELS,
|
|
8
7
|
CLOUD_PROVIDERS,
|
|
9
|
-
|
|
10
|
-
VISION_MODELS,
|
|
8
|
+
MODEL_TIERS,
|
|
11
9
|
cloudChatModels,
|
|
10
|
+
llamaCppModelsForTier,
|
|
11
|
+
modelsForTier,
|
|
12
12
|
recommendCloudChat,
|
|
13
13
|
recommendForRole,
|
|
14
|
+
recommendForTier,
|
|
15
|
+
recommendLlamaCppForTier,
|
|
14
16
|
type CatalogModel,
|
|
15
|
-
type
|
|
17
|
+
type ModelTier,
|
|
16
18
|
} from "./catalog.ts";
|
|
17
19
|
import type { AiSetupApplyInput } from "./apply.ts";
|
|
20
|
+
import { LLAMA_CPP_IMAGE } from "../../docker/recipes/llama-cpp.ts";
|
|
18
21
|
import {
|
|
22
|
+
detectMachineInfo,
|
|
19
23
|
detectOllama,
|
|
20
24
|
detectTotalRamGb,
|
|
21
25
|
isInstalled,
|
|
22
26
|
type OllamaDetectResult,
|
|
23
27
|
} from "./detect-ollama.ts";
|
|
24
28
|
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
modelFitsOnMachine,
|
|
31
|
-
recommendChatForNeeds,
|
|
32
|
-
recommendVisionForNeeds,
|
|
33
|
-
type AiNeeds,
|
|
34
|
-
type AiPriority,
|
|
35
|
-
type AiUseCase,
|
|
29
|
+
formatLlamaCppBanner,
|
|
30
|
+
formatModelRow,
|
|
31
|
+
formatModelTableHeader,
|
|
32
|
+
formatOllamaBanner,
|
|
33
|
+
suggestTierForRam,
|
|
36
34
|
} from "./recommend.ts";
|
|
37
35
|
|
|
38
36
|
/** Provider menu value. */
|
|
39
37
|
export type AiSetupProvider =
|
|
38
|
+
| "llama-cpp"
|
|
40
39
|
| "ollama"
|
|
40
|
+
| "vllm"
|
|
41
|
+
| "sglang"
|
|
41
42
|
| "openai"
|
|
42
43
|
| "anthropic"
|
|
43
44
|
| "gemini"
|
|
@@ -45,7 +46,7 @@ export type AiSetupProvider =
|
|
|
45
46
|
| "openrouter"
|
|
46
47
|
| "custom";
|
|
47
48
|
|
|
48
|
-
/** Sentinel for
|
|
49
|
+
/** Sentinel for Back in AI setup selects. */
|
|
49
50
|
const BACK = "__back__" as const;
|
|
50
51
|
type Back = typeof BACK;
|
|
51
52
|
|
|
@@ -62,7 +63,6 @@ export async function askAiSetup(
|
|
|
62
63
|
readonly ramGb?: number | null;
|
|
63
64
|
} = {},
|
|
64
65
|
): Promise<AiSetupApplyInput | null> {
|
|
65
|
-
// Treat mock as "ask again" — create-oke may pass mock when only docker is Ollama.
|
|
66
66
|
const rawProvider = options.provider?.trim() ?? "";
|
|
67
67
|
let provider: AiSetupProvider | undefined =
|
|
68
68
|
rawProvider === "" || rawProvider === "mock" ? undefined : (rawProvider as AiSetupProvider);
|
|
@@ -71,369 +71,347 @@ export async function askAiSetup(
|
|
|
71
71
|
const value = await select({
|
|
72
72
|
message: "AI Provider",
|
|
73
73
|
options: [
|
|
74
|
+
{
|
|
75
|
+
value: "llama-cpp",
|
|
76
|
+
label: "llama.cpp (Local)",
|
|
77
|
+
hint: "default · Docker Hub ai/ · recommend for your RAM",
|
|
78
|
+
},
|
|
74
79
|
{
|
|
75
80
|
value: "ollama",
|
|
76
|
-
label: "
|
|
81
|
+
label: "Ollama (Local)",
|
|
77
82
|
hint: "detect models · recommend for your RAM",
|
|
78
83
|
},
|
|
79
|
-
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
{
|
|
84
|
+
{
|
|
85
|
+
value: "vllm",
|
|
86
|
+
label: "vLLM (self-hosted GPU)",
|
|
87
|
+
hint: "multi-user / production concurrency",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
value: "sglang",
|
|
91
|
+
label: "SGLang (self-hosted GPU)",
|
|
92
|
+
hint: "structured / agent workloads",
|
|
93
|
+
},
|
|
94
|
+
{ value: "openai", label: "OpenAI" },
|
|
95
|
+
{ value: "anthropic", label: "Anthropic" },
|
|
96
|
+
{ value: "gemini", label: "Gemini", hint: "OpenAI-compatible proxy URL" },
|
|
97
|
+
{ value: "lmstudio", label: "LM Studio" },
|
|
98
|
+
{ value: "openrouter", label: "OpenRouter" },
|
|
99
|
+
{ value: "custom", label: "Custom OpenAI Compatible" },
|
|
85
100
|
],
|
|
86
101
|
});
|
|
87
102
|
if (isCancel(value)) return null;
|
|
88
103
|
provider = String(value) as AiSetupProvider;
|
|
89
104
|
}
|
|
90
105
|
|
|
106
|
+
if (provider === "llama-cpp") {
|
|
107
|
+
return askLlamaCppPath({
|
|
108
|
+
ramGb: options.ramGb === undefined ? detectTotalRamGb() : options.ramGb,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
91
111
|
if (provider === "ollama") {
|
|
92
112
|
return askOllamaPath({
|
|
93
113
|
detect: options.detect ?? detectOllama,
|
|
94
114
|
ramGb: options.ramGb === undefined ? detectTotalRamGb() : options.ramGb,
|
|
95
115
|
});
|
|
96
116
|
}
|
|
117
|
+
if (provider === "vllm" || provider === "sglang") {
|
|
118
|
+
return askSelfHostedGpuPath(provider);
|
|
119
|
+
}
|
|
97
120
|
|
|
98
121
|
return askCloudPath(provider);
|
|
99
122
|
}
|
|
100
123
|
|
|
101
124
|
/**
|
|
102
|
-
*
|
|
125
|
+
* llama.cpp path — banner → Select model (tier) / Manual (Docker Hub `ai/`).
|
|
103
126
|
*
|
|
104
|
-
* @param options -
|
|
127
|
+
* @param options - RAM override
|
|
105
128
|
*/
|
|
106
|
-
async function
|
|
107
|
-
readonly detect: () => Promise<OllamaDetectResult>;
|
|
129
|
+
async function askLlamaCppPath(options: {
|
|
108
130
|
readonly ramGb: number | null;
|
|
109
131
|
}): Promise<AiSetupApplyInput | null> {
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
for (;;) {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
: ["(none from the curated list)"]),
|
|
132
|
-
"",
|
|
133
|
-
"Fits comfortably (leaves RAM for OS/IDE)",
|
|
134
|
-
...(comfortable.length > 0
|
|
135
|
-
? comfortable.map((m) => {
|
|
136
|
-
const star = m.id === recommended.id ? " ⭐ for you" : "";
|
|
137
|
-
const have = isInstalled(m.id, detected.installed) ? " · installed" : "";
|
|
138
|
-
return `• ${m.id} — ≈${m.ramGb}GB-class${star}${have}`;
|
|
139
|
-
})
|
|
140
|
-
: ["(none — see tight fits below)"]),
|
|
141
|
-
];
|
|
142
|
-
if (tight.length > 0) {
|
|
143
|
-
panel.push(
|
|
144
|
-
"",
|
|
145
|
-
"Fits the tier but tight (little headroom)",
|
|
146
|
-
...tight.map((m) => {
|
|
147
|
-
const star = m.id === recommended.id ? " ⭐ for you" : "";
|
|
148
|
-
return `• ${m.id} — ≈${m.ramGb}GB-class${star}`;
|
|
149
|
-
}),
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
if (tooLarge.length > 0) {
|
|
153
|
-
panel.push(
|
|
154
|
-
"",
|
|
155
|
-
`Too large for ~${options.ramGb ?? "?"}GB (needs a bigger machine tier)`,
|
|
156
|
-
...tooLarge.map((m) => `• ${m.id} — ≈${m.ramGb}GB-class`),
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
panel.push(
|
|
160
|
-
"",
|
|
161
|
-
`Tip: Ollama download size ≪ machine RAM · keep ~${OS_HEADROOM_GB}GB free while running.`,
|
|
132
|
+
const machine = detectMachineInfo();
|
|
133
|
+
const ramGb = options.ramGb ?? machine.ramGb;
|
|
134
|
+
note(formatLlamaCppBanner({ ...machine, ramGb }), "llama.cpp");
|
|
135
|
+
|
|
136
|
+
mode: for (;;) {
|
|
137
|
+
const mode = await selectWithBack(
|
|
138
|
+
"How do you want to pick models?",
|
|
139
|
+
[
|
|
140
|
+
{
|
|
141
|
+
value: "select",
|
|
142
|
+
label: "Select model",
|
|
143
|
+
hint: "Ultra Fast · Fast · Balanced · Smart",
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
value: "manual",
|
|
147
|
+
label: "Manual model",
|
|
148
|
+
hint: "type any Docker Hub ai/ model id",
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
"select",
|
|
152
|
+
false,
|
|
162
153
|
);
|
|
163
|
-
if (
|
|
164
|
-
|
|
154
|
+
if (mode === null) return null;
|
|
155
|
+
|
|
156
|
+
if (mode === "manual") {
|
|
157
|
+
const id = await askOtherModelId("smollm2");
|
|
158
|
+
if (id === null) return null;
|
|
159
|
+
return finishLlamaCpp(id);
|
|
165
160
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
hint: `${recommended.label} · ≈${recommended.ramGb}GB-class (not your full ${options.ramGb ?? "?"}GB RAM)`,
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
value: "installed",
|
|
180
|
-
label: "✓ Use installed models",
|
|
181
|
-
hint: detected.curatedInstalled.length === 0 ? "none detected" : "no download",
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
value: "manual",
|
|
185
|
-
label: "◆ Select manually",
|
|
186
|
-
hint: "curated list · fit / tight labeled",
|
|
187
|
-
},
|
|
188
|
-
],
|
|
189
|
-
detected.curatedInstalled.some((m) => m.id === recommended.id)
|
|
190
|
-
? "installed"
|
|
191
|
-
: "recommended",
|
|
161
|
+
|
|
162
|
+
tier: for (;;) {
|
|
163
|
+
const tierPick = await selectWithBack(
|
|
164
|
+
"Select model",
|
|
165
|
+
MODEL_TIERS.map((t) => ({
|
|
166
|
+
value: t.value,
|
|
167
|
+
label: t.label,
|
|
168
|
+
hint: t.hint,
|
|
169
|
+
})),
|
|
170
|
+
suggestTierForRam(ramGb),
|
|
192
171
|
true,
|
|
193
172
|
);
|
|
194
|
-
if (
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
173
|
+
if (tierPick === null) return null;
|
|
174
|
+
if (tierPick === BACK) continue mode;
|
|
175
|
+
|
|
176
|
+
const tier = tierPick as ModelTier;
|
|
177
|
+
const recommended = recommendLlamaCppForTier(tier, ramGb);
|
|
178
|
+
|
|
179
|
+
how: for (;;) {
|
|
180
|
+
const how = await selectWithBack(
|
|
181
|
+
MODEL_TIERS.find((t) => t.value === tier)?.label ?? tier,
|
|
182
|
+
[
|
|
183
|
+
{
|
|
184
|
+
value: "recommended",
|
|
185
|
+
label: "Use recommended",
|
|
186
|
+
hint: formatModelRow(recommended),
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
value: "manual",
|
|
190
|
+
label: "Select manually",
|
|
191
|
+
hint: "up to 20 models in this tier",
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
"recommended",
|
|
195
|
+
true,
|
|
200
196
|
);
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
`Download ${pull.join(", ")}?`,
|
|
204
|
-
[
|
|
205
|
-
{
|
|
206
|
-
value: "yes",
|
|
207
|
-
label: "✓ Yes, download",
|
|
208
|
-
hint: `${recommended.label} · ≈${recommended.ramGb}GB-class machine (pull ≪ RAM)`,
|
|
209
|
-
},
|
|
210
|
-
{ value: "no", label: "✗ No, write ids only", hint: "pull later yourself" },
|
|
211
|
-
],
|
|
212
|
-
"yes",
|
|
213
|
-
true,
|
|
214
|
-
);
|
|
215
|
-
if (doPull === null) return null;
|
|
216
|
-
if (doPull === BACK) continue;
|
|
217
|
-
if (doPull === "yes") await pullModels(pull);
|
|
218
|
-
}
|
|
219
|
-
return {
|
|
220
|
-
driver: "ollama",
|
|
221
|
-
baseUrl: detected.baseUrl,
|
|
222
|
-
chatModel: recommended.id,
|
|
223
|
-
visionModel: visionRec?.id ?? null,
|
|
224
|
-
embedModel: embedRec.id,
|
|
225
|
-
};
|
|
226
|
-
}
|
|
197
|
+
if (how === null) return null;
|
|
198
|
+
if (how === BACK) continue tier;
|
|
227
199
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
note("No curated models installed — try recommended or manual.", "Ollama");
|
|
231
|
-
continue;
|
|
200
|
+
if (how === "recommended") {
|
|
201
|
+
return finishLlamaCpp(recommended.id, recommended);
|
|
232
202
|
}
|
|
233
|
-
const chat =
|
|
234
|
-
detected.curatedInstalled.find(
|
|
235
|
-
(m) => m.role === "chat" && modelFitsComfortably(m, options.ramGb),
|
|
236
|
-
) ??
|
|
237
|
-
detected.curatedInstalled.find(
|
|
238
|
-
(m) => m.role === "chat" && modelFitsOnMachine(m, options.ramGb),
|
|
239
|
-
) ??
|
|
240
|
-
detected.curatedInstalled.find((m) => m.role === "chat") ??
|
|
241
|
-
detected.curatedInstalled[0]!;
|
|
242
|
-
const vision = needs.wantVision
|
|
243
|
-
? (detected.curatedInstalled.find((m) => m.role === "vision" && m.id !== chat.id) ??
|
|
244
|
-
visionRec)
|
|
245
|
-
: null;
|
|
246
|
-
const embed = detected.curatedInstalled.find((m) => m.role === "embed") ?? embedRec;
|
|
247
|
-
return {
|
|
248
|
-
driver: "ollama",
|
|
249
|
-
baseUrl: detected.baseUrl,
|
|
250
|
-
chatModel: chat.id,
|
|
251
|
-
visionModel: vision?.id ?? null,
|
|
252
|
-
embedModel: embed.id,
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
203
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
204
|
+
const list = llamaCppModelsForTier(tier);
|
|
205
|
+
note(formatModelTableHeader(), "Select model");
|
|
206
|
+
const picked = await selectWithBack(
|
|
207
|
+
"Pick a model",
|
|
208
|
+
list.map((m) => ({
|
|
209
|
+
value: m.id,
|
|
210
|
+
label: formatModelRow(m),
|
|
211
|
+
hint: m.hint,
|
|
212
|
+
})),
|
|
213
|
+
recommended.id,
|
|
214
|
+
true,
|
|
215
|
+
);
|
|
216
|
+
if (picked === null) return null;
|
|
217
|
+
if (picked === BACK) continue how;
|
|
218
|
+
const model = list.find((m) => m.id === picked);
|
|
219
|
+
return finishLlamaCpp(picked, model);
|
|
220
|
+
}
|
|
260
221
|
}
|
|
261
222
|
}
|
|
262
223
|
}
|
|
263
224
|
|
|
264
225
|
/**
|
|
265
|
-
*
|
|
226
|
+
* Build apply input for llama.cpp — OpenAI-compatible + curated image pin.
|
|
266
227
|
*
|
|
267
|
-
* @
|
|
228
|
+
* @param chatId - Docker Hub `ai/` model id (org prefix optional)
|
|
229
|
+
* @param catalog - Optional catalog row (modalities)
|
|
268
230
|
*/
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
"What will you use local AI for most?",
|
|
272
|
-
[
|
|
273
|
-
{ value: "coding", label: "◆ Coding & agents", hint: "Qwen-family tends to win" },
|
|
274
|
-
{ value: "general", label: "◎ General chat", hint: "Gemma / Llama" },
|
|
275
|
-
{ value: "reasoning", label: "◉ Reasoning", hint: "DeepSeek R1-class" },
|
|
276
|
-
{ value: "balanced", label: "★ Not sure — balanced", hint: "safe default for most devs" },
|
|
277
|
-
],
|
|
278
|
-
"balanced",
|
|
279
|
-
false,
|
|
280
|
-
);
|
|
281
|
-
if (use === null || use === BACK) return null;
|
|
282
|
-
|
|
283
|
-
const priority = await selectWithBack(
|
|
284
|
-
"Speed or quality?",
|
|
285
|
-
[
|
|
286
|
-
{ value: "speed", label: "⚡ Speed", hint: "smaller model · 8GB-class" },
|
|
287
|
-
{ value: "balanced", label: "★ Balanced", hint: "best fit for 8–16GB machines" },
|
|
288
|
-
{ value: "quality", label: "◆ Quality", hint: "largest that still leaves RAM free" },
|
|
289
|
-
],
|
|
290
|
-
"balanced",
|
|
291
|
-
true,
|
|
292
|
-
);
|
|
293
|
-
if (priority === null) return null;
|
|
294
|
-
if (priority === BACK) return askAiNeedsQuiz();
|
|
295
|
-
|
|
296
|
-
const vision = await selectWithBack(
|
|
297
|
-
"Need a vision model (images)?",
|
|
298
|
-
[
|
|
299
|
-
{ value: "yes", label: "✓ Yes", hint: "adds a small VL model if RAM allows" },
|
|
300
|
-
{ value: "no", label: "✗ No", hint: "chat + embed only" },
|
|
301
|
-
],
|
|
302
|
-
"no",
|
|
303
|
-
true,
|
|
304
|
-
);
|
|
305
|
-
if (vision === null) return null;
|
|
306
|
-
if (vision === BACK) return askAiNeedsQuiz();
|
|
307
|
-
|
|
231
|
+
function finishLlamaCpp(chatId: string, catalog?: CatalogModel): AiSetupApplyInput {
|
|
232
|
+
const id = chatId.replace(/^ai\//, "");
|
|
308
233
|
return {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
234
|
+
driver: "openai-compatible",
|
|
235
|
+
baseUrl: process.env.OKE_AI_URL ?? "http://127.0.0.1:8080/v1",
|
|
236
|
+
chatModel: id,
|
|
237
|
+
visionModel: catalog?.modalities.includes("vision") ? id : null,
|
|
238
|
+
embedModel: null,
|
|
239
|
+
image: LLAMA_CPP_IMAGE,
|
|
312
240
|
};
|
|
313
241
|
}
|
|
314
242
|
|
|
315
243
|
/**
|
|
316
|
-
*
|
|
244
|
+
* vLLM / SGLang — Hugging Face model path for self-hosted GPU inference.
|
|
317
245
|
*
|
|
318
|
-
* @param
|
|
319
|
-
* @param recommended - Recommended chat
|
|
320
|
-
* @param needs - Quiz answers (vision optional)
|
|
321
|
-
* @param ramGb - Machine RAM
|
|
322
|
-
* @param allowBack - Offer ← Back to mode menu
|
|
246
|
+
* @param provider - vllm | sglang
|
|
323
247
|
*/
|
|
324
|
-
async function
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
seen.add(m.id);
|
|
339
|
-
return true;
|
|
248
|
+
async function askSelfHostedGpuPath(
|
|
249
|
+
provider: "vllm" | "sglang",
|
|
250
|
+
): Promise<AiSetupApplyInput | null> {
|
|
251
|
+
const port = provider === "vllm" ? 8000 : 30000;
|
|
252
|
+
const image =
|
|
253
|
+
provider === "vllm" ? "vllm/vllm-openai:v0.26.0" : "lmsysorg/sglang:v0.5.16-runtime";
|
|
254
|
+
const model = await text({
|
|
255
|
+
message: "Hugging Face model id",
|
|
256
|
+
placeholder: "Qwen/Qwen3-0.6B",
|
|
257
|
+
initialValue: "Qwen/Qwen3-0.6B",
|
|
258
|
+
validate: (v) => {
|
|
259
|
+
if (!v?.trim()) return "Model id required";
|
|
260
|
+
return undefined;
|
|
261
|
+
},
|
|
340
262
|
});
|
|
263
|
+
if (isCancel(model)) return null;
|
|
264
|
+
return {
|
|
265
|
+
driver: "openai-compatible",
|
|
266
|
+
baseUrl: process.env.OKE_AI_URL ?? `http://127.0.0.1:${port}/v1`,
|
|
267
|
+
chatModel: String(model).trim(),
|
|
268
|
+
visionModel: null,
|
|
269
|
+
embedModel: null,
|
|
270
|
+
image,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
341
273
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
274
|
+
/**
|
|
275
|
+
* Ollama path — banner → Select model (tier) / Manual model.
|
|
276
|
+
*
|
|
277
|
+
* @param options - Detect / RAM
|
|
278
|
+
*/
|
|
279
|
+
async function askOllamaPath(options: {
|
|
280
|
+
readonly detect: () => Promise<OllamaDetectResult>;
|
|
281
|
+
readonly ramGb: number | null;
|
|
282
|
+
}): Promise<AiSetupApplyInput | null> {
|
|
283
|
+
const detected = await options.detect();
|
|
284
|
+
const machine = detectMachineInfo();
|
|
285
|
+
const ramGb = options.ramGb ?? machine.ramGb;
|
|
286
|
+
const detectedIds =
|
|
287
|
+
detected.installed.length > 0
|
|
288
|
+
? detected.installed.slice(0, 12)
|
|
289
|
+
: detected.curatedInstalled.map((m) => m.id);
|
|
290
|
+
|
|
291
|
+
note(formatOllamaBanner({ ...machine, ramGb }, detectedIds), "Ollama");
|
|
292
|
+
|
|
293
|
+
mode: for (;;) {
|
|
294
|
+
const mode = await selectWithBack(
|
|
295
|
+
"How do you want to pick models?",
|
|
296
|
+
[
|
|
297
|
+
{
|
|
298
|
+
value: "select",
|
|
299
|
+
label: "Select model",
|
|
300
|
+
hint: "Ultra Fast · Fast · Balanced · Smart",
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
value: "manual",
|
|
304
|
+
label: "Manual model",
|
|
305
|
+
hint: "type any Ollama model id",
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
"select",
|
|
353
309
|
false,
|
|
354
|
-
allowBack,
|
|
355
|
-
ramGb,
|
|
356
310
|
);
|
|
357
|
-
if (
|
|
358
|
-
if (chatPick === BACK) return BACK;
|
|
359
|
-
if (typeof chatPick !== "string") continue;
|
|
360
|
-
chat = chatPick;
|
|
361
|
-
|
|
362
|
-
if (needs.wantVision) {
|
|
363
|
-
const visionPick = await pickModel(
|
|
364
|
-
"Vision Model",
|
|
365
|
-
VISION_MODELS,
|
|
366
|
-
vision ?? recommendForRole("vision").id,
|
|
367
|
-
false,
|
|
368
|
-
true,
|
|
369
|
-
true,
|
|
370
|
-
ramGb,
|
|
371
|
-
);
|
|
372
|
-
if (visionPick === null) return null;
|
|
373
|
-
if (visionPick === BACK) continue;
|
|
374
|
-
vision = visionPick === undefined ? null : visionPick;
|
|
375
|
-
}
|
|
311
|
+
if (mode === null) return null;
|
|
376
312
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
false,
|
|
383
|
-
true,
|
|
384
|
-
ramGb,
|
|
385
|
-
);
|
|
386
|
-
if (embedPick === null) return null;
|
|
387
|
-
if (embedPick === BACK) continue;
|
|
388
|
-
if (typeof embedPick !== "string") continue;
|
|
313
|
+
if (mode === "manual") {
|
|
314
|
+
const id = await askOtherModelId("gemma4:e4b");
|
|
315
|
+
if (id === null) return null;
|
|
316
|
+
return finishOllama(id, detected);
|
|
317
|
+
}
|
|
389
318
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
],
|
|
400
|
-
"yes",
|
|
319
|
+
tier: for (;;) {
|
|
320
|
+
const tierPick = await selectWithBack(
|
|
321
|
+
"Select model",
|
|
322
|
+
MODEL_TIERS.map((t) => ({
|
|
323
|
+
value: t.value,
|
|
324
|
+
label: t.label,
|
|
325
|
+
hint: t.hint,
|
|
326
|
+
})),
|
|
327
|
+
suggestTierForRam(ramGb),
|
|
401
328
|
true,
|
|
402
329
|
);
|
|
403
|
-
if (
|
|
404
|
-
if (
|
|
405
|
-
|
|
406
|
-
|
|
330
|
+
if (tierPick === null) return null;
|
|
331
|
+
if (tierPick === BACK) continue mode;
|
|
332
|
+
|
|
333
|
+
const tier = tierPick as ModelTier;
|
|
334
|
+
const recommended = recommendForTier(tier, ramGb);
|
|
335
|
+
|
|
336
|
+
how: for (;;) {
|
|
337
|
+
const how = await selectWithBack(
|
|
338
|
+
MODEL_TIERS.find((t) => t.value === tier)?.label ?? tier,
|
|
339
|
+
[
|
|
340
|
+
{
|
|
341
|
+
value: "recommended",
|
|
342
|
+
label: "Use recommended",
|
|
343
|
+
hint: formatModelRow(recommended),
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
value: "manual",
|
|
347
|
+
label: "Select manually",
|
|
348
|
+
hint: "up to 10 models in this tier",
|
|
349
|
+
},
|
|
350
|
+
],
|
|
351
|
+
"recommended",
|
|
352
|
+
true,
|
|
353
|
+
);
|
|
354
|
+
if (how === null) return null;
|
|
355
|
+
if (how === BACK) continue tier;
|
|
407
356
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
357
|
+
if (how === "recommended") {
|
|
358
|
+
return finishOllama(recommended.id, detected, recommended);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const list = modelsForTier(tier);
|
|
362
|
+
note(formatModelTableHeader(), "Select model");
|
|
363
|
+
const picked = await selectWithBack(
|
|
364
|
+
"Pick a model",
|
|
365
|
+
list.map((m) => ({
|
|
366
|
+
value: m.id,
|
|
367
|
+
label: formatModelRow(m),
|
|
368
|
+
hint: isInstalled(m.id, detected.installed) ? "installed" : m.hint,
|
|
369
|
+
})),
|
|
370
|
+
recommended.id,
|
|
371
|
+
true,
|
|
372
|
+
);
|
|
373
|
+
if (picked === null) return null;
|
|
374
|
+
if (picked === BACK) continue how;
|
|
375
|
+
const model = list.find((m) => m.id === picked);
|
|
376
|
+
return finishOllama(picked, detected, model);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
415
379
|
}
|
|
416
380
|
}
|
|
417
381
|
|
|
418
382
|
/**
|
|
419
|
-
*
|
|
383
|
+
* Build apply input for Ollama — embed default, optional silent pull.
|
|
384
|
+
*
|
|
385
|
+
* @param chatId - Chat model id
|
|
386
|
+
* @param detected - Detect result
|
|
387
|
+
* @param catalog - Optional catalog row (modalities)
|
|
420
388
|
*/
|
|
421
|
-
async function
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
389
|
+
async function finishOllama(
|
|
390
|
+
chatId: string,
|
|
391
|
+
detected: OllamaDetectResult,
|
|
392
|
+
catalog?: CatalogModel,
|
|
393
|
+
): Promise<AiSetupApplyInput> {
|
|
394
|
+
const embed = recommendForRole("embed");
|
|
395
|
+
const visionModel = catalog?.modalities.includes("vision") ? chatId : null;
|
|
396
|
+
|
|
397
|
+
const needed = [chatId, embed.id].filter(
|
|
398
|
+
(id) => detected.available && !isInstalled(id, detected.installed),
|
|
399
|
+
);
|
|
400
|
+
if (needed.length > 0 && detected.available) {
|
|
401
|
+
await pullModels(needed, detected.baseUrl);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
driver: "ollama",
|
|
406
|
+
baseUrl: detected.baseUrl,
|
|
407
|
+
chatModel: chatId,
|
|
408
|
+
visionModel,
|
|
409
|
+
embedModel: embed.id,
|
|
410
|
+
};
|
|
433
411
|
}
|
|
434
412
|
|
|
435
413
|
/**
|
|
436
|
-
* Cloud / openai-compatible path —
|
|
414
|
+
* Cloud / openai-compatible path — API token → Select model / Manual.
|
|
437
415
|
*
|
|
438
416
|
* @param provider - Menu provider
|
|
439
417
|
*/
|
|
@@ -443,83 +421,136 @@ async function askCloudPath(provider: AiSetupProvider): Promise<AiSetupApplyInpu
|
|
|
443
421
|
|
|
444
422
|
let baseUrl: string | undefined = meta?.baseUrl;
|
|
445
423
|
if (provider === "custom" || provider === "gemini" || !baseUrl) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
424
|
+
if (provider !== "anthropic") {
|
|
425
|
+
const urlValue = await text({
|
|
426
|
+
message:
|
|
427
|
+
provider === "gemini"
|
|
428
|
+
? "OpenAI-compatible base URL for Gemini"
|
|
429
|
+
: "OpenAI-compatible base URL",
|
|
430
|
+
placeholder: baseUrl ?? "https://api.example.com/v1",
|
|
431
|
+
initialValue: baseUrl ?? "",
|
|
432
|
+
});
|
|
433
|
+
if (isCancel(urlValue)) return null;
|
|
434
|
+
const trimmed = String(urlValue).trim();
|
|
435
|
+
baseUrl = trimmed.length > 0 ? trimmed : undefined;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const apiKeyEnv = meta?.apiKeyEnv;
|
|
440
|
+
let apiKey: string | undefined;
|
|
441
|
+
if (apiKeyEnv) {
|
|
442
|
+
const token = await password({
|
|
443
|
+
message: `API token (${apiKeyEnv})`,
|
|
444
|
+
validate: (v) => {
|
|
445
|
+
if (!v?.trim()) return "API token is required";
|
|
446
|
+
return undefined;
|
|
447
|
+
},
|
|
453
448
|
});
|
|
454
|
-
if (isCancel(
|
|
455
|
-
|
|
456
|
-
baseUrl = trimmed.length > 0 ? trimmed : undefined;
|
|
449
|
+
if (isCancel(token)) return null;
|
|
450
|
+
apiKey = String(token).trim();
|
|
457
451
|
}
|
|
458
452
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
453
|
+
for (;;) {
|
|
454
|
+
const mode = await selectWithBack(
|
|
455
|
+
"How do you want to pick models?",
|
|
456
|
+
[
|
|
457
|
+
{
|
|
458
|
+
value: "select",
|
|
459
|
+
label: "Select model",
|
|
460
|
+
hint: "up to 10 latest curated models",
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
value: "manual",
|
|
464
|
+
label: "Manual model",
|
|
465
|
+
hint: "type any model id",
|
|
466
|
+
},
|
|
467
|
+
],
|
|
468
|
+
"select",
|
|
469
|
+
false,
|
|
470
|
+
);
|
|
471
|
+
if (mode === null) return null;
|
|
472
|
+
|
|
473
|
+
if (mode === "manual") {
|
|
474
|
+
const id = await askOtherModelId(recommendCloudChat(provider));
|
|
475
|
+
if (id === null) return null;
|
|
476
|
+
return {
|
|
477
|
+
driver,
|
|
478
|
+
...(baseUrl !== undefined ? { baseUrl } : {}),
|
|
479
|
+
chatModel: id,
|
|
480
|
+
visionModel: null,
|
|
481
|
+
embedModel: null,
|
|
482
|
+
...(apiKeyEnv ? { apiKeyEnv, ...(apiKey ? { apiKey } : {}) } : {}),
|
|
483
|
+
};
|
|
484
|
+
}
|
|
462
485
|
|
|
463
|
-
|
|
486
|
+
const models = cloudChatModels(provider);
|
|
487
|
+
if (models.length === 0) {
|
|
488
|
+
const id = await askOtherModelId(recommendCloudChat(provider));
|
|
489
|
+
if (id === null) return null;
|
|
490
|
+
return {
|
|
491
|
+
driver,
|
|
492
|
+
...(baseUrl !== undefined ? { baseUrl } : {}),
|
|
493
|
+
chatModel: id,
|
|
494
|
+
visionModel: null,
|
|
495
|
+
embedModel: null,
|
|
496
|
+
...(apiKeyEnv ? { apiKeyEnv, ...(apiKey ? { apiKey } : {}) } : {}),
|
|
497
|
+
};
|
|
498
|
+
}
|
|
464
499
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
500
|
+
const initial = recommendCloudChat(provider);
|
|
501
|
+
const chat = await selectWithBack(
|
|
502
|
+
"Select model",
|
|
503
|
+
models.map((m) => ({
|
|
504
|
+
value: m.id,
|
|
505
|
+
label: m.label,
|
|
506
|
+
hint: m.hint,
|
|
507
|
+
})),
|
|
508
|
+
models.some((m) => m.id === initial) ? initial : models[0]!.id,
|
|
509
|
+
true,
|
|
510
|
+
);
|
|
511
|
+
if (chat === null) return null;
|
|
512
|
+
if (chat === BACK) continue;
|
|
474
513
|
|
|
475
|
-
/**
|
|
476
|
-
* Format a catalog row — for Ollama, annotate fit vs too-large.
|
|
477
|
-
*
|
|
478
|
-
* @param m - Catalog / cloud model
|
|
479
|
-
* @param ramGb - Machine RAM (ollama only)
|
|
480
|
-
* @param recommendedId - Id marked "for you"
|
|
481
|
-
*/
|
|
482
|
-
function catalogOption(
|
|
483
|
-
m: CatalogModel | CloudModel,
|
|
484
|
-
ramGb: number | null = null,
|
|
485
|
-
recommendedId?: string,
|
|
486
|
-
): { value: string; label: string; hint: string } {
|
|
487
|
-
const isRec = m.recommended || m.id === recommendedId;
|
|
488
|
-
if ("ramGb" in m && typeof m.ramGb === "number") {
|
|
489
|
-
const onMachine = modelFitsOnMachine(m, ramGb);
|
|
490
|
-
const comfortable = modelFitsComfortably(m, ramGb);
|
|
491
|
-
const fitLabel = !onMachine
|
|
492
|
-
? "too large for this machine"
|
|
493
|
-
: comfortable
|
|
494
|
-
? "fits comfortably"
|
|
495
|
-
: "tight fit";
|
|
496
514
|
return {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
515
|
+
driver,
|
|
516
|
+
...(baseUrl !== undefined ? { baseUrl } : {}),
|
|
517
|
+
chatModel: chat,
|
|
518
|
+
visionModel: null,
|
|
519
|
+
embedModel: null,
|
|
520
|
+
...(apiKeyEnv ? { apiKeyEnv, ...(apiKey ? { apiKey } : {}) } : {}),
|
|
500
521
|
};
|
|
501
522
|
}
|
|
502
|
-
return {
|
|
503
|
-
value: m.id,
|
|
504
|
-
label: isRec ? `⭐ ${m.label} (Recommended)` : ` ${m.label}`,
|
|
505
|
-
hint: m.hint,
|
|
506
|
-
};
|
|
507
523
|
}
|
|
508
524
|
|
|
509
|
-
|
|
525
|
+
/**
|
|
526
|
+
* Select with optional trailing Back.
|
|
527
|
+
*/
|
|
528
|
+
async function selectWithBack(
|
|
529
|
+
message: string,
|
|
530
|
+
options: readonly { value: string; label: string; hint?: string }[],
|
|
531
|
+
initialValue: string,
|
|
532
|
+
allowBack: boolean,
|
|
533
|
+
): Promise<string | Back | null> {
|
|
534
|
+
const list = [...options, ...(allowBack ? [{ value: BACK, label: "Back" }] : [])];
|
|
535
|
+
const value = await select({ message, options: list, initialValue });
|
|
536
|
+
if (isCancel(value)) return null;
|
|
537
|
+
const picked = String(value);
|
|
538
|
+
if (picked === BACK) return BACK;
|
|
539
|
+
return picked;
|
|
540
|
+
}
|
|
510
541
|
|
|
511
542
|
/**
|
|
512
|
-
* Ask for a custom model id
|
|
543
|
+
* Ask for a custom model id.
|
|
513
544
|
*
|
|
514
545
|
* @param placeholder - Example id
|
|
515
546
|
*/
|
|
516
547
|
async function askOtherModelId(placeholder: string): Promise<string | null> {
|
|
517
548
|
const value = await text({
|
|
518
|
-
message: "Model
|
|
549
|
+
message: "Model name",
|
|
519
550
|
placeholder,
|
|
520
551
|
initialValue: "",
|
|
521
552
|
validate: (v) => {
|
|
522
|
-
if (!v?.trim()) return "Model
|
|
553
|
+
if (!v?.trim()) return "Model name is required";
|
|
523
554
|
return undefined;
|
|
524
555
|
},
|
|
525
556
|
});
|
|
@@ -528,122 +559,35 @@ async function askOtherModelId(placeholder: string): Promise<string | null> {
|
|
|
528
559
|
}
|
|
529
560
|
|
|
530
561
|
/**
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
* @param message - Prompt
|
|
534
|
-
* @param models - Curated list
|
|
535
|
-
* @param initial - Default id
|
|
536
|
-
*/
|
|
537
|
-
async function pickCloudModel(
|
|
538
|
-
message: string,
|
|
539
|
-
models: readonly CloudModel[],
|
|
540
|
-
initial: string,
|
|
541
|
-
): Promise<string | null> {
|
|
542
|
-
const options: { value: string; label: string; hint?: string }[] = [
|
|
543
|
-
...models.map((m) => catalogOption(m)),
|
|
544
|
-
{
|
|
545
|
-
value: OTHER_MODEL,
|
|
546
|
-
label: "⋯ Other…",
|
|
547
|
-
hint: "type any model id",
|
|
548
|
-
},
|
|
549
|
-
];
|
|
550
|
-
const initialValue = models.some((m) => m.id === initial) ? initial : OTHER_MODEL;
|
|
551
|
-
const value = await select({
|
|
552
|
-
message,
|
|
553
|
-
options,
|
|
554
|
-
initialValue,
|
|
555
|
-
});
|
|
556
|
-
if (isCancel(value)) return null;
|
|
557
|
-
const picked = String(value);
|
|
558
|
-
if (picked === OTHER_MODEL) {
|
|
559
|
-
return askOtherModelId(initial || "model-id");
|
|
560
|
-
}
|
|
561
|
-
return picked;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
/**
|
|
565
|
-
* Pick from a curated list, with optional Show all + Skip + Other + Back.
|
|
566
|
-
*
|
|
567
|
-
* @returns model id, null cancel, undefined skip, BACK
|
|
568
|
-
*/
|
|
569
|
-
async function pickModel(
|
|
570
|
-
message: string,
|
|
571
|
-
models: readonly CatalogModel[],
|
|
572
|
-
initial: string,
|
|
573
|
-
showAllHint: boolean,
|
|
574
|
-
allowSkip = false,
|
|
575
|
-
allowBack = false,
|
|
576
|
-
ramGb: number | null = null,
|
|
577
|
-
): Promise<string | Back | null | undefined> {
|
|
578
|
-
const short = models.slice(0, 5);
|
|
579
|
-
const options: { value: string; label: string; hint?: string }[] = short.map((m) =>
|
|
580
|
-
catalogOption(m, ramGb, initial),
|
|
581
|
-
);
|
|
582
|
-
if (showAllHint && models.length > short.length) {
|
|
583
|
-
options.push({ value: "__all__", label: "⋯ Show all models", hint: "full curated catalog" });
|
|
584
|
-
}
|
|
585
|
-
options.push({
|
|
586
|
-
value: OTHER_MODEL,
|
|
587
|
-
label: "⋯ Other…",
|
|
588
|
-
hint: "type any model id",
|
|
589
|
-
});
|
|
590
|
-
if (allowSkip) {
|
|
591
|
-
options.push({ value: "__skip__", label: "⊘ Skip", hint: "no vision model" });
|
|
592
|
-
}
|
|
593
|
-
if (allowBack) {
|
|
594
|
-
options.push({ value: BACK, label: "← Back" });
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
const value = await select({
|
|
598
|
-
message,
|
|
599
|
-
options,
|
|
600
|
-
initialValue: models.some((m) => m.id === initial) ? initial : short[0]?.id,
|
|
601
|
-
});
|
|
602
|
-
if (isCancel(value)) return null;
|
|
603
|
-
const picked = String(value);
|
|
604
|
-
if (picked === BACK) return BACK;
|
|
605
|
-
if (picked === "__skip__") return undefined;
|
|
606
|
-
if (picked === OTHER_MODEL) {
|
|
607
|
-
return askOtherModelId(initial);
|
|
608
|
-
}
|
|
609
|
-
if (picked === "__all__") {
|
|
610
|
-
const allOptions = [
|
|
611
|
-
...models.map((m) => catalogOption(m, ramGb, initial)),
|
|
612
|
-
{ value: OTHER_MODEL, label: "⋯ Other…", hint: "type any model id" },
|
|
613
|
-
...(allowSkip ? [{ value: "__skip__", label: "⊘ Skip", hint: "no vision model" }] : []),
|
|
614
|
-
...(allowBack ? [{ value: BACK, label: "← Back" }] : []),
|
|
615
|
-
];
|
|
616
|
-
const allValue = await select({
|
|
617
|
-
message,
|
|
618
|
-
options: allOptions,
|
|
619
|
-
initialValue: initial,
|
|
620
|
-
});
|
|
621
|
-
if (isCancel(allValue)) return null;
|
|
622
|
-
const allPicked = String(allValue);
|
|
623
|
-
if (allPicked === BACK) return BACK;
|
|
624
|
-
if (allPicked === "__skip__") return undefined;
|
|
625
|
-
if (allPicked === OTHER_MODEL) return askOtherModelId(initial);
|
|
626
|
-
return allPicked;
|
|
627
|
-
}
|
|
628
|
-
return picked;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
/**
|
|
632
|
-
* Run `ollama pull` for each model with a spinner-friendly console log.
|
|
562
|
+
* Pull each model via the Ollama HTTP API (`POST /api/pull`).
|
|
633
563
|
*
|
|
634
564
|
* @param models - Model ids
|
|
565
|
+
* @param baseUrl - Ollama server base URL
|
|
635
566
|
*/
|
|
636
|
-
async function pullModels(models: readonly string[]): Promise<void> {
|
|
567
|
+
async function pullModels(models: readonly string[], baseUrl: string): Promise<void> {
|
|
568
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
637
569
|
for (const id of models) {
|
|
638
|
-
console.log(`ollama
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
570
|
+
console.log(`ollama: pulling ${id} via ${base}/api/pull…`);
|
|
571
|
+
try {
|
|
572
|
+
const tags = await fetch(`${base}/api/tags`);
|
|
573
|
+
if (!tags.ok) {
|
|
574
|
+
throw new Error(`GET ${base}/api/tags → ${tags.status}`);
|
|
575
|
+
}
|
|
576
|
+
const res = await fetch(`${base}/api/pull`, {
|
|
577
|
+
method: "POST",
|
|
578
|
+
headers: { "content-type": "application/json" },
|
|
579
|
+
body: JSON.stringify({ model: id, stream: false }),
|
|
580
|
+
});
|
|
581
|
+
if (!res.ok) {
|
|
582
|
+
const body = await res.text().catch(() => "");
|
|
583
|
+
throw new Error(
|
|
584
|
+
`POST ${base}/api/pull → ${res.status}${body ? ` ${body.slice(0, 120)}` : ""}`,
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
await res.arrayBuffer();
|
|
588
|
+
} catch (err) {
|
|
589
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
590
|
+
console.error(`oke ai setup: pull ${id} failed (continuing) — ${msg}`);
|
|
647
591
|
}
|
|
648
592
|
}
|
|
649
593
|
}
|