@freesyntax/notch-cli 0.5.20 → 0.5.22

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.
Files changed (40) hide show
  1. package/dist/{apply-patch-D5PDUXUC.js → apply-patch-U6K67CMT.js} +1 -0
  2. package/dist/auth-UAMMP5IJ.js +29 -0
  3. package/dist/chunk-4HPRBCSY.js +167 -0
  4. package/dist/chunk-6NKRMZTX.js +198 -0
  5. package/dist/{chunk-YBYF7L4A.js → chunk-EPSOOCNB.js} +1832 -1331
  6. package/dist/chunk-FZVPGJJW.js +511 -0
  7. package/dist/chunk-GFVLHUSS.js +155 -0
  8. package/dist/chunk-J66N6AFH.js +137 -0
  9. package/dist/chunk-JXQ4HZ47.js +544 -0
  10. package/dist/chunk-KCAR5DOB.js +52 -0
  11. package/dist/chunk-KFQGP6VL.js +33 -0
  12. package/dist/chunk-O6AKZ4OH.js +0 -0
  13. package/dist/{chunk-6M6CXXWR.js → chunk-PKZKVOAN.js} +209 -1
  14. package/dist/{chunk-FIFC4V2R.js → chunk-PPEBWOMJ.js} +91 -7
  15. package/dist/compression-YJLWEHCC.js +33 -0
  16. package/dist/config-set-3IWEVZQ4.js +110 -0
  17. package/dist/{edit-JEFEK43H.js → edit-6QYAXVNU.js} +1 -0
  18. package/dist/{git-5T5TSQTX.js → git-DNQ5EELH.js} +1 -0
  19. package/dist/{github-DWRGWX6U.js → github-34T4QQIH.js} +1 -0
  20. package/dist/{glob-BI3P4C7Q.js → glob-XT43LEJ4.js} +1 -0
  21. package/dist/{grep-VZ3I5GNW.js → grep-T2CXYNRI.js} +1 -0
  22. package/dist/index.js +2606 -960
  23. package/dist/{lsp-UPY6I3L7.js → lsp-JXQVU7NP.js} +1 -0
  24. package/dist/model-download-3NDKS3VM.js +176 -0
  25. package/dist/{notebook-FXJBTSPA.js → notebook-MFODW345.js} +1 -0
  26. package/dist/ollama-bench-5V5CCOCQ.js +194 -0
  27. package/dist/ollama-launch-P5KBK7AJ.js +22 -0
  28. package/dist/ollama-usage-3PROM2WC.js +70 -0
  29. package/dist/{plugins-OG2P75K5.js → plugins-PNGRZLFW.js} +1 -0
  30. package/dist/{read-OVJG2XKW.js → read-B64XE7N3.js} +1 -0
  31. package/dist/{server-W7FRCVRZ.js → server-IGOZHW52.js} +17 -15
  32. package/dist/session-index-7FWEVP6E.js +22 -0
  33. package/dist/{shell-4X545EVN.js → shell-BOZTHQUT.js} +1 -0
  34. package/dist/{task-OS3E5F3X.js → task-67G4KLYC.js} +1 -0
  35. package/dist/{tools-Q7CDHB4K.js → tools-XWKCW4RN.js} +4 -1
  36. package/dist/{web-fetch-KNIV3Z3W.js → web-fetch-OTNDICGJ.js} +1 -0
  37. package/dist/{write-NNHLOTYK.js → write-ZOSB7I4J.js} +1 -0
  38. package/package.json +2 -1
  39. package/dist/auth-JQX6MHJG.js +0 -16
  40. package/dist/compression-UTB2Y4BB.js +0 -16
@@ -0,0 +1,137 @@
1
+ import {
2
+ readOllamaCreds
3
+ } from "./chunk-KCAR5DOB.js";
4
+ import {
5
+ findByokProvider,
6
+ isByokRef,
7
+ isValidModel
8
+ } from "./chunk-JXQ4HZ47.js";
9
+ import {
10
+ init_auth,
11
+ loadCredentials
12
+ } from "./chunk-PPEBWOMJ.js";
13
+
14
+ // src/config.ts
15
+ import fs from "fs/promises";
16
+ import path from "path";
17
+ init_auth();
18
+ var DEFAULT_MODEL = {
19
+ model: "notch-pyre",
20
+ temperature: 0.3
21
+ };
22
+ var DEFAULTS = {
23
+ models: { chat: DEFAULT_MODEL },
24
+ projectRoot: process.cwd(),
25
+ autoConfirm: false,
26
+ maxIterations: 25,
27
+ useRepoMap: true,
28
+ renderMarkdown: true,
29
+ enableMemory: true,
30
+ enableHooks: true,
31
+ permissionMode: "auto",
32
+ theme: "default"
33
+ };
34
+ async function loadConfig(overrides = {}) {
35
+ const config = { ...DEFAULTS, models: { chat: { ...DEFAULT_MODEL } } };
36
+ const configPath = path.resolve(config.projectRoot, ".notch.json");
37
+ try {
38
+ const raw = await fs.readFile(configPath, "utf-8");
39
+ const fileConfig = JSON.parse(raw);
40
+ if (fileConfig.model && (isValidModel(fileConfig.model) || isByokRef(fileConfig.model))) {
41
+ config.models.chat.model = fileConfig.model;
42
+ }
43
+ if (fileConfig.baseUrl) config.models.chat.baseUrl = fileConfig.baseUrl;
44
+ if (fileConfig.apiKey) config.models.chat.apiKey = fileConfig.apiKey;
45
+ if (fileConfig.byok && typeof fileConfig.byok === "object") {
46
+ const byok = fileConfig.byok;
47
+ config.byok = { ...byok };
48
+ if (byok.provider && findByokProvider(byok.provider === "custom" ? "__custom__" : byok.provider)) {
49
+ config.models.chat.byokProvider = byok.provider === "custom" ? "__custom__" : byok.provider;
50
+ if (byok.model) config.models.chat.model = byok.model;
51
+ if (byok.baseUrl) config.models.chat.baseUrl = byok.baseUrl;
52
+ if (byok.headers) {
53
+ config.models.chat.byokHeaders = { ...config.models.chat.byokHeaders, ...byok.headers };
54
+ }
55
+ if (byok.apiShape === "openai" || byok.apiShape === "anthropic") {
56
+ config.models.chat.byokApiShape = byok.apiShape;
57
+ }
58
+ }
59
+ }
60
+ if (fileConfig.hybrid && typeof fileConfig.hybrid === "object") {
61
+ const hybrid = fileConfig.hybrid;
62
+ config.hybrid = hybrid;
63
+ }
64
+ if (fileConfig.maxIterations) config.maxIterations = fileConfig.maxIterations;
65
+ if (fileConfig.useRepoMap !== void 0) config.useRepoMap = fileConfig.useRepoMap;
66
+ if (fileConfig.temperature !== void 0) config.models.chat.temperature = fileConfig.temperature;
67
+ if (fileConfig.renderMarkdown !== void 0) config.renderMarkdown = fileConfig.renderMarkdown;
68
+ if (fileConfig.enableMemory !== void 0) config.enableMemory = fileConfig.enableMemory;
69
+ if (fileConfig.enableHooks !== void 0) config.enableHooks = fileConfig.enableHooks;
70
+ if (fileConfig.permissionMode) config.permissionMode = fileConfig.permissionMode;
71
+ if (fileConfig.shellTimeout) config.shellTimeout = fileConfig.shellTimeout;
72
+ if (fileConfig.theme) config.theme = fileConfig.theme;
73
+ } catch {
74
+ }
75
+ const activeProviderId = config.models.chat.byokProvider ?? (typeof config.models.chat.model === "string" && isByokRef(config.models.chat.model) ? config.models.chat.model.split(":", 1)[0] : void 0);
76
+ const isOllamaProvider = activeProviderId === "ollama" || activeProviderId === "ollama-cloud" || activeProviderId === "ollama-anthropic";
77
+ if (isOllamaProvider) {
78
+ if (!config.models.chat.apiKey && !process.env.OLLAMA_API_KEY) {
79
+ const ollamaCreds = await readOllamaCreds();
80
+ if (ollamaCreds?.apiKey) {
81
+ config.models.chat.apiKey = ollamaCreds.apiKey;
82
+ }
83
+ }
84
+ } else {
85
+ const creds = await loadCredentials();
86
+ if (creds?.token) {
87
+ config.models.chat.apiKey = creds.token;
88
+ }
89
+ }
90
+ if (process.env.NOTCH_MODEL) {
91
+ const envModel = process.env.NOTCH_MODEL;
92
+ if (isValidModel(envModel)) {
93
+ config.models.chat.model = envModel;
94
+ } else if (isByokRef(envModel)) {
95
+ config.models.chat.model = envModel;
96
+ config.models.chat.byokProvider = void 0;
97
+ }
98
+ }
99
+ if (process.env.NOTCH_BASE_URL) {
100
+ config.models.chat.baseUrl = process.env.NOTCH_BASE_URL;
101
+ }
102
+ if (process.env.NOTCH_API_KEY) {
103
+ config.models.chat.apiKey = process.env.NOTCH_API_KEY;
104
+ }
105
+ if (config.models.chat.temperature !== void 0) {
106
+ config.models.chat.temperature = Math.max(0, Math.min(2, config.models.chat.temperature));
107
+ }
108
+ config.maxIterations = Math.max(1, Math.min(100, config.maxIterations));
109
+ return { ...config, ...overrides };
110
+ }
111
+ async function persistConfigPatch(projectRoot, patch) {
112
+ const configPath = path.resolve(projectRoot, ".notch.json");
113
+ let current = {};
114
+ try {
115
+ const raw = await fs.readFile(configPath, "utf-8");
116
+ const parsed = JSON.parse(raw);
117
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
118
+ current = parsed;
119
+ }
120
+ } catch {
121
+ }
122
+ const merged = { ...current };
123
+ for (const [k, v] of Object.entries(patch)) {
124
+ if (v === void 0) delete merged[k];
125
+ else merged[k] = v;
126
+ }
127
+ const serialised = JSON.stringify(merged, null, 2) + "\n";
128
+ const tmpPath = `${configPath}.tmp`;
129
+ await fs.writeFile(tmpPath, serialised, "utf-8");
130
+ await fs.rename(tmpPath, configPath);
131
+ return merged;
132
+ }
133
+
134
+ export {
135
+ loadConfig,
136
+ persistConfigPatch
137
+ };
@@ -0,0 +1,544 @@
1
+ import {
2
+ auth_exports,
3
+ init_auth
4
+ } from "./chunk-PPEBWOMJ.js";
5
+ import {
6
+ __toCommonJS
7
+ } from "./chunk-KFQGP6VL.js";
8
+
9
+ // src/providers/byok.ts
10
+ import { createOpenAI } from "@ai-sdk/openai";
11
+ import { createAnthropic } from "@ai-sdk/anthropic";
12
+ var BUILTIN_BYOK_PROVIDERS = [
13
+ {
14
+ id: "openai",
15
+ label: "OpenAI",
16
+ baseUrl: "https://api.openai.com/v1",
17
+ apiKeyEnv: "OPENAI_API_KEY",
18
+ defaultModel: "gpt-4o",
19
+ models: [
20
+ "gpt-4o",
21
+ "gpt-4o-mini",
22
+ "gpt-4-turbo",
23
+ "gpt-4.1",
24
+ "gpt-4.1-mini",
25
+ "gpt-5",
26
+ "gpt-5-mini",
27
+ "o3-mini",
28
+ "o4-mini"
29
+ ],
30
+ compatibility: "strict"
31
+ },
32
+ {
33
+ id: "anthropic",
34
+ label: "Anthropic (Claude)",
35
+ // Anthropic's OpenAI compat lives at /v1/ (root), not /v1/openai.
36
+ // See https://platform.claude.com/docs/en/api/openai-sdk
37
+ baseUrl: "https://api.anthropic.com/v1",
38
+ apiKeyEnv: "ANTHROPIC_API_KEY",
39
+ defaultModel: "claude-sonnet-4-6",
40
+ models: [
41
+ "claude-opus-4-7",
42
+ "claude-sonnet-4-6",
43
+ "claude-haiku-4-5"
44
+ ],
45
+ compatibility: "compatible",
46
+ // anthropic-version is optional on the OpenAI compat layer, but we
47
+ // pin it so behaviour is deterministic across CLI releases.
48
+ headers: {
49
+ "anthropic-version": "2023-06-01"
50
+ }
51
+ },
52
+ {
53
+ id: "openrouter",
54
+ label: "OpenRouter",
55
+ baseUrl: "https://openrouter.ai/api/v1",
56
+ apiKeyEnv: "OPENROUTER_API_KEY",
57
+ defaultModel: "anthropic/claude-sonnet-4-6",
58
+ // OpenRouter exposes hundreds of models — leave undefined and let
59
+ // users discover via openrouter.ai/models.
60
+ compatibility: "strict",
61
+ headers: {
62
+ "HTTP-Referer": "https://driftrail.com/notch",
63
+ "X-Title": "Notch CLI"
64
+ }
65
+ },
66
+ {
67
+ id: "together",
68
+ label: "Together AI",
69
+ baseUrl: "https://api.together.xyz/v1",
70
+ apiKeyEnv: "TOGETHER_API_KEY",
71
+ defaultModel: "meta-llama/Llama-4-70B-Instruct",
72
+ models: [
73
+ "meta-llama/Llama-4-70B-Instruct",
74
+ "meta-llama/Llama-4-8B-Instruct",
75
+ "meta-llama/Llama-3.3-70B-Instruct-Turbo",
76
+ "Qwen/Qwen2.5-72B-Instruct-Turbo",
77
+ "Qwen/QwQ-32B-Preview",
78
+ "deepseek-ai/DeepSeek-V3",
79
+ "mistralai/Mixtral-8x22B-Instruct-v0.1"
80
+ ],
81
+ compatibility: "strict"
82
+ },
83
+ {
84
+ id: "fireworks",
85
+ label: "Fireworks AI",
86
+ baseUrl: "https://api.fireworks.ai/inference/v1",
87
+ apiKeyEnv: "FIREWORKS_API_KEY",
88
+ defaultModel: "accounts/fireworks/models/llama-v4-70b-instruct",
89
+ models: [
90
+ "accounts/fireworks/models/llama-v4-70b-instruct",
91
+ "accounts/fireworks/models/llama-v3p3-70b-instruct",
92
+ "accounts/fireworks/models/qwen2p5-72b-instruct",
93
+ "accounts/fireworks/models/deepseek-v3",
94
+ "accounts/fireworks/models/mixtral-8x22b-instruct"
95
+ ],
96
+ compatibility: "strict"
97
+ },
98
+ {
99
+ id: "groq",
100
+ label: "Groq",
101
+ baseUrl: "https://api.groq.com/openai/v1",
102
+ apiKeyEnv: "GROQ_API_KEY",
103
+ defaultModel: "llama-4-70b-8192",
104
+ models: [
105
+ "llama-4-70b-8192",
106
+ "llama-3.3-70b-versatile",
107
+ "llama-3.1-8b-instant",
108
+ "mixtral-8x7b-32768",
109
+ "gemma2-9b-it",
110
+ "qwen-qwq-32b"
111
+ ],
112
+ compatibility: "strict"
113
+ },
114
+ {
115
+ id: "ollama",
116
+ label: "Ollama (local)",
117
+ baseUrl: "http://localhost:11434/v1",
118
+ apiKeyEnv: "OLLAMA_API_KEY",
119
+ defaultModel: "llama3.2:latest",
120
+ // Ollama ignores the api key but openai-compat clients require a
121
+ // non-empty string. Default to the literal "ollama" per their docs.
122
+ fallbackApiKey: "ollama",
123
+ compatibility: "compatible",
124
+ apiShape: "openai"
125
+ },
126
+ {
127
+ // Same daemon, Anthropic Messages API shape. Ollama v0.14+ serves
128
+ // /v1/messages at the root (no /v1 suffix on the baseUrl because the
129
+ // Anthropic SDK owns the path). Higher tool-calling fidelity for
130
+ // Claude-style agents.
131
+ id: "ollama-anthropic",
132
+ label: "Ollama (local, Anthropic-compat)",
133
+ baseUrl: "http://localhost:11434",
134
+ apiKeyEnv: "OLLAMA_API_KEY",
135
+ defaultModel: "qwen3-coder:30b",
136
+ fallbackApiKey: "ollama",
137
+ apiShape: "anthropic"
138
+ },
139
+ {
140
+ // Ollama Cloud: same endpoint surface as local, hosted on ollama.com.
141
+ // Requires OLLAMA_API_KEY from https://ollama.com/settings/keys.
142
+ id: "ollama-cloud",
143
+ label: "Ollama Cloud",
144
+ baseUrl: "https://ollama.com/v1",
145
+ apiKeyEnv: "OLLAMA_API_KEY",
146
+ defaultModel: "gpt-oss:120b",
147
+ models: [
148
+ "gpt-oss:120b",
149
+ "gpt-oss:20b",
150
+ "kimi-k2.5:cloud",
151
+ "glm-5:cloud",
152
+ "qwen3.5:cloud",
153
+ "deepseek-v3.1:671b",
154
+ "minimax-m2.7:cloud"
155
+ ],
156
+ compatibility: "compatible",
157
+ apiShape: "openai"
158
+ },
159
+ {
160
+ id: "lmstudio",
161
+ label: "LM Studio (local)",
162
+ baseUrl: "http://localhost:1234/v1",
163
+ apiKeyEnv: "",
164
+ defaultModel: "local-model",
165
+ fallbackApiKey: "lm-studio",
166
+ compatibility: "compatible"
167
+ },
168
+ {
169
+ id: "vllm",
170
+ label: "vLLM (local)",
171
+ baseUrl: "http://localhost:8000/v1",
172
+ apiKeyEnv: "",
173
+ defaultModel: "local-vllm",
174
+ fallbackApiKey: "EMPTY",
175
+ compatibility: "strict"
176
+ },
177
+ {
178
+ id: "__custom__",
179
+ label: "Custom (user-supplied)",
180
+ // User MUST supply --base-url. These defaults are placeholders that
181
+ // will fail fast if the user forgets the flag.
182
+ baseUrl: "",
183
+ apiKeyEnv: "",
184
+ defaultModel: "",
185
+ compatibility: "compatible"
186
+ }
187
+ ];
188
+ var BYOK_BY_ID = Object.fromEntries(
189
+ BUILTIN_BYOK_PROVIDERS.map((p) => [p.id, p])
190
+ );
191
+ function findByokProvider(id) {
192
+ return BYOK_BY_ID[id];
193
+ }
194
+ function listByokProviders() {
195
+ return BUILTIN_BYOK_PROVIDERS.filter((p) => p.id !== "__custom__");
196
+ }
197
+ function isByokRef(model) {
198
+ return model.includes(":") && !isOllamaTagOnly(model);
199
+ }
200
+ function isOllamaTagOnly(model) {
201
+ const [head] = model.split(":");
202
+ if (!head) return false;
203
+ return !BYOK_BY_ID[head];
204
+ }
205
+ function parseByokRef(ref) {
206
+ const colon = ref.indexOf(":");
207
+ if (colon < 0) {
208
+ throw new Error(`BYOK ref "${ref}" is missing a colon separator.`);
209
+ }
210
+ const rawProvider = ref.slice(0, colon);
211
+ const model = ref.slice(colon + 1);
212
+ const provider = rawProvider === "custom" ? "__custom__" : rawProvider;
213
+ return { provider, model };
214
+ }
215
+ var ByokMissingApiKeyError = class extends Error {
216
+ constructor(provider) {
217
+ super(
218
+ provider.apiKeyEnv ? `Missing API key for ${provider.label}. Set ${provider.apiKeyEnv} or pass --api-key.` : `Missing API key for ${provider.label}. Pass --api-key.`
219
+ );
220
+ this.provider = provider;
221
+ this.name = "ByokMissingApiKeyError";
222
+ }
223
+ provider;
224
+ };
225
+ var ByokMissingBaseUrlError = class extends Error {
226
+ constructor() {
227
+ super(
228
+ "Custom BYOK provider requires --base-url (and usually --api-key). Example: notch --provider custom --base-url https://my.endpoint/v1 --model my-model"
229
+ );
230
+ this.name = "ByokMissingBaseUrlError";
231
+ }
232
+ };
233
+ function resolveByokModel(spec) {
234
+ const providerInfo = findByokProvider(spec.provider);
235
+ if (!providerInfo) {
236
+ throw new Error(
237
+ `Unknown BYOK provider "${spec.provider}". Available: ${listByokProviders().map((p) => p.id).join(", ")}, custom`
238
+ );
239
+ }
240
+ if (providerInfo.id === "__custom__" && !spec.baseUrl) {
241
+ throw new ByokMissingBaseUrlError();
242
+ }
243
+ const baseUrl = spec.baseUrl ?? providerInfo.baseUrl;
244
+ const modelId = spec.model && spec.model.length > 0 ? spec.model : providerInfo.defaultModel;
245
+ if (!modelId) {
246
+ throw new Error(
247
+ `BYOK provider "${providerInfo.id}" has no default model. Pass --model or set byok.model in .notch.json.`
248
+ );
249
+ }
250
+ let apiKey = spec.apiKey;
251
+ if (!apiKey && providerInfo.apiKeyEnv) {
252
+ apiKey = process.env[providerInfo.apiKeyEnv];
253
+ }
254
+ if (!apiKey) {
255
+ const { loadSyncedByokKeysSync } = (init_auth(), __toCommonJS(auth_exports));
256
+ const synced = loadSyncedByokKeysSync();
257
+ if (synced) {
258
+ const fromSync = synced.keys[providerInfo.id];
259
+ if (typeof fromSync === "string" && fromSync.length > 0) apiKey = fromSync;
260
+ }
261
+ }
262
+ if (!apiKey && providerInfo.fallbackApiKey) {
263
+ apiKey = providerInfo.fallbackApiKey;
264
+ }
265
+ if (!apiKey && !providerInfo.apiKeyEnv) {
266
+ apiKey = "not-needed";
267
+ }
268
+ if (!apiKey) {
269
+ throw new ByokMissingApiKeyError(providerInfo);
270
+ }
271
+ const headers = {
272
+ ...providerInfo.headers ?? {},
273
+ ...spec.headers ?? {}
274
+ };
275
+ const shape = spec.apiShape ?? providerInfo.apiShape ?? "openai";
276
+ if (shape === "anthropic") {
277
+ const anthropic = createAnthropic({
278
+ apiKey,
279
+ baseURL: baseUrl,
280
+ headers
281
+ });
282
+ return {
283
+ model: anthropic(modelId),
284
+ providerInfo,
285
+ modelId
286
+ };
287
+ }
288
+ const provider = createOpenAI({
289
+ apiKey,
290
+ baseURL: baseUrl,
291
+ headers,
292
+ // Anthropic's compat layer and some shims reject unknown fields —
293
+ // `compatibility: 'compatible'` tells @ai-sdk/openai to stick to the
294
+ // lowest-common-denominator feature set.
295
+ compatibility: providerInfo.compatibility ?? "compatible"
296
+ });
297
+ return {
298
+ model: provider(modelId),
299
+ providerInfo,
300
+ modelId
301
+ };
302
+ }
303
+ async function validateByokConfig(spec) {
304
+ const providerInfo = findByokProvider(spec.provider);
305
+ if (!providerInfo) {
306
+ return { ok: false, error: `Unknown BYOK provider "${spec.provider}"` };
307
+ }
308
+ if (providerInfo.id === "__custom__" && !spec.baseUrl) {
309
+ return { ok: false, error: "Custom provider requires --base-url" };
310
+ }
311
+ const baseUrl = spec.baseUrl ?? providerInfo.baseUrl;
312
+ if (!baseUrl) {
313
+ return { ok: false, error: `No base URL for ${providerInfo.label}` };
314
+ }
315
+ const apiKey = spec.apiKey ?? (providerInfo.apiKeyEnv ? process.env[providerInfo.apiKeyEnv] : void 0) ?? providerInfo.fallbackApiKey;
316
+ if (providerInfo.apiKeyEnv && !apiKey) {
317
+ return {
318
+ ok: false,
319
+ error: `${providerInfo.label}: set ${providerInfo.apiKeyEnv} or pass --api-key`
320
+ };
321
+ }
322
+ return { ok: true };
323
+ }
324
+
325
+ // src/providers/registry.ts
326
+ import { createOpenAI as createOpenAI2 } from "@ai-sdk/openai";
327
+ var MissingApiKeyError = class extends Error {
328
+ /** Which flow caused this error (informs the onboarding message). */
329
+ flow;
330
+ /** Env var name the user can set to fix it. */
331
+ envVar;
332
+ /** Human-friendly provider label (e.g. "OpenRouter"). */
333
+ providerLabel;
334
+ constructor(opts) {
335
+ const flow = opts?.flow ?? "notch";
336
+ const envVar = opts?.envVar ?? "NOTCH_API_KEY";
337
+ const providerLabel = opts?.providerLabel ?? "Notch";
338
+ super(`${envVar} is not set (${providerLabel})`);
339
+ this.name = "MissingApiKeyError";
340
+ this.flow = flow;
341
+ this.envVar = envVar;
342
+ this.providerLabel = providerLabel;
343
+ }
344
+ };
345
+ var MODEL_CATALOG = {
346
+ "notch-pyre": {
347
+ id: "notch-pyre",
348
+ label: "Pyre",
349
+ size: "9B",
350
+ gpu: "L40S",
351
+ contextWindow: 131072,
352
+ maxOutputTokens: 16384,
353
+ baseUrl: "https://acemagnifique--notch-serve-pyre-notchpyreserver-serve.modal.run/v1",
354
+ hardware: {
355
+ vramGb: 10,
356
+ ramGb: 16,
357
+ diskGb: 6,
358
+ recommendedGpu: "RTX 3090 / 4070 Ti+ \xB7 12GB+ VRAM",
359
+ tier: "light"
360
+ },
361
+ // null until we push Q4 merged weights to the public Hub. The CLI
362
+ // surfaces this as "hosted-only" in the /model picker so users know
363
+ // local download isn't wired yet — no fake error, no mock repo.
364
+ hfRepo: null
365
+ },
366
+ "notch-ignis": {
367
+ id: "notch-ignis",
368
+ label: "Ignis",
369
+ size: "27B",
370
+ gpu: "A100-80GB",
371
+ contextWindow: 131072,
372
+ maxOutputTokens: 16384,
373
+ baseUrl: "https://acemagnifique--notch-serve-ignis-notchignisserver-serve.modal.run/v1",
374
+ hardware: {
375
+ vramGb: 20,
376
+ ramGb: 32,
377
+ diskGb: 18,
378
+ recommendedGpu: "RTX 4090 / A6000+ \xB7 24GB+ VRAM",
379
+ tier: "standard"
380
+ },
381
+ hfRepo: null
382
+ },
383
+ "notch-solace": {
384
+ id: "notch-solace",
385
+ label: "Solace",
386
+ size: "31B",
387
+ gpu: "A100-80GB",
388
+ contextWindow: 131072,
389
+ maxOutputTokens: 16384,
390
+ baseUrl: "https://acemagnifique--notch-serve-solace-notchsolaceserver-serve.modal.run/v1",
391
+ hardware: {
392
+ vramGb: 24,
393
+ ramGb: 64,
394
+ diskGb: 22,
395
+ recommendedGpu: "RTX 4090 + offload / A100-40GB+",
396
+ tier: "heavy"
397
+ },
398
+ hfRepo: null
399
+ },
400
+ "notch-solace-lite": {
401
+ id: "notch-solace-lite",
402
+ label: "Solace Lite",
403
+ size: "E4B",
404
+ gpu: "L4",
405
+ contextWindow: 65536,
406
+ maxOutputTokens: 8192,
407
+ baseUrl: "https://acemagnifique--notch-serve-solace-lite-notchsolacelitese-0e4da6.modal.run/v1",
408
+ hardware: {
409
+ vramGb: 6,
410
+ ramGb: 8,
411
+ diskGb: 3,
412
+ recommendedGpu: "RTX 3060 / M2 Pro+ \xB7 8GB+ VRAM",
413
+ tier: "light"
414
+ },
415
+ hfRepo: null
416
+ }
417
+ };
418
+ var MODEL_IDS = Object.keys(MODEL_CATALOG);
419
+ function isValidModel(id) {
420
+ return id in MODEL_CATALOG;
421
+ }
422
+ function modelSupportsImages(modelId) {
423
+ if (!modelId) return false;
424
+ if (modelId in MODEL_CATALOG) return true;
425
+ if (modelId.startsWith("notch-")) return true;
426
+ const prefix = modelId.split(":", 1)[0]?.toLowerCase() ?? "";
427
+ switch (prefix) {
428
+ case "openai":
429
+ case "anthropic":
430
+ case "google":
431
+ case "together":
432
+ case "openrouter":
433
+ return true;
434
+ case "groq":
435
+ return false;
436
+ default:
437
+ return false;
438
+ }
439
+ }
440
+ function resolveModel(config) {
441
+ if (config.byokProvider) {
442
+ const resolved = resolveByokModel({
443
+ provider: config.byokProvider,
444
+ model: typeof config.model === "string" && config.model.length > 0 ? config.model : void 0,
445
+ apiKey: config.apiKey,
446
+ baseUrl: config.baseUrl,
447
+ headers: { ...config.headers, ...config.byokHeaders },
448
+ apiShape: config.byokApiShape
449
+ });
450
+ return resolved.model;
451
+ }
452
+ if (typeof config.model === "string" && isByokRef(config.model)) {
453
+ const { provider: provider2, model } = parseByokRef(config.model);
454
+ const resolved = resolveByokModel({
455
+ provider: provider2,
456
+ model,
457
+ apiKey: config.apiKey,
458
+ baseUrl: config.baseUrl,
459
+ headers: { ...config.headers, ...config.byokHeaders },
460
+ apiShape: config.byokApiShape
461
+ });
462
+ return resolved.model;
463
+ }
464
+ const info = MODEL_CATALOG[config.model];
465
+ if (!info) {
466
+ throw new Error(
467
+ `Unknown model "${config.model}". Notch models: ${MODEL_IDS.join(", ")}. For BYOK use "<provider>:<model>" (e.g. openrouter:anthropic/claude-sonnet-4-6).`
468
+ );
469
+ }
470
+ const baseUrl = config.baseUrl ?? process.env.NOTCH_BASE_URL ?? info.baseUrl;
471
+ const apiKey = config.apiKey ?? process.env.NOTCH_API_KEY;
472
+ if (!apiKey) {
473
+ throw new MissingApiKeyError({ flow: "notch", envVar: "NOTCH_API_KEY", providerLabel: "Notch" });
474
+ }
475
+ const proxyKey = process.env.MODAL_PROXY_KEY;
476
+ const proxySecret = process.env.MODAL_PROXY_SECRET;
477
+ const modalProxyHeaders = proxyKey && proxySecret ? { "Modal-Key": proxyKey, "Modal-Secret": proxySecret } : {};
478
+ const provider = createOpenAI2({
479
+ apiKey,
480
+ baseURL: baseUrl,
481
+ headers: { ...modalProxyHeaders, ...config.headers }
482
+ });
483
+ return provider(config.model);
484
+ }
485
+ async function validateConfig(config) {
486
+ if (config.byokProvider) {
487
+ return validateByokConfig({
488
+ provider: config.byokProvider,
489
+ model: typeof config.model === "string" && config.model.length > 0 ? config.model : void 0,
490
+ apiKey: config.apiKey,
491
+ baseUrl: config.baseUrl,
492
+ headers: { ...config.headers, ...config.byokHeaders },
493
+ apiShape: config.byokApiShape
494
+ });
495
+ }
496
+ if (typeof config.model === "string" && isByokRef(config.model)) {
497
+ const { provider, model } = parseByokRef(config.model);
498
+ return validateByokConfig({
499
+ provider,
500
+ model,
501
+ apiKey: config.apiKey,
502
+ baseUrl: config.baseUrl,
503
+ headers: { ...config.headers, ...config.byokHeaders },
504
+ apiShape: config.byokApiShape
505
+ });
506
+ }
507
+ const info = MODEL_CATALOG[config.model];
508
+ if (!info) {
509
+ return { ok: false, error: `Unknown model "${config.model}". Available: ${MODEL_IDS.join(", ")}` };
510
+ }
511
+ const baseUrl = config.baseUrl ?? process.env.NOTCH_BASE_URL ?? info.baseUrl;
512
+ const proxyKey = process.env.MODAL_PROXY_KEY;
513
+ const proxySecret = process.env.MODAL_PROXY_SECRET;
514
+ const proxyHeaders = proxyKey && proxySecret ? { "Modal-Key": proxyKey, "Modal-Secret": proxySecret } : {};
515
+ try {
516
+ const res = await fetch(`${baseUrl.replace(/\/v1$/, "")}/health`, {
517
+ signal: AbortSignal.timeout(5e3),
518
+ headers: proxyHeaders
519
+ });
520
+ if (!res.ok) {
521
+ return { ok: false, error: `Notch ${info.label} returned ${res.status} at ${baseUrl}` };
522
+ }
523
+ return { ok: true };
524
+ } catch {
525
+ return { ok: false, error: `Cannot reach Notch ${info.label} at ${baseUrl}` };
526
+ }
527
+ }
528
+
529
+ export {
530
+ findByokProvider,
531
+ listByokProviders,
532
+ isByokRef,
533
+ parseByokRef,
534
+ ByokMissingApiKeyError,
535
+ ByokMissingBaseUrlError,
536
+ resolveByokModel,
537
+ MissingApiKeyError,
538
+ MODEL_CATALOG,
539
+ MODEL_IDS,
540
+ isValidModel,
541
+ modelSupportsImages,
542
+ resolveModel,
543
+ validateConfig
544
+ };