@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.
- package/dist/{apply-patch-D5PDUXUC.js → apply-patch-U6K67CMT.js} +1 -0
- package/dist/auth-UAMMP5IJ.js +29 -0
- package/dist/chunk-4HPRBCSY.js +167 -0
- package/dist/chunk-6NKRMZTX.js +198 -0
- package/dist/{chunk-YBYF7L4A.js → chunk-EPSOOCNB.js} +1832 -1331
- package/dist/chunk-FZVPGJJW.js +511 -0
- package/dist/chunk-GFVLHUSS.js +155 -0
- package/dist/chunk-J66N6AFH.js +137 -0
- package/dist/chunk-JXQ4HZ47.js +544 -0
- package/dist/chunk-KCAR5DOB.js +52 -0
- package/dist/chunk-KFQGP6VL.js +33 -0
- package/dist/chunk-O6AKZ4OH.js +0 -0
- package/dist/{chunk-6M6CXXWR.js → chunk-PKZKVOAN.js} +209 -1
- package/dist/{chunk-FIFC4V2R.js → chunk-PPEBWOMJ.js} +91 -7
- package/dist/compression-YJLWEHCC.js +33 -0
- package/dist/config-set-3IWEVZQ4.js +110 -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 +2606 -960
- package/dist/{lsp-UPY6I3L7.js → lsp-JXQVU7NP.js} +1 -0
- package/dist/model-download-3NDKS3VM.js +176 -0
- package/dist/{notebook-FXJBTSPA.js → notebook-MFODW345.js} +1 -0
- package/dist/ollama-bench-5V5CCOCQ.js +194 -0
- package/dist/ollama-launch-P5KBK7AJ.js +22 -0
- package/dist/ollama-usage-3PROM2WC.js +70 -0
- package/dist/{plugins-OG2P75K5.js → plugins-PNGRZLFW.js} +1 -0
- package/dist/{read-OVJG2XKW.js → read-B64XE7N3.js} +1 -0
- package/dist/{server-W7FRCVRZ.js → server-IGOZHW52.js} +17 -15
- package/dist/session-index-7FWEVP6E.js +22 -0
- package/dist/{shell-4X545EVN.js → shell-BOZTHQUT.js} +1 -0
- package/dist/{task-OS3E5F3X.js → task-67G4KLYC.js} +1 -0
- package/dist/{tools-Q7CDHB4K.js → tools-XWKCW4RN.js} +4 -1
- 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 +2 -1
- package/dist/auth-JQX6MHJG.js +0 -16
- 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
|
+
};
|