@freesyntax/notch-cli 0.5.21 → 0.5.23
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/{apply-patch-D5PDUXUC.js → apply-patch-U6K67CMT.js} +1 -0
- package/dist/auth-UAMMP5IJ.js +29 -0
- package/dist/{chunk-TU465P2P.js → chunk-474TAHDN.js} +50 -8
- package/dist/{chunk-OSWUX6TC.js → chunk-4HPRBCSY.js} +1 -1
- package/dist/{chunk-QKM27RHS.js → chunk-6NKRMZTX.js} +1 -1
- package/dist/{chunk-443G6HCC.js → chunk-JVFOAPYV.js} +331 -79
- package/dist/chunk-KCAR5DOB.js +52 -0
- package/dist/chunk-KFQGP6VL.js +33 -0
- package/dist/chunk-O6AKZ4OH.js +0 -0
- package/dist/{chunk-FIFC4V2R.js → chunk-PPEBWOMJ.js} +91 -7
- package/dist/chunk-UHK6SI4H.js +206 -0
- package/dist/{chunk-MMBFNIKE.js → chunk-YNYVQ7ZI.js} +10 -8
- package/dist/{compression-SQAIQ2UU.js → compression-YJLWEHCC.js} +1 -0
- package/dist/config-set-5F4VK7IT.js +111 -0
- package/dist/{edit-JEFEK43H.js → edit-6QYAXVNU.js} +1 -0
- package/dist/{git-5T5TSQTX.js → git-DNQ5EELH.js} +1 -0
- package/dist/{github-DWRGWX6U.js → github-34T4QQIH.js} +1 -0
- package/dist/{glob-BI3P4C7Q.js → glob-XT43LEJ4.js} +1 -0
- package/dist/{grep-VZ3I5GNW.js → grep-T2CXYNRI.js} +1 -0
- package/dist/index.js +878 -428
- package/dist/{lsp-UPY6I3L7.js → lsp-JXQVU7NP.js} +1 -0
- package/dist/model-download-KCQJCEPW.js +176 -0
- package/dist/{notebook-FXJBTSPA.js → notebook-MFODW345.js} +1 -0
- package/dist/{ollama-bench-QQHBIG2D.js → ollama-bench-JLC5POG3.js} +8 -4
- package/dist/{ollama-launch-2ASVER3S.js → ollama-launch-3IKB2A3Z.js} +6 -2
- package/dist/{ollama-usage-2WPCZJJI.js → ollama-usage-3PROM2WC.js} +1 -0
- package/dist/{plugins-OG2P75K5.js → plugins-PNGRZLFW.js} +1 -0
- package/dist/{read-OVJG2XKW.js → read-B64XE7N3.js} +1 -0
- package/dist/server-GMF4WV67.js +187 -0
- package/dist/{session-index-SSGOOZXK.js → session-index-7FWEVP6E.js} +3 -2
- package/dist/{shell-4X545EVN.js → shell-BOZTHQUT.js} +1 -0
- package/dist/{task-OS3E5F3X.js → task-67G4KLYC.js} +1 -0
- package/dist/{tools-7WAWS6V4.js → tools-ABRZPCEJ.js} +6 -3
- package/dist/{web-fetch-KNIV3Z3W.js → web-fetch-OTNDICGJ.js} +1 -0
- package/dist/{write-NNHLOTYK.js → write-ZOSB7I4J.js} +1 -0
- package/package.json +60 -57
- package/dist/auth-JQX6MHJG.js +0 -16
- package/dist/server-7UQKCB2Z.js +0 -1477
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
auth_exports,
|
|
3
|
+
init_auth
|
|
4
|
+
} from "./chunk-PPEBWOMJ.js";
|
|
5
|
+
import {
|
|
6
|
+
__toCommonJS
|
|
7
|
+
} from "./chunk-KFQGP6VL.js";
|
|
8
|
+
|
|
1
9
|
// src/providers/byok.ts
|
|
2
10
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
3
11
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
@@ -7,16 +15,17 @@ var BUILTIN_BYOK_PROVIDERS = [
|
|
|
7
15
|
label: "OpenAI",
|
|
8
16
|
baseUrl: "https://api.openai.com/v1",
|
|
9
17
|
apiKeyEnv: "OPENAI_API_KEY",
|
|
10
|
-
defaultModel: "gpt-
|
|
18
|
+
defaultModel: "gpt-5",
|
|
11
19
|
models: [
|
|
12
|
-
"gpt-4o",
|
|
13
|
-
"gpt-4o-mini",
|
|
14
|
-
"gpt-4-turbo",
|
|
15
|
-
"gpt-4.1",
|
|
16
|
-
"gpt-4.1-mini",
|
|
17
20
|
"gpt-5",
|
|
18
21
|
"gpt-5-mini",
|
|
19
|
-
"
|
|
22
|
+
"gpt-5-nano",
|
|
23
|
+
"gpt-5-chat-latest",
|
|
24
|
+
"gpt-4.1",
|
|
25
|
+
"gpt-4.1-mini",
|
|
26
|
+
"gpt-4o",
|
|
27
|
+
"gpt-4o-mini",
|
|
28
|
+
"o3",
|
|
20
29
|
"o4-mini"
|
|
21
30
|
],
|
|
22
31
|
compatibility: "strict"
|
|
@@ -55,6 +64,33 @@ var BUILTIN_BYOK_PROVIDERS = [
|
|
|
55
64
|
"X-Title": "Notch CLI"
|
|
56
65
|
}
|
|
57
66
|
},
|
|
67
|
+
{
|
|
68
|
+
id: "google",
|
|
69
|
+
label: "Google Gemini",
|
|
70
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
71
|
+
apiKeyEnv: "GEMINI_API_KEY",
|
|
72
|
+
defaultModel: "gemini-3.5-flash",
|
|
73
|
+
models: [
|
|
74
|
+
"gemini-3.5-flash",
|
|
75
|
+
"gemini-2.5-pro",
|
|
76
|
+
"gemini-2.5-flash"
|
|
77
|
+
],
|
|
78
|
+
compatibility: "compatible"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "deepseek",
|
|
82
|
+
label: "DeepSeek",
|
|
83
|
+
baseUrl: "https://api.deepseek.com",
|
|
84
|
+
apiKeyEnv: "DEEPSEEK_API_KEY",
|
|
85
|
+
defaultModel: "deepseek-v4-flash",
|
|
86
|
+
models: [
|
|
87
|
+
"deepseek-v4-flash",
|
|
88
|
+
"deepseek-v4-pro",
|
|
89
|
+
"deepseek-chat",
|
|
90
|
+
"deepseek-reasoner"
|
|
91
|
+
],
|
|
92
|
+
compatibility: "strict"
|
|
93
|
+
},
|
|
58
94
|
{
|
|
59
95
|
id: "together",
|
|
60
96
|
label: "Together AI",
|
|
@@ -186,23 +222,35 @@ function findByokProvider(id) {
|
|
|
186
222
|
function listByokProviders() {
|
|
187
223
|
return BUILTIN_BYOK_PROVIDERS.filter((p) => p.id !== "__custom__");
|
|
188
224
|
}
|
|
189
|
-
function
|
|
190
|
-
return
|
|
225
|
+
function normalizeProviderId(id) {
|
|
226
|
+
return id === "custom" ? "__custom__" : id;
|
|
191
227
|
}
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
228
|
+
function providerExists(id) {
|
|
229
|
+
return Boolean(BYOK_BY_ID[normalizeProviderId(id)]);
|
|
230
|
+
}
|
|
231
|
+
function isByokRef(model) {
|
|
232
|
+
const slash = model.indexOf("/");
|
|
233
|
+
if (slash > 0 && providerExists(model.slice(0, slash))) return true;
|
|
234
|
+
const colon = model.indexOf(":");
|
|
235
|
+
if (colon > 0 && providerExists(model.slice(0, colon))) return true;
|
|
236
|
+
return false;
|
|
196
237
|
}
|
|
197
238
|
function parseByokRef(ref) {
|
|
239
|
+
const slash = ref.indexOf("/");
|
|
198
240
|
const colon = ref.indexOf(":");
|
|
199
|
-
if (colon < 0) {
|
|
200
|
-
|
|
241
|
+
if (slash > 0 && (colon < 0 || slash < colon) && providerExists(ref.slice(0, slash))) {
|
|
242
|
+
const rawProvider = ref.slice(0, slash);
|
|
243
|
+
const model = ref.slice(slash + 1);
|
|
244
|
+
return { provider: normalizeProviderId(rawProvider), model };
|
|
245
|
+
}
|
|
246
|
+
if (colon > 0 && providerExists(ref.slice(0, colon))) {
|
|
247
|
+
const rawProvider = ref.slice(0, colon);
|
|
248
|
+
const model = ref.slice(colon + 1);
|
|
249
|
+
return { provider: normalizeProviderId(rawProvider), model };
|
|
201
250
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return { provider, model };
|
|
251
|
+
throw new Error(
|
|
252
|
+
`Provider model ref "${ref}" must be "<provider>/<model>" or "<provider>:<model>".`
|
|
253
|
+
);
|
|
206
254
|
}
|
|
207
255
|
var ByokMissingApiKeyError = class extends Error {
|
|
208
256
|
constructor(provider) {
|
|
@@ -217,7 +265,7 @@ var ByokMissingApiKeyError = class extends Error {
|
|
|
217
265
|
var ByokMissingBaseUrlError = class extends Error {
|
|
218
266
|
constructor() {
|
|
219
267
|
super(
|
|
220
|
-
"Custom
|
|
268
|
+
"Custom provider requires --base-url (and usually --api-key). Example: notch --provider custom --base-url https://my.endpoint/v1 --model my-model"
|
|
221
269
|
);
|
|
222
270
|
this.name = "ByokMissingBaseUrlError";
|
|
223
271
|
}
|
|
@@ -226,7 +274,7 @@ function resolveByokModel(spec) {
|
|
|
226
274
|
const providerInfo = findByokProvider(spec.provider);
|
|
227
275
|
if (!providerInfo) {
|
|
228
276
|
throw new Error(
|
|
229
|
-
`Unknown
|
|
277
|
+
`Unknown provider "${spec.provider}". Available: ${listByokProviders().map((p) => p.id).join(", ")}, custom`
|
|
230
278
|
);
|
|
231
279
|
}
|
|
232
280
|
if (providerInfo.id === "__custom__" && !spec.baseUrl) {
|
|
@@ -236,13 +284,21 @@ function resolveByokModel(spec) {
|
|
|
236
284
|
const modelId = spec.model && spec.model.length > 0 ? spec.model : providerInfo.defaultModel;
|
|
237
285
|
if (!modelId) {
|
|
238
286
|
throw new Error(
|
|
239
|
-
`
|
|
287
|
+
`Provider "${providerInfo.id}" has no default model. Pass --model or set byok.model in .notch.json.`
|
|
240
288
|
);
|
|
241
289
|
}
|
|
242
290
|
let apiKey = spec.apiKey;
|
|
243
291
|
if (!apiKey && providerInfo.apiKeyEnv) {
|
|
244
292
|
apiKey = process.env[providerInfo.apiKeyEnv];
|
|
245
293
|
}
|
|
294
|
+
if (!apiKey) {
|
|
295
|
+
const { loadSyncedByokKeysSync } = (init_auth(), __toCommonJS(auth_exports));
|
|
296
|
+
const synced = loadSyncedByokKeysSync();
|
|
297
|
+
if (synced) {
|
|
298
|
+
const fromSync = synced.keys[providerInfo.id];
|
|
299
|
+
if (typeof fromSync === "string" && fromSync.length > 0) apiKey = fromSync;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
246
302
|
if (!apiKey && providerInfo.fallbackApiKey) {
|
|
247
303
|
apiKey = providerInfo.fallbackApiKey;
|
|
248
304
|
}
|
|
@@ -287,7 +343,7 @@ function resolveByokModel(spec) {
|
|
|
287
343
|
async function validateByokConfig(spec) {
|
|
288
344
|
const providerInfo = findByokProvider(spec.provider);
|
|
289
345
|
if (!providerInfo) {
|
|
290
|
-
return { ok: false, error: `Unknown
|
|
346
|
+
return { ok: false, error: `Unknown provider "${spec.provider}"` };
|
|
291
347
|
}
|
|
292
348
|
if (providerInfo.id === "__custom__" && !spec.baseUrl) {
|
|
293
349
|
return { ok: false, error: "Custom provider requires --base-url" };
|
|
@@ -308,6 +364,156 @@ async function validateByokConfig(spec) {
|
|
|
308
364
|
|
|
309
365
|
// src/providers/registry.ts
|
|
310
366
|
import { createOpenAI as createOpenAI2 } from "@ai-sdk/openai";
|
|
367
|
+
|
|
368
|
+
// src/providers/pi-ai-adapter.ts
|
|
369
|
+
var PI_AI_PROVIDER_MAP = {
|
|
370
|
+
openai: "openai",
|
|
371
|
+
anthropic: "anthropic",
|
|
372
|
+
google: "google",
|
|
373
|
+
"google-vertex": "google-vertex",
|
|
374
|
+
mistral: "mistral",
|
|
375
|
+
bedrock: "amazon-bedrock",
|
|
376
|
+
azure: "azure-openai"
|
|
377
|
+
};
|
|
378
|
+
var UNSUPPORTED_BY_PI_AI = /* @__PURE__ */ new Set(["openrouter", "groq", "together"]);
|
|
379
|
+
function piAiMode() {
|
|
380
|
+
const enabled = process.env.NOTCH_USE_PI_AI;
|
|
381
|
+
if (!enabled || enabled === "0" || enabled.toLowerCase() === "false") return "off";
|
|
382
|
+
const mode = (process.env.NOTCH_PI_AI_MODE ?? "shadow").toLowerCase();
|
|
383
|
+
return mode === "swap" ? "swap" : "shadow";
|
|
384
|
+
}
|
|
385
|
+
function shouldRouteViaPiAi(config) {
|
|
386
|
+
if (piAiMode() === "off") return false;
|
|
387
|
+
const provider = extractProvider(config);
|
|
388
|
+
if (!provider) return false;
|
|
389
|
+
if (UNSUPPORTED_BY_PI_AI.has(provider)) return false;
|
|
390
|
+
return provider in PI_AI_PROVIDER_MAP;
|
|
391
|
+
}
|
|
392
|
+
var PiAiNotInstalledError = class extends Error {
|
|
393
|
+
constructor(originalError) {
|
|
394
|
+
super(
|
|
395
|
+
`NOTCH_USE_PI_AI is set but @mariozechner/pi-ai could not be loaded. Install with: pnpm --filter @freesyntax/notch-cli add @mariozechner/pi-ai@0.70.2. Original error: ${originalError instanceof Error ? originalError.message : String(originalError)}`
|
|
396
|
+
);
|
|
397
|
+
this.name = "PiAiNotInstalledError";
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
async function resolveModelViaPiAi(config) {
|
|
401
|
+
const { providerName, modelId } = extractProviderAndModel(config);
|
|
402
|
+
const piAiProvider = PI_AI_PROVIDER_MAP[providerName];
|
|
403
|
+
if (!piAiProvider) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
`pi-ai does not support BYOK provider "${providerName}". Supported: ${Object.keys(PI_AI_PROVIDER_MAP).join(", ")}. Set NOTCH_USE_PI_AI=0 or use a different provider.`
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
if (!modelId) {
|
|
409
|
+
throw new Error(
|
|
410
|
+
`pi-ai resolution requires a non-empty model id. Config: ${JSON.stringify({ byokProvider: config.byokProvider, model: config.model })}`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
const piAi = await loadPiAi();
|
|
414
|
+
const model = piAi.getModel(piAiProvider, modelId);
|
|
415
|
+
if (!model) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
`pi-ai has no registered model "${piAiProvider}/${modelId}". pi-ai only lists tool-calling-capable models; if your model is text-only, keep it on the AI SDK path (UNSUPPORTED_BY_PI_AI) or clear NOTCH_USE_PI_AI.`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
return { model, piAiProvider, modelId };
|
|
421
|
+
}
|
|
422
|
+
async function computePiAiCostUsd(config, usage) {
|
|
423
|
+
try {
|
|
424
|
+
const handle = await resolveModelViaPiAi(config);
|
|
425
|
+
const piAi = await loadPiAi();
|
|
426
|
+
const usageBucket = {
|
|
427
|
+
input: usage.input,
|
|
428
|
+
output: usage.output,
|
|
429
|
+
cacheRead: usage.cacheRead ?? 0,
|
|
430
|
+
cacheWrite: usage.cacheWrite ?? 0,
|
|
431
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
|
|
432
|
+
};
|
|
433
|
+
piAi.calculateCost(handle.model, usageBucket);
|
|
434
|
+
return usageBucket.cost.total;
|
|
435
|
+
} catch {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
var shadowContexts = /* @__PURE__ */ new WeakMap();
|
|
440
|
+
function registerShadowContext(model, context) {
|
|
441
|
+
if (piAiMode() === "off") return;
|
|
442
|
+
shadowContexts.set(model, context);
|
|
443
|
+
}
|
|
444
|
+
async function recordShadowUsage(model, usage, opts) {
|
|
445
|
+
if (piAiMode() === "off") return;
|
|
446
|
+
const ctx = shadowContexts.get(model);
|
|
447
|
+
if (!ctx) return;
|
|
448
|
+
const inputTokens = usage.promptTokens ?? 0;
|
|
449
|
+
const outputTokens = usage.completionTokens ?? 0;
|
|
450
|
+
const piAiCostUsd = await computePiAiCostUsd(ctx.config, {
|
|
451
|
+
input: inputTokens,
|
|
452
|
+
output: outputTokens
|
|
453
|
+
});
|
|
454
|
+
await recordShadowCost({
|
|
455
|
+
requestId: opts?.requestId ?? `usage-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
|
|
456
|
+
provider: ctx.provider,
|
|
457
|
+
modelId: ctx.modelId,
|
|
458
|
+
inputTokens,
|
|
459
|
+
outputTokens,
|
|
460
|
+
piAiCostUsd,
|
|
461
|
+
stripeChargeUsd: opts?.stripeChargeUsd ?? null,
|
|
462
|
+
timestamp: Date.now()
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
async function recordShadowCost(event) {
|
|
466
|
+
if (piAiMode() === "off") return;
|
|
467
|
+
try {
|
|
468
|
+
const { homedir } = await import("os");
|
|
469
|
+
const { mkdir, appendFile } = await import("fs/promises");
|
|
470
|
+
const { join } = await import("path");
|
|
471
|
+
const dir = join(homedir(), ".notch");
|
|
472
|
+
await mkdir(dir, { recursive: true });
|
|
473
|
+
await appendFile(join(dir, "pi-ai-shadow.jsonl"), `${JSON.stringify(event)}
|
|
474
|
+
`, "utf8");
|
|
475
|
+
} catch {
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
var piAiModulePromise = null;
|
|
479
|
+
async function loadPiAi() {
|
|
480
|
+
if (!piAiModulePromise) {
|
|
481
|
+
piAiModulePromise = (async () => {
|
|
482
|
+
try {
|
|
483
|
+
const mod = await import("@mariozechner/pi-ai");
|
|
484
|
+
return mod;
|
|
485
|
+
} catch (err) {
|
|
486
|
+
piAiModulePromise = null;
|
|
487
|
+
throw new PiAiNotInstalledError(err);
|
|
488
|
+
}
|
|
489
|
+
})();
|
|
490
|
+
}
|
|
491
|
+
return piAiModulePromise;
|
|
492
|
+
}
|
|
493
|
+
function extractProvider(config) {
|
|
494
|
+
if (config.byokProvider) return config.byokProvider;
|
|
495
|
+
if (typeof config.model === "string" && isByokRef(config.model)) {
|
|
496
|
+
return parseByokRef(config.model).provider;
|
|
497
|
+
}
|
|
498
|
+
return void 0;
|
|
499
|
+
}
|
|
500
|
+
function extractProviderAndModel(config) {
|
|
501
|
+
if (config.byokProvider) {
|
|
502
|
+
return {
|
|
503
|
+
providerName: config.byokProvider,
|
|
504
|
+
modelId: typeof config.model === "string" ? config.model : ""
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
if (typeof config.model === "string" && isByokRef(config.model)) {
|
|
508
|
+
const { provider, model } = parseByokRef(config.model);
|
|
509
|
+
return { providerName: provider, modelId: model };
|
|
510
|
+
}
|
|
511
|
+
throw new Error(
|
|
512
|
+
"resolveModelViaPiAi called on a non-BYOK config. Notch-hosted Modal endpoints still use the AI SDK path."
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// src/providers/registry.ts
|
|
311
517
|
var MissingApiKeyError = class extends Error {
|
|
312
518
|
/** Which flow caused this error (informs the onboarding message). */
|
|
313
519
|
flow;
|
|
@@ -334,7 +540,18 @@ var MODEL_CATALOG = {
|
|
|
334
540
|
gpu: "L40S",
|
|
335
541
|
contextWindow: 131072,
|
|
336
542
|
maxOutputTokens: 16384,
|
|
337
|
-
baseUrl: "https://acemagnifique--notch-serve-pyre-notchpyreserver-serve.modal.run/v1"
|
|
543
|
+
baseUrl: "https://acemagnifique--notch-serve-pyre-notchpyreserver-serve.modal.run/v1",
|
|
544
|
+
hardware: {
|
|
545
|
+
vramGb: 10,
|
|
546
|
+
ramGb: 16,
|
|
547
|
+
diskGb: 6,
|
|
548
|
+
recommendedGpu: "RTX 3090 / 4070 Ti+ \xB7 12GB+ VRAM",
|
|
549
|
+
tier: "light"
|
|
550
|
+
},
|
|
551
|
+
// null until we push Q4 merged weights to the public Hub. The CLI
|
|
552
|
+
// surfaces this as "hosted-only" in the /model picker so users know
|
|
553
|
+
// local download isn't wired yet — no fake error, no mock repo.
|
|
554
|
+
hfRepo: null
|
|
338
555
|
},
|
|
339
556
|
"notch-ignis": {
|
|
340
557
|
id: "notch-ignis",
|
|
@@ -343,7 +560,15 @@ var MODEL_CATALOG = {
|
|
|
343
560
|
gpu: "A100-80GB",
|
|
344
561
|
contextWindow: 131072,
|
|
345
562
|
maxOutputTokens: 16384,
|
|
346
|
-
baseUrl: "https://acemagnifique--notch-serve-ignis-notchignisserver-serve.modal.run/v1"
|
|
563
|
+
baseUrl: "https://acemagnifique--notch-serve-ignis-notchignisserver-serve.modal.run/v1",
|
|
564
|
+
hardware: {
|
|
565
|
+
vramGb: 20,
|
|
566
|
+
ramGb: 32,
|
|
567
|
+
diskGb: 18,
|
|
568
|
+
recommendedGpu: "RTX 4090 / A6000+ \xB7 24GB+ VRAM",
|
|
569
|
+
tier: "standard"
|
|
570
|
+
},
|
|
571
|
+
hfRepo: null
|
|
347
572
|
},
|
|
348
573
|
"notch-solace": {
|
|
349
574
|
id: "notch-solace",
|
|
@@ -352,7 +577,15 @@ var MODEL_CATALOG = {
|
|
|
352
577
|
gpu: "A100-80GB",
|
|
353
578
|
contextWindow: 131072,
|
|
354
579
|
maxOutputTokens: 16384,
|
|
355
|
-
baseUrl: "https://acemagnifique--notch-serve-solace-notchsolaceserver-serve.modal.run/v1"
|
|
580
|
+
baseUrl: "https://acemagnifique--notch-serve-solace-notchsolaceserver-serve.modal.run/v1",
|
|
581
|
+
hardware: {
|
|
582
|
+
vramGb: 24,
|
|
583
|
+
ramGb: 64,
|
|
584
|
+
diskGb: 22,
|
|
585
|
+
recommendedGpu: "RTX 4090 + offload / A100-40GB+",
|
|
586
|
+
tier: "heavy"
|
|
587
|
+
},
|
|
588
|
+
hfRepo: null
|
|
356
589
|
},
|
|
357
590
|
"notch-solace-lite": {
|
|
358
591
|
id: "notch-solace-lite",
|
|
@@ -361,7 +594,15 @@ var MODEL_CATALOG = {
|
|
|
361
594
|
gpu: "L4",
|
|
362
595
|
contextWindow: 65536,
|
|
363
596
|
maxOutputTokens: 8192,
|
|
364
|
-
baseUrl: "https://acemagnifique--notch-serve-solace-lite-notchsolacelitese-0e4da6.modal.run/v1"
|
|
597
|
+
baseUrl: "https://acemagnifique--notch-serve-solace-lite-notchsolacelitese-0e4da6.modal.run/v1",
|
|
598
|
+
hardware: {
|
|
599
|
+
vramGb: 6,
|
|
600
|
+
ramGb: 8,
|
|
601
|
+
diskGb: 3,
|
|
602
|
+
recommendedGpu: "RTX 3060 / M2 Pro+ \xB7 8GB+ VRAM",
|
|
603
|
+
tier: "light"
|
|
604
|
+
},
|
|
605
|
+
hfRepo: null
|
|
365
606
|
}
|
|
366
607
|
};
|
|
367
608
|
var MODEL_IDS = Object.keys(MODEL_CATALOG);
|
|
@@ -370,22 +611,40 @@ function isValidModel(id) {
|
|
|
370
611
|
}
|
|
371
612
|
function modelSupportsImages(modelId) {
|
|
372
613
|
if (!modelId) return false;
|
|
614
|
+
const lowerModelId = modelId.toLowerCase();
|
|
373
615
|
if (modelId in MODEL_CATALOG) return true;
|
|
374
|
-
if (
|
|
375
|
-
const
|
|
616
|
+
if (lowerModelId.startsWith("notch-")) return true;
|
|
617
|
+
const ref = isByokRef(lowerModelId) ? parseByokRef(lowerModelId) : null;
|
|
618
|
+
const prefix = ref?.provider ?? "";
|
|
376
619
|
switch (prefix) {
|
|
377
620
|
case "openai":
|
|
378
621
|
case "anthropic":
|
|
379
622
|
case "google":
|
|
380
623
|
case "together":
|
|
381
|
-
case "openrouter":
|
|
382
624
|
return true;
|
|
625
|
+
case "openrouter":
|
|
626
|
+
return openRouterModelSupportsImages(ref?.model ?? "");
|
|
383
627
|
case "groq":
|
|
384
628
|
return false;
|
|
385
629
|
default:
|
|
386
630
|
return false;
|
|
387
631
|
}
|
|
388
632
|
}
|
|
633
|
+
function openRouterModelSupportsImages(upstreamModelId) {
|
|
634
|
+
return [
|
|
635
|
+
"openai/gpt-5.3-codex",
|
|
636
|
+
"openai/gpt-5.2-codex",
|
|
637
|
+
"openai/gpt-5.1-codex",
|
|
638
|
+
"openai/gpt-5-codex",
|
|
639
|
+
"anthropic/claude-sonnet-4.6",
|
|
640
|
+
"anthropic/claude-sonnet-4.5",
|
|
641
|
+
"anthropic/claude-sonnet-4",
|
|
642
|
+
"moonshotai/kimi-k2.6",
|
|
643
|
+
"moonshotai/kimi-k2.5",
|
|
644
|
+
"~anthropic/claude-sonnet-latest",
|
|
645
|
+
"~moonshotai/kimi-latest"
|
|
646
|
+
].some((id) => upstreamModelId === id || upstreamModelId.startsWith(`${id}:`));
|
|
647
|
+
}
|
|
389
648
|
function resolveModel(config) {
|
|
390
649
|
if (config.byokProvider) {
|
|
391
650
|
const resolved = resolveByokModel({
|
|
@@ -396,6 +655,7 @@ function resolveModel(config) {
|
|
|
396
655
|
headers: { ...config.headers, ...config.byokHeaders },
|
|
397
656
|
apiShape: config.byokApiShape
|
|
398
657
|
});
|
|
658
|
+
firePiAiShadowProbe(config, resolved.model);
|
|
399
659
|
return resolved.model;
|
|
400
660
|
}
|
|
401
661
|
if (typeof config.model === "string" && isByokRef(config.model)) {
|
|
@@ -408,12 +668,13 @@ function resolveModel(config) {
|
|
|
408
668
|
headers: { ...config.headers, ...config.byokHeaders },
|
|
409
669
|
apiShape: config.byokApiShape
|
|
410
670
|
});
|
|
671
|
+
firePiAiShadowProbe(config, resolved.model);
|
|
411
672
|
return resolved.model;
|
|
412
673
|
}
|
|
413
674
|
const info = MODEL_CATALOG[config.model];
|
|
414
675
|
if (!info) {
|
|
415
676
|
throw new Error(
|
|
416
|
-
`Unknown model "${config.model}".
|
|
677
|
+
`Unknown model "${config.model}". Use provider/model, for example openrouter/anthropic/claude-sonnet-4-6.`
|
|
417
678
|
);
|
|
418
679
|
}
|
|
419
680
|
const baseUrl = config.baseUrl ?? process.env.NOTCH_BASE_URL ?? info.baseUrl;
|
|
@@ -474,51 +735,46 @@ async function validateConfig(config) {
|
|
|
474
735
|
return { ok: false, error: `Cannot reach Notch ${info.label} at ${baseUrl}` };
|
|
475
736
|
}
|
|
476
737
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
throw new Error("Cannot determine home directory for credential storage.");
|
|
489
|
-
}
|
|
490
|
-
return path.join(home, ".config", "notch");
|
|
491
|
-
}
|
|
492
|
-
function ollamaCredsPath() {
|
|
493
|
-
return path.join(notchConfigDir(), "ollama.json");
|
|
494
|
-
}
|
|
495
|
-
async function readOllamaCreds() {
|
|
496
|
-
try {
|
|
497
|
-
const raw = await fs.readFile(ollamaCredsPath(), "utf-8");
|
|
498
|
-
const parsed = JSON.parse(raw);
|
|
499
|
-
if (typeof parsed.apiKey !== "string" || typeof parsed.endpoint !== "string") return null;
|
|
500
|
-
return {
|
|
501
|
-
apiKey: parsed.apiKey,
|
|
502
|
-
endpoint: parsed.endpoint,
|
|
503
|
-
createdAt: typeof parsed.createdAt === "number" ? parsed.createdAt : Date.now()
|
|
504
|
-
};
|
|
505
|
-
} catch {
|
|
506
|
-
return null;
|
|
738
|
+
function firePiAiShadowProbe(config, resolvedModel) {
|
|
739
|
+
if (piAiMode() === "off") return;
|
|
740
|
+
if (!shouldRouteViaPiAi(config)) return;
|
|
741
|
+
const provider = extractByokProvider(config);
|
|
742
|
+
const modelId = typeof config.model === "string" ? config.model : "unknown";
|
|
743
|
+
if (provider) {
|
|
744
|
+
registerShadowContext(resolvedModel, {
|
|
745
|
+
provider,
|
|
746
|
+
modelId,
|
|
747
|
+
config
|
|
748
|
+
});
|
|
507
749
|
}
|
|
750
|
+
void (async () => {
|
|
751
|
+
let handle = null;
|
|
752
|
+
let error = null;
|
|
753
|
+
try {
|
|
754
|
+
handle = await resolveModelViaPiAi(config);
|
|
755
|
+
} catch (err) {
|
|
756
|
+
error = err instanceof Error ? err.message : String(err);
|
|
757
|
+
}
|
|
758
|
+
await recordShadowCost({
|
|
759
|
+
requestId: `probe-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
|
|
760
|
+
provider: handle?.piAiProvider ?? provider ?? "unknown",
|
|
761
|
+
modelId: handle?.modelId ?? modelId,
|
|
762
|
+
inputTokens: 0,
|
|
763
|
+
// real counts come from loop.ts via recordShadowUsage
|
|
764
|
+
outputTokens: 0,
|
|
765
|
+
piAiCostUsd: null,
|
|
766
|
+
stripeChargeUsd: null,
|
|
767
|
+
timestamp: Date.now(),
|
|
768
|
+
...error ? { error } : {}
|
|
769
|
+
});
|
|
770
|
+
})();
|
|
508
771
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
await fs.writeFile(file, JSON.stringify(creds, null, 2) + "\n", { mode: 384 });
|
|
514
|
-
}
|
|
515
|
-
async function clearOllamaCreds() {
|
|
516
|
-
try {
|
|
517
|
-
await fs.unlink(ollamaCredsPath());
|
|
518
|
-
} catch (err) {
|
|
519
|
-
const code = err.code;
|
|
520
|
-
if (code !== "ENOENT") throw err;
|
|
772
|
+
function extractByokProvider(config) {
|
|
773
|
+
if (config.byokProvider) return config.byokProvider;
|
|
774
|
+
if (typeof config.model === "string" && isByokRef(config.model)) {
|
|
775
|
+
return parseByokRef(config.model).provider;
|
|
521
776
|
}
|
|
777
|
+
return void 0;
|
|
522
778
|
}
|
|
523
779
|
|
|
524
780
|
export {
|
|
@@ -529,15 +785,11 @@ export {
|
|
|
529
785
|
ByokMissingApiKeyError,
|
|
530
786
|
ByokMissingBaseUrlError,
|
|
531
787
|
resolveByokModel,
|
|
788
|
+
recordShadowUsage,
|
|
532
789
|
MissingApiKeyError,
|
|
533
790
|
MODEL_CATALOG,
|
|
534
|
-
MODEL_IDS,
|
|
535
791
|
isValidModel,
|
|
536
792
|
modelSupportsImages,
|
|
537
793
|
resolveModel,
|
|
538
|
-
validateConfig
|
|
539
|
-
ollamaCredsPath,
|
|
540
|
-
readOllamaCreds,
|
|
541
|
-
writeOllamaCreds,
|
|
542
|
-
clearOllamaCreds
|
|
794
|
+
validateConfig
|
|
543
795
|
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/providers/ollama-credentials.ts
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
function notchConfigDir() {
|
|
5
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
6
|
+
if (xdg) return path.join(xdg, "notch");
|
|
7
|
+
const appdata = process.env.APPDATA;
|
|
8
|
+
if (appdata && process.platform === "win32") return path.join(appdata, "notch");
|
|
9
|
+
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
10
|
+
if (!home) {
|
|
11
|
+
throw new Error("Cannot determine home directory for credential storage.");
|
|
12
|
+
}
|
|
13
|
+
return path.join(home, ".config", "notch");
|
|
14
|
+
}
|
|
15
|
+
function ollamaCredsPath() {
|
|
16
|
+
return path.join(notchConfigDir(), "ollama.json");
|
|
17
|
+
}
|
|
18
|
+
async function readOllamaCreds() {
|
|
19
|
+
try {
|
|
20
|
+
const raw = await fs.readFile(ollamaCredsPath(), "utf-8");
|
|
21
|
+
const parsed = JSON.parse(raw);
|
|
22
|
+
if (typeof parsed.apiKey !== "string" || typeof parsed.endpoint !== "string") return null;
|
|
23
|
+
return {
|
|
24
|
+
apiKey: parsed.apiKey,
|
|
25
|
+
endpoint: parsed.endpoint,
|
|
26
|
+
createdAt: typeof parsed.createdAt === "number" ? parsed.createdAt : Date.now()
|
|
27
|
+
};
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function writeOllamaCreds(creds) {
|
|
33
|
+
const dir = notchConfigDir();
|
|
34
|
+
await fs.mkdir(dir, { recursive: true, mode: 448 });
|
|
35
|
+
const file = ollamaCredsPath();
|
|
36
|
+
await fs.writeFile(file, JSON.stringify(creds, null, 2) + "\n", { mode: 384 });
|
|
37
|
+
}
|
|
38
|
+
async function clearOllamaCreds() {
|
|
39
|
+
try {
|
|
40
|
+
await fs.unlink(ollamaCredsPath());
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const code = err.code;
|
|
43
|
+
if (code !== "ENOENT") throw err;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
ollamaCredsPath,
|
|
49
|
+
readOllamaCreds,
|
|
50
|
+
writeOllamaCreds,
|
|
51
|
+
clearOllamaCreds
|
|
52
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
6
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
7
|
+
}) : x)(function(x) {
|
|
8
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
9
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
10
|
+
});
|
|
11
|
+
var __esm = (fn, res) => function __init() {
|
|
12
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
13
|
+
};
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
__require,
|
|
30
|
+
__esm,
|
|
31
|
+
__export,
|
|
32
|
+
__toCommonJS
|
|
33
|
+
};
|
|
File without changes
|