@freesyntax/notch-cli 0.5.22 → 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/{chunk-EPSOOCNB.js → chunk-474TAHDN.js} +46 -6
- package/dist/{chunk-JXQ4HZ47.js → chunk-JVFOAPYV.js} +279 -28
- package/dist/{chunk-J66N6AFH.js → chunk-UHK6SI4H.js} +87 -18
- package/dist/{chunk-FZVPGJJW.js → chunk-YNYVQ7ZI.js} +10 -10
- package/dist/{config-set-3IWEVZQ4.js → config-set-5F4VK7IT.js} +3 -2
- package/dist/index.js +540 -212
- package/dist/{model-download-3NDKS3VM.js → model-download-KCQJCEPW.js} +1 -1
- package/dist/{ollama-bench-5V5CCOCQ.js → ollama-bench-JLC5POG3.js} +6 -6
- package/dist/{ollama-launch-P5KBK7AJ.js → ollama-launch-3IKB2A3Z.js} +3 -3
- package/dist/server-GMF4WV67.js +187 -0
- package/dist/{tools-XWKCW4RN.js → tools-ABRZPCEJ.js} +3 -1
- package/package.json +60 -57
- package/dist/server-IGOZHW52.js +0 -1479
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
|
|
51
51
|
// src/tools/index.ts
|
|
52
52
|
import { tool } from "ai";
|
|
53
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
53
54
|
|
|
54
55
|
// src/tools/code-mode.ts
|
|
55
56
|
import vm from "vm";
|
|
@@ -1696,6 +1697,7 @@ function mcpToolsToNotch(client, serverName) {
|
|
|
1696
1697
|
name: `mcp_${serverName}_${toolDef.name}`,
|
|
1697
1698
|
description: `[MCP/${serverName}] ${toolDef.description}`,
|
|
1698
1699
|
parameters: params,
|
|
1700
|
+
inputSchema: toolDef.inputSchema,
|
|
1699
1701
|
async execute(args, _ctx) {
|
|
1700
1702
|
try {
|
|
1701
1703
|
const result = await client.callTool(toolDef.name, args);
|
|
@@ -2318,8 +2320,7 @@ async function spawnSubagent(config) {
|
|
|
2318
2320
|
## Working Directory
|
|
2319
2321
|
${toolContext.cwd}
|
|
2320
2322
|
|
|
2321
|
-
|
|
2322
|
-
${Object.keys(tools).map((n) => `- ${n}`).join("\n")}`;
|
|
2323
|
+
${describeToolSchemas(subagentCtx, { includeNames: Object.keys(tools) })}`;
|
|
2323
2324
|
const messages = [
|
|
2324
2325
|
{ role: "user", content: prompt }
|
|
2325
2326
|
];
|
|
@@ -2454,8 +2455,7 @@ async function spawnBuiltinAgent(config) {
|
|
|
2454
2455
|
## Working Directory
|
|
2455
2456
|
${toolContext.cwd}
|
|
2456
2457
|
|
|
2457
|
-
|
|
2458
|
-
${Object.keys(tools).map((n) => `- ${n}`).join("\n")}`;
|
|
2458
|
+
${describeToolSchemas(subagentCtx, { includeNames: Object.keys(tools) })}`;
|
|
2459
2459
|
const messages = [{ role: "user", content: prompt }];
|
|
2460
2460
|
let iterations = 0;
|
|
2461
2461
|
let totalToolCalls = 0;
|
|
@@ -3081,11 +3081,50 @@ ${paramSummary}`
|
|
|
3081
3081
|
function listToolNames(ctx) {
|
|
3082
3082
|
return getAllTools(ctx).map((t) => t.name);
|
|
3083
3083
|
}
|
|
3084
|
-
function
|
|
3084
|
+
function describeTools(ctx) {
|
|
3085
3085
|
return getAllTools(ctx).map(
|
|
3086
3086
|
(t) => `- **${t.name}**: ${t.description}`
|
|
3087
3087
|
).join("\n");
|
|
3088
3088
|
}
|
|
3089
|
+
function describeToolSchemas(ctx, options = {}) {
|
|
3090
|
+
const include = options.includeNames ? new Set(options.includeNames) : null;
|
|
3091
|
+
const tools = getAllTools(ctx).filter((t) => !include || include.has(t.name));
|
|
3092
|
+
if (tools.length === 0) return "## Tool Schemas\nNo tools are available in this mode.";
|
|
3093
|
+
const lines = [
|
|
3094
|
+
"## Tool Schemas",
|
|
3095
|
+
"Native tool schemas are sent with every model request. They are repeated here so OpenAI-compatible routers and local models can reliably choose valid tools.",
|
|
3096
|
+
"When using a tool, call the native tool with exactly one listed name and a JSON object matching its parameters schema. Do not print tool-call JSON as normal text.",
|
|
3097
|
+
""
|
|
3098
|
+
];
|
|
3099
|
+
for (const t of tools) {
|
|
3100
|
+
const schema = toolPromptSchema(t);
|
|
3101
|
+
lines.push(`### ${t.name}`);
|
|
3102
|
+
lines.push(t.description);
|
|
3103
|
+
lines.push("Parameters JSON Schema:");
|
|
3104
|
+
lines.push("```json");
|
|
3105
|
+
lines.push(JSON.stringify(schema));
|
|
3106
|
+
lines.push("```");
|
|
3107
|
+
lines.push("");
|
|
3108
|
+
}
|
|
3109
|
+
return lines.join("\n").trimEnd();
|
|
3110
|
+
}
|
|
3111
|
+
function toolPromptSchema(t) {
|
|
3112
|
+
const schema = t.inputSchema ?? zodToJsonSchema(t.parameters, {
|
|
3113
|
+
target: "jsonSchema7",
|
|
3114
|
+
$refStrategy: "none"
|
|
3115
|
+
});
|
|
3116
|
+
return stripSchemaNoise(schema);
|
|
3117
|
+
}
|
|
3118
|
+
function stripSchemaNoise(value) {
|
|
3119
|
+
if (Array.isArray(value)) return value.map(stripSchemaNoise);
|
|
3120
|
+
if (!value || typeof value !== "object") return value;
|
|
3121
|
+
const out = {};
|
|
3122
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
3123
|
+
if (key === "$schema" || key === "definitions" || key === "$defs") continue;
|
|
3124
|
+
out[key] = stripSchemaNoise(raw);
|
|
3125
|
+
}
|
|
3126
|
+
return out;
|
|
3127
|
+
}
|
|
3089
3128
|
function mcpToolCount() {
|
|
3090
3129
|
return mcpTools.length;
|
|
3091
3130
|
}
|
|
@@ -3103,6 +3142,7 @@ export {
|
|
|
3103
3142
|
disconnectMCPServers,
|
|
3104
3143
|
buildToolMap,
|
|
3105
3144
|
listToolNames,
|
|
3106
|
-
|
|
3145
|
+
describeTools,
|
|
3146
|
+
describeToolSchemas,
|
|
3107
3147
|
mcpToolCount
|
|
3108
3148
|
};
|
|
@@ -15,16 +15,17 @@ var BUILTIN_BYOK_PROVIDERS = [
|
|
|
15
15
|
label: "OpenAI",
|
|
16
16
|
baseUrl: "https://api.openai.com/v1",
|
|
17
17
|
apiKeyEnv: "OPENAI_API_KEY",
|
|
18
|
-
defaultModel: "gpt-
|
|
18
|
+
defaultModel: "gpt-5",
|
|
19
19
|
models: [
|
|
20
|
-
"gpt-4o",
|
|
21
|
-
"gpt-4o-mini",
|
|
22
|
-
"gpt-4-turbo",
|
|
23
|
-
"gpt-4.1",
|
|
24
|
-
"gpt-4.1-mini",
|
|
25
20
|
"gpt-5",
|
|
26
21
|
"gpt-5-mini",
|
|
27
|
-
"
|
|
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",
|
|
28
29
|
"o4-mini"
|
|
29
30
|
],
|
|
30
31
|
compatibility: "strict"
|
|
@@ -63,6 +64,33 @@ var BUILTIN_BYOK_PROVIDERS = [
|
|
|
63
64
|
"X-Title": "Notch CLI"
|
|
64
65
|
}
|
|
65
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
|
+
},
|
|
66
94
|
{
|
|
67
95
|
id: "together",
|
|
68
96
|
label: "Together AI",
|
|
@@ -194,23 +222,35 @@ function findByokProvider(id) {
|
|
|
194
222
|
function listByokProviders() {
|
|
195
223
|
return BUILTIN_BYOK_PROVIDERS.filter((p) => p.id !== "__custom__");
|
|
196
224
|
}
|
|
197
|
-
function
|
|
198
|
-
return
|
|
225
|
+
function normalizeProviderId(id) {
|
|
226
|
+
return id === "custom" ? "__custom__" : id;
|
|
199
227
|
}
|
|
200
|
-
function
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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;
|
|
204
237
|
}
|
|
205
238
|
function parseByokRef(ref) {
|
|
239
|
+
const slash = ref.indexOf("/");
|
|
206
240
|
const colon = ref.indexOf(":");
|
|
207
|
-
if (colon < 0) {
|
|
208
|
-
|
|
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 };
|
|
209
245
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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 };
|
|
250
|
+
}
|
|
251
|
+
throw new Error(
|
|
252
|
+
`Provider model ref "${ref}" must be "<provider>/<model>" or "<provider>:<model>".`
|
|
253
|
+
);
|
|
214
254
|
}
|
|
215
255
|
var ByokMissingApiKeyError = class extends Error {
|
|
216
256
|
constructor(provider) {
|
|
@@ -225,7 +265,7 @@ var ByokMissingApiKeyError = class extends Error {
|
|
|
225
265
|
var ByokMissingBaseUrlError = class extends Error {
|
|
226
266
|
constructor() {
|
|
227
267
|
super(
|
|
228
|
-
"Custom
|
|
268
|
+
"Custom provider requires --base-url (and usually --api-key). Example: notch --provider custom --base-url https://my.endpoint/v1 --model my-model"
|
|
229
269
|
);
|
|
230
270
|
this.name = "ByokMissingBaseUrlError";
|
|
231
271
|
}
|
|
@@ -234,7 +274,7 @@ function resolveByokModel(spec) {
|
|
|
234
274
|
const providerInfo = findByokProvider(spec.provider);
|
|
235
275
|
if (!providerInfo) {
|
|
236
276
|
throw new Error(
|
|
237
|
-
`Unknown
|
|
277
|
+
`Unknown provider "${spec.provider}". Available: ${listByokProviders().map((p) => p.id).join(", ")}, custom`
|
|
238
278
|
);
|
|
239
279
|
}
|
|
240
280
|
if (providerInfo.id === "__custom__" && !spec.baseUrl) {
|
|
@@ -244,7 +284,7 @@ function resolveByokModel(spec) {
|
|
|
244
284
|
const modelId = spec.model && spec.model.length > 0 ? spec.model : providerInfo.defaultModel;
|
|
245
285
|
if (!modelId) {
|
|
246
286
|
throw new Error(
|
|
247
|
-
`
|
|
287
|
+
`Provider "${providerInfo.id}" has no default model. Pass --model or set byok.model in .notch.json.`
|
|
248
288
|
);
|
|
249
289
|
}
|
|
250
290
|
let apiKey = spec.apiKey;
|
|
@@ -303,7 +343,7 @@ function resolveByokModel(spec) {
|
|
|
303
343
|
async function validateByokConfig(spec) {
|
|
304
344
|
const providerInfo = findByokProvider(spec.provider);
|
|
305
345
|
if (!providerInfo) {
|
|
306
|
-
return { ok: false, error: `Unknown
|
|
346
|
+
return { ok: false, error: `Unknown provider "${spec.provider}"` };
|
|
307
347
|
}
|
|
308
348
|
if (providerInfo.id === "__custom__" && !spec.baseUrl) {
|
|
309
349
|
return { ok: false, error: "Custom provider requires --base-url" };
|
|
@@ -324,6 +364,156 @@ async function validateByokConfig(spec) {
|
|
|
324
364
|
|
|
325
365
|
// src/providers/registry.ts
|
|
326
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
|
|
327
517
|
var MissingApiKeyError = class extends Error {
|
|
328
518
|
/** Which flow caused this error (informs the onboarding message). */
|
|
329
519
|
flow;
|
|
@@ -421,22 +611,40 @@ function isValidModel(id) {
|
|
|
421
611
|
}
|
|
422
612
|
function modelSupportsImages(modelId) {
|
|
423
613
|
if (!modelId) return false;
|
|
614
|
+
const lowerModelId = modelId.toLowerCase();
|
|
424
615
|
if (modelId in MODEL_CATALOG) return true;
|
|
425
|
-
if (
|
|
426
|
-
const
|
|
616
|
+
if (lowerModelId.startsWith("notch-")) return true;
|
|
617
|
+
const ref = isByokRef(lowerModelId) ? parseByokRef(lowerModelId) : null;
|
|
618
|
+
const prefix = ref?.provider ?? "";
|
|
427
619
|
switch (prefix) {
|
|
428
620
|
case "openai":
|
|
429
621
|
case "anthropic":
|
|
430
622
|
case "google":
|
|
431
623
|
case "together":
|
|
432
|
-
case "openrouter":
|
|
433
624
|
return true;
|
|
625
|
+
case "openrouter":
|
|
626
|
+
return openRouterModelSupportsImages(ref?.model ?? "");
|
|
434
627
|
case "groq":
|
|
435
628
|
return false;
|
|
436
629
|
default:
|
|
437
630
|
return false;
|
|
438
631
|
}
|
|
439
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
|
+
}
|
|
440
648
|
function resolveModel(config) {
|
|
441
649
|
if (config.byokProvider) {
|
|
442
650
|
const resolved = resolveByokModel({
|
|
@@ -447,6 +655,7 @@ function resolveModel(config) {
|
|
|
447
655
|
headers: { ...config.headers, ...config.byokHeaders },
|
|
448
656
|
apiShape: config.byokApiShape
|
|
449
657
|
});
|
|
658
|
+
firePiAiShadowProbe(config, resolved.model);
|
|
450
659
|
return resolved.model;
|
|
451
660
|
}
|
|
452
661
|
if (typeof config.model === "string" && isByokRef(config.model)) {
|
|
@@ -459,12 +668,13 @@ function resolveModel(config) {
|
|
|
459
668
|
headers: { ...config.headers, ...config.byokHeaders },
|
|
460
669
|
apiShape: config.byokApiShape
|
|
461
670
|
});
|
|
671
|
+
firePiAiShadowProbe(config, resolved.model);
|
|
462
672
|
return resolved.model;
|
|
463
673
|
}
|
|
464
674
|
const info = MODEL_CATALOG[config.model];
|
|
465
675
|
if (!info) {
|
|
466
676
|
throw new Error(
|
|
467
|
-
`Unknown model "${config.model}".
|
|
677
|
+
`Unknown model "${config.model}". Use provider/model, for example openrouter/anthropic/claude-sonnet-4-6.`
|
|
468
678
|
);
|
|
469
679
|
}
|
|
470
680
|
const baseUrl = config.baseUrl ?? process.env.NOTCH_BASE_URL ?? info.baseUrl;
|
|
@@ -525,6 +735,47 @@ async function validateConfig(config) {
|
|
|
525
735
|
return { ok: false, error: `Cannot reach Notch ${info.label} at ${baseUrl}` };
|
|
526
736
|
}
|
|
527
737
|
}
|
|
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
|
+
});
|
|
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
|
+
})();
|
|
771
|
+
}
|
|
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;
|
|
776
|
+
}
|
|
777
|
+
return void 0;
|
|
778
|
+
}
|
|
528
779
|
|
|
529
780
|
export {
|
|
530
781
|
findByokProvider,
|
|
@@ -534,9 +785,9 @@ export {
|
|
|
534
785
|
ByokMissingApiKeyError,
|
|
535
786
|
ByokMissingBaseUrlError,
|
|
536
787
|
resolveByokModel,
|
|
788
|
+
recordShadowUsage,
|
|
537
789
|
MissingApiKeyError,
|
|
538
790
|
MODEL_CATALOG,
|
|
539
|
-
MODEL_IDS,
|
|
540
791
|
isValidModel,
|
|
541
792
|
modelSupportsImages,
|
|
542
793
|
resolveModel,
|
|
@@ -4,11 +4,18 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
findByokProvider,
|
|
6
6
|
isByokRef,
|
|
7
|
-
isValidModel
|
|
8
|
-
|
|
7
|
+
isValidModel,
|
|
8
|
+
listByokProviders,
|
|
9
|
+
parseByokRef
|
|
10
|
+
} from "./chunk-JVFOAPYV.js";
|
|
11
|
+
import {
|
|
12
|
+
OLLAMA_LOCAL_BASE_URL,
|
|
13
|
+
listModels
|
|
14
|
+
} from "./chunk-GFVLHUSS.js";
|
|
9
15
|
import {
|
|
10
16
|
init_auth,
|
|
11
|
-
loadCredentials
|
|
17
|
+
loadCredentials,
|
|
18
|
+
loadSyncedByokKeys
|
|
12
19
|
} from "./chunk-PPEBWOMJ.js";
|
|
13
20
|
|
|
14
21
|
// src/config.ts
|
|
@@ -16,7 +23,7 @@ import fs from "fs/promises";
|
|
|
16
23
|
import path from "path";
|
|
17
24
|
init_auth();
|
|
18
25
|
var DEFAULT_MODEL = {
|
|
19
|
-
model: "
|
|
26
|
+
model: "openrouter/anthropic/claude-sonnet-4-6",
|
|
20
27
|
temperature: 0.3
|
|
21
28
|
};
|
|
22
29
|
var DEFAULTS = {
|
|
@@ -33,17 +40,20 @@ var DEFAULTS = {
|
|
|
33
40
|
};
|
|
34
41
|
async function loadConfig(overrides = {}) {
|
|
35
42
|
const config = { ...DEFAULTS, models: { chat: { ...DEFAULT_MODEL } } };
|
|
43
|
+
let explicitModelConfig = false;
|
|
36
44
|
const configPath = path.resolve(config.projectRoot, ".notch.json");
|
|
37
45
|
try {
|
|
38
46
|
const raw = await fs.readFile(configPath, "utf-8");
|
|
39
47
|
const fileConfig = JSON.parse(raw);
|
|
40
48
|
if (fileConfig.model && (isValidModel(fileConfig.model) || isByokRef(fileConfig.model))) {
|
|
41
49
|
config.models.chat.model = fileConfig.model;
|
|
50
|
+
explicitModelConfig = true;
|
|
42
51
|
}
|
|
43
52
|
if (fileConfig.baseUrl) config.models.chat.baseUrl = fileConfig.baseUrl;
|
|
44
53
|
if (fileConfig.apiKey) config.models.chat.apiKey = fileConfig.apiKey;
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
const providerBlock = fileConfig.provider && typeof fileConfig.provider === "object" ? fileConfig.provider : fileConfig.byok;
|
|
55
|
+
if (providerBlock && typeof providerBlock === "object") {
|
|
56
|
+
const byok = providerBlock;
|
|
47
57
|
config.byok = { ...byok };
|
|
48
58
|
if (byok.provider && findByokProvider(byok.provider === "custom" ? "__custom__" : byok.provider)) {
|
|
49
59
|
config.models.chat.byokProvider = byok.provider === "custom" ? "__custom__" : byok.provider;
|
|
@@ -55,6 +65,7 @@ async function loadConfig(overrides = {}) {
|
|
|
55
65
|
if (byok.apiShape === "openai" || byok.apiShape === "anthropic") {
|
|
56
66
|
config.models.chat.byokApiShape = byok.apiShape;
|
|
57
67
|
}
|
|
68
|
+
explicitModelConfig = true;
|
|
58
69
|
}
|
|
59
70
|
}
|
|
60
71
|
if (fileConfig.hybrid && typeof fileConfig.hybrid === "object") {
|
|
@@ -72,7 +83,34 @@ async function loadConfig(overrides = {}) {
|
|
|
72
83
|
if (fileConfig.theme) config.theme = fileConfig.theme;
|
|
73
84
|
} catch {
|
|
74
85
|
}
|
|
75
|
-
|
|
86
|
+
if (process.env.NOTCH_PROVIDER) {
|
|
87
|
+
const providerId = process.env.NOTCH_PROVIDER === "custom" ? "__custom__" : process.env.NOTCH_PROVIDER;
|
|
88
|
+
const provider = findByokProvider(providerId);
|
|
89
|
+
if (provider) {
|
|
90
|
+
config.models.chat.byokProvider = providerId;
|
|
91
|
+
config.models.chat.model = process.env.NOTCH_MODEL || provider.defaultModel;
|
|
92
|
+
explicitModelConfig = true;
|
|
93
|
+
}
|
|
94
|
+
} else if (process.env.NOTCH_MODEL) {
|
|
95
|
+
const envModel = process.env.NOTCH_MODEL;
|
|
96
|
+
if (isValidModel(envModel)) {
|
|
97
|
+
config.models.chat.model = envModel;
|
|
98
|
+
config.models.chat.byokProvider = void 0;
|
|
99
|
+
explicitModelConfig = true;
|
|
100
|
+
} else if (isByokRef(envModel)) {
|
|
101
|
+
config.models.chat.model = envModel;
|
|
102
|
+
config.models.chat.byokProvider = void 0;
|
|
103
|
+
explicitModelConfig = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (!explicitModelConfig) {
|
|
107
|
+
const inferred = await inferDefaultProviderConfig();
|
|
108
|
+
config.models.chat.model = inferred.modelRef;
|
|
109
|
+
config.models.chat.byokProvider = void 0;
|
|
110
|
+
if (inferred.baseUrl) config.models.chat.baseUrl = inferred.baseUrl;
|
|
111
|
+
if (inferred.apiShape) config.models.chat.byokApiShape = inferred.apiShape;
|
|
112
|
+
}
|
|
113
|
+
const activeProviderId = config.models.chat.byokProvider ?? (typeof config.models.chat.model === "string" && isByokRef(config.models.chat.model) ? parseByokRef(config.models.chat.model).provider : void 0);
|
|
76
114
|
const isOllamaProvider = activeProviderId === "ollama" || activeProviderId === "ollama-cloud" || activeProviderId === "ollama-anthropic";
|
|
77
115
|
if (isOllamaProvider) {
|
|
78
116
|
if (!config.models.chat.apiKey && !process.env.OLLAMA_API_KEY) {
|
|
@@ -81,25 +119,16 @@ async function loadConfig(overrides = {}) {
|
|
|
81
119
|
config.models.chat.apiKey = ollamaCreds.apiKey;
|
|
82
120
|
}
|
|
83
121
|
}
|
|
84
|
-
} else {
|
|
122
|
+
} else if (!activeProviderId) {
|
|
85
123
|
const creds = await loadCredentials();
|
|
86
124
|
if (creds?.token) {
|
|
87
125
|
config.models.chat.apiKey = creds.token;
|
|
88
126
|
}
|
|
89
127
|
}
|
|
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
128
|
if (process.env.NOTCH_BASE_URL) {
|
|
100
129
|
config.models.chat.baseUrl = process.env.NOTCH_BASE_URL;
|
|
101
130
|
}
|
|
102
|
-
if (process.env.NOTCH_API_KEY) {
|
|
131
|
+
if (process.env.NOTCH_API_KEY && !activeProviderId) {
|
|
103
132
|
config.models.chat.apiKey = process.env.NOTCH_API_KEY;
|
|
104
133
|
}
|
|
105
134
|
if (config.models.chat.temperature !== void 0) {
|
|
@@ -108,6 +137,46 @@ async function loadConfig(overrides = {}) {
|
|
|
108
137
|
config.maxIterations = Math.max(1, Math.min(100, config.maxIterations));
|
|
109
138
|
return { ...config, ...overrides };
|
|
110
139
|
}
|
|
140
|
+
async function inferDefaultProviderConfig() {
|
|
141
|
+
const providers = listByokProviders();
|
|
142
|
+
const envMatch = providers.find((p) => p.apiKeyEnv && process.env[p.apiKeyEnv]);
|
|
143
|
+
if (envMatch) return { modelRef: `${envMatch.id}/${envMatch.defaultModel}` };
|
|
144
|
+
const synced = await loadSyncedByokKeys();
|
|
145
|
+
if (synced) {
|
|
146
|
+
const syncedMatch = providers.find((p) => {
|
|
147
|
+
const key = synced.keys[p.id];
|
|
148
|
+
return typeof key === "string" && key.length > 0;
|
|
149
|
+
});
|
|
150
|
+
if (syncedMatch) return { modelRef: `${syncedMatch.id}/${syncedMatch.defaultModel}` };
|
|
151
|
+
if (synced.localEndpoint) {
|
|
152
|
+
return {
|
|
153
|
+
modelRef: "custom/local-model",
|
|
154
|
+
baseUrl: synced.localEndpoint,
|
|
155
|
+
apiShape: "openai"
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const localOllama = await inferLocalOllamaModel();
|
|
160
|
+
if (localOllama) return localOllama;
|
|
161
|
+
const openrouter = findByokProvider("openrouter");
|
|
162
|
+
if (openrouter) return { modelRef: `${openrouter.id}/${openrouter.defaultModel}` };
|
|
163
|
+
const first = providers[0];
|
|
164
|
+
return { modelRef: first ? `${first.id}/${first.defaultModel}` : DEFAULT_MODEL.model };
|
|
165
|
+
}
|
|
166
|
+
async function inferLocalOllamaModel() {
|
|
167
|
+
try {
|
|
168
|
+
const baseUrl = process.env.OLLAMA_HOST?.startsWith("http") ? process.env.OLLAMA_HOST : OLLAMA_LOCAL_BASE_URL;
|
|
169
|
+
const rows = await listModels(baseUrl, { timeoutMs: 300 });
|
|
170
|
+
const modelName = rows[0]?.name;
|
|
171
|
+
if (!modelName) return null;
|
|
172
|
+
return {
|
|
173
|
+
modelRef: `ollama/${modelName}`,
|
|
174
|
+
baseUrl: `${baseUrl.replace(/\/+$/, "")}/v1`
|
|
175
|
+
};
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
111
180
|
async function persistConfigPatch(projectRoot, patch) {
|
|
112
181
|
const configPath = path.resolve(projectRoot, ".notch.json");
|
|
113
182
|
let current = {};
|