@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.
Files changed (38) 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-TU465P2P.js → chunk-474TAHDN.js} +50 -8
  4. package/dist/{chunk-OSWUX6TC.js → chunk-4HPRBCSY.js} +1 -1
  5. package/dist/{chunk-QKM27RHS.js → chunk-6NKRMZTX.js} +1 -1
  6. package/dist/{chunk-443G6HCC.js → chunk-JVFOAPYV.js} +331 -79
  7. package/dist/chunk-KCAR5DOB.js +52 -0
  8. package/dist/chunk-KFQGP6VL.js +33 -0
  9. package/dist/chunk-O6AKZ4OH.js +0 -0
  10. package/dist/{chunk-FIFC4V2R.js → chunk-PPEBWOMJ.js} +91 -7
  11. package/dist/chunk-UHK6SI4H.js +206 -0
  12. package/dist/{chunk-MMBFNIKE.js → chunk-YNYVQ7ZI.js} +10 -8
  13. package/dist/{compression-SQAIQ2UU.js → compression-YJLWEHCC.js} +1 -0
  14. package/dist/config-set-5F4VK7IT.js +111 -0
  15. package/dist/{edit-JEFEK43H.js → edit-6QYAXVNU.js} +1 -0
  16. package/dist/{git-5T5TSQTX.js → git-DNQ5EELH.js} +1 -0
  17. package/dist/{github-DWRGWX6U.js → github-34T4QQIH.js} +1 -0
  18. package/dist/{glob-BI3P4C7Q.js → glob-XT43LEJ4.js} +1 -0
  19. package/dist/{grep-VZ3I5GNW.js → grep-T2CXYNRI.js} +1 -0
  20. package/dist/index.js +878 -428
  21. package/dist/{lsp-UPY6I3L7.js → lsp-JXQVU7NP.js} +1 -0
  22. package/dist/model-download-KCQJCEPW.js +176 -0
  23. package/dist/{notebook-FXJBTSPA.js → notebook-MFODW345.js} +1 -0
  24. package/dist/{ollama-bench-QQHBIG2D.js → ollama-bench-JLC5POG3.js} +8 -4
  25. package/dist/{ollama-launch-2ASVER3S.js → ollama-launch-3IKB2A3Z.js} +6 -2
  26. package/dist/{ollama-usage-2WPCZJJI.js → ollama-usage-3PROM2WC.js} +1 -0
  27. package/dist/{plugins-OG2P75K5.js → plugins-PNGRZLFW.js} +1 -0
  28. package/dist/{read-OVJG2XKW.js → read-B64XE7N3.js} +1 -0
  29. package/dist/server-GMF4WV67.js +187 -0
  30. package/dist/{session-index-SSGOOZXK.js → session-index-7FWEVP6E.js} +3 -2
  31. package/dist/{shell-4X545EVN.js → shell-BOZTHQUT.js} +1 -0
  32. package/dist/{task-OS3E5F3X.js → task-67G4KLYC.js} +1 -0
  33. package/dist/{tools-7WAWS6V4.js → tools-ABRZPCEJ.js} +6 -3
  34. package/dist/{web-fetch-KNIV3Z3W.js → web-fetch-OTNDICGJ.js} +1 -0
  35. package/dist/{write-NNHLOTYK.js → write-ZOSB7I4J.js} +1 -0
  36. package/package.json +60 -57
  37. package/dist/auth-JQX6MHJG.js +0 -16
  38. 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-4o",
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
- "o3-mini",
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 isByokRef(model) {
190
- return model.includes(":") && !isOllamaTagOnly(model);
225
+ function normalizeProviderId(id) {
226
+ return id === "custom" ? "__custom__" : id;
191
227
  }
192
- function isOllamaTagOnly(model) {
193
- const [head] = model.split(":");
194
- if (!head) return false;
195
- return !BYOK_BY_ID[head];
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
- throw new Error(`BYOK ref "${ref}" is missing a colon separator.`);
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
- const rawProvider = ref.slice(0, colon);
203
- const model = ref.slice(colon + 1);
204
- const provider = rawProvider === "custom" ? "__custom__" : rawProvider;
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 BYOK provider requires --base-url (and usually --api-key). Example: notch --provider custom --base-url https://my.endpoint/v1 --model my-model"
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 BYOK provider "${spec.provider}". Available: ${listByokProviders().map((p) => p.id).join(", ")}, custom`
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
- `BYOK provider "${providerInfo.id}" has no default model. Pass --model or set byok.model in .notch.json.`
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 BYOK provider "${spec.provider}"` };
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 (modelId.startsWith("notch-")) return true;
375
- const prefix = modelId.split(":", 1)[0]?.toLowerCase() ?? "";
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}". Notch models: ${MODEL_IDS.join(", ")}. For BYOK use "<provider>:<model>" (e.g. openrouter:anthropic/claude-sonnet-4-6).`
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
- // src/providers/ollama-credentials.ts
479
- import fs from "fs/promises";
480
- import path from "path";
481
- function notchConfigDir() {
482
- const xdg = process.env.XDG_CONFIG_HOME;
483
- if (xdg) return path.join(xdg, "notch");
484
- const appdata = process.env.APPDATA;
485
- if (appdata && process.platform === "win32") return path.join(appdata, "notch");
486
- const home = process.env.HOME ?? process.env.USERPROFILE;
487
- if (!home) {
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
- async function writeOllamaCreds(creds) {
510
- const dir = notchConfigDir();
511
- await fs.mkdir(dir, { recursive: true, mode: 448 });
512
- const file = ollamaCredsPath();
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