@nvae/llmswitch 0.2.0 → 0.5.0

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.
@@ -0,0 +1,439 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { request as httpRequest } from "node:http";
3
+ import { request as httpsRequest } from "node:https";
4
+ import { HttpsProxyAgent } from "https-proxy-agent";
5
+ import { SocksProxyAgent } from "socks-proxy-agent";
6
+ const SUPPORTED_TARGET_PROTOCOLS = new Set(["http:", "https:"]);
7
+ const SUPPORTED_PROXY_PROTOCOLS = new Set([
8
+ "http:",
9
+ "https:",
10
+ "socks:",
11
+ "socks4:",
12
+ "socks4a:",
13
+ "socks5:",
14
+ "socks5h:",
15
+ ]);
16
+ /** Hop-by-hop and inbound-credential headers never forwarded upstream. */
17
+ const STRIPPED_REQUEST_HEADERS = new Set([
18
+ "connection",
19
+ "keep-alive",
20
+ "proxy-authenticate",
21
+ "proxy-authorization",
22
+ "te",
23
+ "trailer",
24
+ "transfer-encoding",
25
+ "upgrade",
26
+ "host",
27
+ "content-length",
28
+ ]);
29
+ const DEFAULT_CONNECT_TIMEOUT_MS = 30_000;
30
+ const DEFAULT_IDLE_TIMEOUT_MS = 90_000;
31
+ const DEFAULT_TOTAL_TIMEOUT_MS = 600_000;
32
+ const DEFAULT_MAX_REDIRECTS = 5;
33
+ export class TransportTimeoutError extends Error {
34
+ constructor(message = "上游请求超时") {
35
+ super(message);
36
+ this.name = "TransportTimeoutError";
37
+ }
38
+ }
39
+ export class ResponseLimitError extends Error {
40
+ constructor(message = "上游响应超出大小限制") {
41
+ super(message);
42
+ this.name = "ResponseLimitError";
43
+ }
44
+ }
45
+ export class CrossOriginRedirectError extends Error {
46
+ constructor(message = "拒绝跨源 redirect") {
47
+ super(message);
48
+ this.name = "CrossOriginRedirectError";
49
+ }
50
+ }
51
+ export class TransportProtocolError extends Error {
52
+ constructor(message) {
53
+ super(message);
54
+ this.name = "TransportProtocolError";
55
+ }
56
+ }
57
+ /**
58
+ * Resolve the proxy URL for a target. A profile has a single proxy applied to
59
+ * all traffic; the URL scheme (http/https/socks*) selects the agent.
60
+ */
61
+ export function selectProxyUrl(_target, proxy) {
62
+ const chosen = proxy?.trim();
63
+ return chosen || null;
64
+ }
65
+ /**
66
+ * Build a fresh per-request Agent for the given proxy URL. Callers own its
67
+ * lifecycle and must call `.destroy()` once the response is drained.
68
+ */
69
+ export function createTransportAgent(_target, proxyUrl) {
70
+ let proxy;
71
+ try {
72
+ proxy = new URL(proxyUrl);
73
+ }
74
+ catch {
75
+ throw new TransportProtocolError(`无效的 proxy URL: ${proxyUrl}`);
76
+ }
77
+ if (!SUPPORTED_PROXY_PROTOCOLS.has(proxy.protocol)) {
78
+ throw new TransportProtocolError(`不支持的 proxy protocol: ${proxy.protocol}`);
79
+ }
80
+ if (proxy.protocol === "http:" || proxy.protocol === "https:") {
81
+ return new HttpsProxyAgent(proxy);
82
+ }
83
+ // socks5 resolves DNS locally; socks5h/socks4a defer to the proxy.
84
+ return new SocksProxyAgent(proxy);
85
+ }
86
+ function sanitizeHeaders(headers) {
87
+ const out = {};
88
+ if (!headers)
89
+ return out;
90
+ for (const [key, value] of Object.entries(headers)) {
91
+ if (value === undefined)
92
+ continue;
93
+ if (STRIPPED_REQUEST_HEADERS.has(key.toLowerCase()))
94
+ continue;
95
+ out[key] = value;
96
+ }
97
+ return out;
98
+ }
99
+ function isSameOrigin(a, b) {
100
+ return (a.protocol === b.protocol && a.hostname === b.hostname && a.port === b.port);
101
+ }
102
+ /**
103
+ * A Web-`Response`-like view. Consume the body exactly once: either as a
104
+ * stream (`body`) for SSE pass-through, or buffered (`text`/`json`/
105
+ * `arrayBuffer`) with the response size limit enforced. Both paths re-arm the
106
+ * idle timer, and finalize timers/abort listener/proxy Agent on completion.
107
+ */
108
+ class NodeTransportResponse {
109
+ status;
110
+ statusText;
111
+ headers;
112
+ ctx;
113
+ consumed = false;
114
+ pendingFatal = null;
115
+ failSink = null;
116
+ constructor(status, statusText, headers, ctx) {
117
+ this.status = status;
118
+ this.statusText = statusText;
119
+ this.headers = headers;
120
+ this.ctx = ctx;
121
+ }
122
+ get ok() {
123
+ return this.status >= 200 && this.status < 300;
124
+ }
125
+ /** Routes a fatal condition (idle/total timeout, abort) to the active consumer. */
126
+ fail(err) {
127
+ if (this.failSink)
128
+ this.failSink(err);
129
+ else
130
+ this.pendingFatal = err;
131
+ }
132
+ get body() {
133
+ if (this.consumed)
134
+ return null;
135
+ this.consumed = true;
136
+ const ctx = this.ctx;
137
+ const { res } = ctx;
138
+ return new ReadableStream({
139
+ start: (controller) => {
140
+ let closed = false;
141
+ const stop = (err) => {
142
+ if (closed)
143
+ return;
144
+ closed = true;
145
+ ctx.finalize();
146
+ if (err) {
147
+ res.destroy();
148
+ try {
149
+ controller.error(err);
150
+ }
151
+ catch {
152
+ // already errored
153
+ }
154
+ }
155
+ else {
156
+ try {
157
+ controller.close();
158
+ }
159
+ catch {
160
+ // already closed
161
+ }
162
+ }
163
+ };
164
+ this.failSink = (err) => stop(err);
165
+ if (this.pendingFatal) {
166
+ stop(this.pendingFatal);
167
+ return;
168
+ }
169
+ res.on("data", (chunk) => {
170
+ ctx.armIdle();
171
+ controller.enqueue(new Uint8Array(chunk));
172
+ });
173
+ res.on("end", () => stop(null));
174
+ res.on("error", (err) => stop(err));
175
+ res.on("aborted", () => stop(new TransportTimeoutError("上游连接中断")));
176
+ res.resume();
177
+ },
178
+ cancel: () => {
179
+ ctx.finalize();
180
+ res.destroy();
181
+ },
182
+ });
183
+ }
184
+ buffered() {
185
+ if (this.consumed) {
186
+ return Promise.reject(new Error("响应 body 已被消费"));
187
+ }
188
+ this.consumed = true;
189
+ const ctx = this.ctx;
190
+ const { res } = ctx;
191
+ const max = ctx.maxResponseBytes;
192
+ return new Promise((resolve, reject) => {
193
+ const chunks = [];
194
+ let total = 0;
195
+ let done = false;
196
+ const finish = (err, buf) => {
197
+ if (done)
198
+ return;
199
+ done = true;
200
+ ctx.finalize();
201
+ if (err) {
202
+ res.destroy();
203
+ reject(err);
204
+ }
205
+ else {
206
+ resolve(buf ?? Buffer.concat(chunks));
207
+ }
208
+ };
209
+ this.failSink = (err) => finish(err);
210
+ if (this.pendingFatal) {
211
+ finish(this.pendingFatal);
212
+ return;
213
+ }
214
+ res.on("data", (chunk) => {
215
+ ctx.armIdle();
216
+ total += chunk.byteLength;
217
+ if (max !== undefined && total > max) {
218
+ finish(new ResponseLimitError());
219
+ return;
220
+ }
221
+ chunks.push(chunk);
222
+ });
223
+ res.on("end", () => finish(null, Buffer.concat(chunks)));
224
+ res.on("error", (err) => finish(err));
225
+ res.on("aborted", () => finish(new TransportTimeoutError("上游连接中断")));
226
+ res.resume();
227
+ });
228
+ }
229
+ async arrayBuffer() {
230
+ const buf = await this.buffered();
231
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
232
+ }
233
+ async text() {
234
+ return (await this.buffered()).toString("utf8");
235
+ }
236
+ async json() {
237
+ return JSON.parse(await this.text());
238
+ }
239
+ }
240
+ export async function requestWithNodeTransport(options) {
241
+ const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
242
+ const origin = parseTarget(options.url);
243
+ let current = origin;
244
+ for (let redirects = 0;; redirects += 1) {
245
+ const result = await performRequest(current, options);
246
+ if (result.type === "redirect" &&
247
+ redirects < maxRedirects &&
248
+ result.location) {
249
+ const next = parseTarget(new URL(result.location, current).toString());
250
+ if (!isSameOrigin(next, origin)) {
251
+ throw new CrossOriginRedirectError(`拒绝跨源 redirect: ${current.origin} → ${next.origin}`);
252
+ }
253
+ current = next;
254
+ continue;
255
+ }
256
+ if (result.type === "redirect" && redirects >= maxRedirects) {
257
+ throw new TransportProtocolError("redirect 次数超过上限");
258
+ }
259
+ return result.response;
260
+ }
261
+ }
262
+ function parseTarget(url) {
263
+ let target;
264
+ try {
265
+ target = new URL(url);
266
+ }
267
+ catch {
268
+ throw new TransportProtocolError(`无效的目标 URL: ${url}`);
269
+ }
270
+ if (!SUPPORTED_TARGET_PROTOCOLS.has(target.protocol)) {
271
+ throw new TransportProtocolError(`不支持的 target protocol: ${target.protocol}`);
272
+ }
273
+ return target;
274
+ }
275
+ function performRequest(target, options) {
276
+ const proxyUrl = selectProxyUrl(target, options.proxy);
277
+ const agent = proxyUrl ? createTransportAgent(target, proxyUrl) : undefined;
278
+ const isHttps = target.protocol === "https:";
279
+ const requestImpl = isHttps ? httpsRequest : httpRequest;
280
+ const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
281
+ const idleTimeoutMs = options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
282
+ const totalTimeoutMs = options.totalTimeoutMs ?? DEFAULT_TOTAL_TIMEOUT_MS;
283
+ const maxResponseBytes = options.maxResponseBytes;
284
+ const headers = sanitizeHeaders(options.headers);
285
+ const bodyBuffer = normalizeBody(options.body);
286
+ if (bodyBuffer) {
287
+ headers["Content-Length"] = String(bodyBuffer.byteLength);
288
+ }
289
+ return new Promise((resolve, reject) => {
290
+ let settled = false;
291
+ let req = null;
292
+ const timers = new Set();
293
+ // Set once headers arrive; routes body-phase failures to the body reader.
294
+ let bodyController = null;
295
+ const timerBag = {
296
+ clearAll() {
297
+ for (const timer of timers)
298
+ clearTimeout(timer);
299
+ timers.clear();
300
+ },
301
+ };
302
+ const cleanupAgent = () => {
303
+ if (agent && typeof agent.destroy === "function") {
304
+ agent.destroy();
305
+ }
306
+ };
307
+ const fail = (err) => {
308
+ if (settled)
309
+ return;
310
+ settled = true;
311
+ timerBag.clearAll();
312
+ if (options.signal)
313
+ options.signal.removeEventListener("abort", onAbort);
314
+ req?.destroy();
315
+ cleanupAgent();
316
+ reject(err);
317
+ };
318
+ // Fatal condition (timeout/abort): before headers reject the request
319
+ // promise; during the body phase reject the body reader instead.
320
+ const onFatal = (err) => {
321
+ if (bodyController)
322
+ bodyController.fail(err);
323
+ else
324
+ fail(err);
325
+ };
326
+ const onAbort = () => {
327
+ const reason = options.signal?.reason instanceof Error
328
+ ? options.signal.reason
329
+ : abortError();
330
+ onFatal(reason);
331
+ };
332
+ if (options.signal) {
333
+ if (options.signal.aborted) {
334
+ cleanupAgent();
335
+ reject(abortError());
336
+ return;
337
+ }
338
+ options.signal.addEventListener("abort", onAbort, { once: true });
339
+ }
340
+ const totalTimer = setTimeout(() => onFatal(new TransportTimeoutError("上游总超时")), totalTimeoutMs);
341
+ timers.add(totalTimer);
342
+ req = requestImpl({
343
+ protocol: target.protocol,
344
+ hostname: target.hostname,
345
+ port: target.port || (isHttps ? 443 : 80),
346
+ path: `${target.pathname}${target.search}`,
347
+ method: options.method ?? "GET",
348
+ headers,
349
+ agent,
350
+ }, (res) => {
351
+ const location = res.headers.location;
352
+ const status = res.statusCode ?? 0;
353
+ const isRedirect = status >= 300 && status < 400 && typeof location === "string";
354
+ if (isRedirect) {
355
+ // Drain and discard the redirect body, keep the connection tidy.
356
+ res.resume();
357
+ if (!settled) {
358
+ settled = true;
359
+ timerBag.clearAll();
360
+ if (options.signal)
361
+ options.signal.removeEventListener("abort", onAbort);
362
+ cleanupAgent();
363
+ const redirectResponse = new NodeTransportResponse(status, res.statusMessage ?? "", toHeaders(res.headers), { res, armIdle: () => undefined, finalize: () => undefined });
364
+ resolve({
365
+ type: "redirect",
366
+ location: location,
367
+ response: redirectResponse,
368
+ });
369
+ }
370
+ return;
371
+ }
372
+ // Pause until a consumer (stream or buffered) attaches, so no chunk is
373
+ // lost between headers and consumption.
374
+ res.pause();
375
+ let idleTimer = null;
376
+ let finalized = false;
377
+ const armIdle = () => {
378
+ if (idleTimer) {
379
+ clearTimeout(idleTimer);
380
+ timers.delete(idleTimer);
381
+ }
382
+ idleTimer = setTimeout(() => bodyController?.fail(new TransportTimeoutError("上游空闲超时")), idleTimeoutMs);
383
+ timers.add(idleTimer);
384
+ };
385
+ const finalize = () => {
386
+ if (finalized)
387
+ return;
388
+ finalized = true;
389
+ timerBag.clearAll();
390
+ if (options.signal)
391
+ options.signal.removeEventListener("abort", onAbort);
392
+ cleanupAgent();
393
+ };
394
+ const response = new NodeTransportResponse(status, res.statusMessage ?? "", toHeaders(res.headers), { res, maxResponseBytes, armIdle, finalize });
395
+ bodyController = { fail: (err) => response.fail(err) };
396
+ armIdle();
397
+ if (!settled) {
398
+ settled = true;
399
+ resolve({ type: "response", response });
400
+ }
401
+ });
402
+ req.setTimeout(connectTimeoutMs, () => {
403
+ fail(new TransportTimeoutError("上游连接超时"));
404
+ });
405
+ req.on("error", (err) => fail(err));
406
+ if (bodyBuffer)
407
+ req.write(bodyBuffer);
408
+ req.end();
409
+ });
410
+ }
411
+ function normalizeBody(body) {
412
+ if (body == null)
413
+ return null;
414
+ if (Buffer.isBuffer(body))
415
+ return body;
416
+ if (typeof body === "string")
417
+ return Buffer.from(body, "utf8");
418
+ return Buffer.from(body);
419
+ }
420
+ function toHeaders(raw) {
421
+ const headers = new Headers();
422
+ for (const [key, value] of Object.entries(raw)) {
423
+ if (value === undefined)
424
+ continue;
425
+ if (Array.isArray(value)) {
426
+ for (const item of value)
427
+ headers.append(key, item);
428
+ }
429
+ else {
430
+ headers.set(key, value);
431
+ }
432
+ }
433
+ return headers;
434
+ }
435
+ function abortError() {
436
+ const err = new Error("请求已取消");
437
+ err.name = "AbortError";
438
+ return err;
439
+ }
package/dist/cli.js CHANGED
@@ -3,13 +3,16 @@ import { TOOLS } from "./types.js";
3
3
  import { registerToolCommand } from "./commands/tool.js";
4
4
  import { registerLaunchCommand } from "./commands/launch-cmd.js";
5
5
  import { registerBridgeCommand } from "./commands/bridge-cmd.js";
6
+ import { registerSetupCommand } from "./commands/setup-cmd.js";
7
+ import { registerHomeCommand } from "./commands/home-cmd.js";
6
8
  import { getAppConfigRoot } from "./utils/paths.js";
9
+ import { getVersion } from "./utils/version.js";
7
10
  export function createProgram() {
8
11
  const program = new Command();
9
12
  program
10
13
  .name("llms")
11
14
  .description("为 Claude Code / Codex / OpenCode 切换供应商、模型与上游代理")
12
- .version("0.2.0")
15
+ .version(getVersion())
13
16
  .option("--json", "部分命令支持 JSON 输出(见子命令)");
14
17
  program
15
18
  .command("path")
@@ -19,12 +22,15 @@ export function createProgram() {
19
22
  });
20
23
  registerLaunchCommand(program);
21
24
  registerBridgeCommand(program);
25
+ registerSetupCommand(program);
22
26
  for (const tool of TOOLS) {
23
27
  registerToolCommand(program, tool);
24
28
  }
29
+ registerHomeCommand(program);
25
30
  program.configureOutput({
26
31
  writeErr: (str) => process.stderr.write(str),
27
32
  });
33
+ program.addHelpText("afterAll", "\n反馈与建议:rchdg50@gmail.com\nGitHub Issues:https://github.com/rchdg/llmswitch/issues\n");
28
34
  return program;
29
35
  }
30
36
  export async function run(argv = process.argv) {
@@ -1,7 +1,8 @@
1
- import { ensureBridgeForProfile, isBridgeAlive, isPidRunning, readPid, runBridgeForeground, startBridgeDaemon, stopBridge, upstreamFromProfile, } from "../bridge/manager.js";
2
- import { bridgeBaseUrl, bridgeRootUrl, readBridgeState, writeBridgeUpstream, } from "../bridge/state.js";
1
+ import { ensureBridgeForProfile, isBridgeAlive, isPidRunning, readPid, runBridgeForeground, startBridgeDaemon, stopBridge, } from "../bridge/manager.js";
2
+ import { bridgeBaseUrl, bridgeRootUrl, readBridgeState, } from "../bridge/state.js";
3
+ import { parseBridgePort } from "../bridge/runtime.js";
3
4
  import { DEFAULT_BRIDGE_HOST, DEFAULT_BRIDGE_PORT } from "../bridge/types.js";
4
- import { getActiveProfile, requireProfile } from "../store/profiles.js";
5
+ import { getActiveProfile, resolveProfileOrThrow } from "../store/profiles.js";
5
6
  import { isTool } from "../types.js";
6
7
  export function registerBridgeCommand(program) {
7
8
  const bridge = program
@@ -12,25 +13,27 @@ export function registerBridgeCommand(program) {
12
13
  .description("前台运行 bridge(守护进程由 use/launch 自动拉起)")
13
14
  .option("--host <host>", "监听地址", DEFAULT_BRIDGE_HOST)
14
15
  .option("--port <port>", "监听端口", String(DEFAULT_BRIDGE_PORT))
16
+ .option("--allow-remote", "允许非回环监听(仍强制认证和限制)")
15
17
  .action(async (opts) => {
16
- const port = Number(opts.port) || DEFAULT_BRIDGE_PORT;
17
- await runBridgeForeground(opts.host || DEFAULT_BRIDGE_HOST, port);
18
+ const port = parseBridgePort(opts.port);
19
+ await runBridgeForeground(opts.host || DEFAULT_BRIDGE_HOST, port, Boolean(opts.allowRemote));
18
20
  });
19
21
  bridge
20
22
  .command("start")
21
23
  .description("后台启动 bridge")
22
24
  .option("--host <host>", "监听地址", DEFAULT_BRIDGE_HOST)
23
25
  .option("--port <port>", "监听端口", String(DEFAULT_BRIDGE_PORT))
26
+ .option("--allow-remote", "允许非回环监听(仍强制认证和限制)")
24
27
  .action(async (opts) => {
25
28
  const host = opts.host || DEFAULT_BRIDGE_HOST;
26
- const port = Number(opts.port) || DEFAULT_BRIDGE_PORT;
27
- const pid = await startBridgeDaemon(host, port);
29
+ const port = parseBridgePort(opts.port);
30
+ const pid = await startBridgeDaemon(host, port, Boolean(opts.allowRemote));
28
31
  for (let i = 0; i < 30; i++) {
29
- if (await isBridgeAlive(host, port))
32
+ if (await isBridgeAlive())
30
33
  break;
31
34
  await new Promise((r) => setTimeout(r, 100));
32
35
  }
33
- if (!(await isBridgeAlive(host, port))) {
36
+ if (!(await isBridgeAlive())) {
34
37
  throw new Error("bridge 启动失败,请尝试:llms bridge serve");
35
38
  }
36
39
  console.log(`bridge 已启动 pid=${pid} ${bridgeRootUrl()}`);
@@ -101,13 +104,12 @@ export function registerBridgeCommand(program) {
101
104
  }
102
105
  const tool = toolName;
103
106
  const profile = opts.profile
104
- ? requireProfile(tool, opts.profile)
107
+ ? resolveProfileOrThrow(tool, opts.profile)
105
108
  : getActiveProfile(tool);
106
109
  if (!profile) {
107
110
  throw new Error(`没有可用的 ${tool} profile`);
108
111
  }
109
- writeBridgeUpstream(tool, upstreamFromProfile(profile, tool));
110
- const base = await ensureBridgeForProfile(profile, tool);
111
- console.log(`已刷新 ${tool} 上游 ${profile.name} → bridge ${base}`);
112
+ const connection = await ensureBridgeForProfile(profile, tool);
113
+ console.log(`已刷新 ${tool} 上游 ${profile.name} → bridge ${connection.baseUrl}`);
112
114
  });
113
115
  }
@@ -0,0 +1,8 @@
1
+ import { pickSetupTool, runToolFlow } from "./setup-cmd.js";
2
+ /** 无子命令时:选择工具 → 连贯启动(未配置则自动引导)。 */
3
+ export function registerHomeCommand(program) {
4
+ program.action(async () => {
5
+ const tool = await pickSetupTool();
6
+ await runToolFlow(tool);
7
+ });
8
+ }
@@ -1,5 +1,8 @@
1
+ import * as p from "@clack/prompts";
1
2
  import { isTool, TOOLS } from "../types.js";
3
+ import { listProfiles } from "../store/profiles.js";
2
4
  import { launchTool, resolveBinary, resolveLaunchTarget, } from "./launch.js";
5
+ import { runSetupWizard } from "./setup-cmd.js";
3
6
  /**
4
7
  * ollama-like quick start:
5
8
  * llms launch codex --model gpt-4.1
@@ -23,6 +26,21 @@ export function registerLaunchCommand(program) {
23
26
  if (!isTool(toolArg)) {
24
27
  throw new Error(`未知工具「${toolArg}」。可选:${TOOLS.join(", ")}`);
25
28
  }
29
+ if (listProfiles(toolArg).length === 0 &&
30
+ process.stdin.isTTY &&
31
+ !opts.dryRun) {
32
+ const setup = await p.confirm({
33
+ message: `${toolArg} 尚未配置供应商,是否进入引导?`,
34
+ initialValue: true,
35
+ });
36
+ if (p.isCancel(setup)) {
37
+ p.cancel("已取消");
38
+ process.exit(0);
39
+ }
40
+ if (setup) {
41
+ await runSetupWizard(toolArg);
42
+ }
43
+ }
26
44
  let model = opts.model?.trim() || undefined;
27
45
  const queue = [...parts];
28
46
  if (!model && queue[0] && !queue[0].startsWith("-")) {
@@ -3,7 +3,7 @@ import { existsSync } from "node:fs";
3
3
  import { delimiter, join } from "node:path";
4
4
  import { isTool } from "../types.js";
5
5
  import { applyProfile } from "../adapters/index.js";
6
- import { getActiveProfile, getDefaultProfile, listProfiles, requireProfile, saveProfile, ensureDefaultProvider, } from "../store/profiles.js";
6
+ import { getActiveProfile, getDefaultProfile, listProfiles, requireProfile, resolveProfileOrThrow, saveProfile, ensureDefaultProvider, } from "../store/profiles.js";
7
7
  const BINARY = {
8
8
  claude: "claude",
9
9
  codex: "codex",
@@ -75,7 +75,7 @@ export function resolveLaunchTarget(tool, opts) {
75
75
  const modelQuery = opts.model?.trim() || undefined;
76
76
  let profile = null;
77
77
  if (opts.profile) {
78
- profile = requireProfile(tool, opts.profile);
78
+ profile = resolveProfileOrThrow(tool, opts.profile);
79
79
  }
80
80
  if (!profile && modelQuery) {
81
81
  profile = findProfileForModel(profiles, modelQuery);