@askalf/dario 5.5.83 → 5.5.85

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.
@@ -15,10 +15,11 @@
15
15
  * Claude adapter the whole proxy and the OpenAI adapter nearly empty. The seam
16
16
  * that pays for itself is routing + request-shaping; the rest is shared.
17
17
  *
18
- * The adapters reuse the same primitive proxy.ts uses (`isOpenAIModel`), so this
19
- * is a consolidation of the existing decision, not a re-derivation of it.
18
+ * The adapters reuse the same primitives proxy.ts uses (`isOpenAIModel`,
19
+ * `isCodexModel`), so this is a consolidation of the existing decision, not a
20
+ * re-derivation of it.
20
21
  */
21
- export type ProviderId = 'claude' | 'openai';
22
+ export type ProviderId = 'claude' | 'openai' | 'codex';
22
23
  /** Inputs the routing decision needs, computed once per request. */
23
24
  export interface RouteContext {
24
25
  /** urlPath === '/v1/chat/completions' (OpenAI chat shape). */
@@ -29,6 +30,10 @@ export interface RouteContext {
29
30
  forcedProvider: ProviderId | null;
30
31
  /** An openai-compat backend is configured (`dario backend add …`). */
31
32
  hasOpenAIBackend: boolean;
33
+ /** At least one Codex/ChatGPT-subscription account is stored (`dario codex add …`). */
34
+ hasCodexAccount: boolean;
35
+ /** Model slugs the Codex backend lists for the selected account (discovered, cached). */
36
+ codexModels: readonly string[];
32
37
  /** `--pool-fallback=<model>` value, or null when disabled. */
33
38
  poolFallbackModel: string | null;
34
39
  /** Live pool account count. */
@@ -49,6 +54,22 @@ export interface ProviderAdapter {
49
54
  /** True if this adapter should PRIMARILY handle the request. */
50
55
  claimsPrimary(ctx: RouteContext): boolean;
51
56
  }
57
+ /**
58
+ * Codex/ChatGPT-subscription adapter — the "altman" engine (dario#1009).
59
+ * Claims an OpenAI-shape request when a Codex account is stored and either the
60
+ * model carries a `codex:`/`chatgpt:` prefix or its name is one the backend
61
+ * itself listed for that account (`codexModels`, discovered by
62
+ * codex-backend.ts — the slugs are per-account and move, so they are never
63
+ * hardcoded here). Ranks above the openai adapter so a listed slug reaches the
64
+ * subscription even when an API-key backend is also configured; anything not
65
+ * listed — `gpt-4o` and friends — is untouched and still lands on that backend.
66
+ *
67
+ * OpenAI-path only: the ChatGPT backend speaks Responses, and dario's
68
+ * chat/completions⇄Responses translation (codex-backend.ts) is written against
69
+ * the OpenAI request shape. An Anthropic-shape /v1/messages request would need
70
+ * a second translation that doesn't exist, so it stays with Claude.
71
+ */
72
+ export declare const codexAdapter: ProviderAdapter;
52
73
  /**
53
74
  * OpenAI-compat backend adapter. Claims a request under exactly the condition
54
75
  * the request handler reroutes on: a configured backend, an OpenAI-shape
@@ -15,10 +15,40 @@
15
15
  * Claude adapter the whole proxy and the OpenAI adapter nearly empty. The seam
16
16
  * that pays for itself is routing + request-shaping; the rest is shared.
17
17
  *
18
- * The adapters reuse the same primitive proxy.ts uses (`isOpenAIModel`), so this
19
- * is a consolidation of the existing decision, not a re-derivation of it.
18
+ * The adapters reuse the same primitives proxy.ts uses (`isOpenAIModel`,
19
+ * `isCodexModel`), so this is a consolidation of the existing decision, not a
20
+ * re-derivation of it.
20
21
  */
21
22
  import { isOpenAIModel } from './openai-backend.js';
23
+ import { isCodexModel } from './codex-backend.js';
24
+ /**
25
+ * Codex/ChatGPT-subscription adapter — the "altman" engine (dario#1009).
26
+ * Claims an OpenAI-shape request when a Codex account is stored and either the
27
+ * model carries a `codex:`/`chatgpt:` prefix or its name is one the backend
28
+ * itself listed for that account (`codexModels`, discovered by
29
+ * codex-backend.ts — the slugs are per-account and move, so they are never
30
+ * hardcoded here). Ranks above the openai adapter so a listed slug reaches the
31
+ * subscription even when an API-key backend is also configured; anything not
32
+ * listed — `gpt-4o` and friends — is untouched and still lands on that backend.
33
+ *
34
+ * OpenAI-path only: the ChatGPT backend speaks Responses, and dario's
35
+ * chat/completions⇄Responses translation (codex-backend.ts) is written against
36
+ * the OpenAI request shape. An Anthropic-shape /v1/messages request would need
37
+ * a second translation that doesn't exist, so it stays with Claude.
38
+ */
39
+ export const codexAdapter = {
40
+ id: 'codex',
41
+ priority: 200,
42
+ claimsPrimary(ctx) {
43
+ if (!ctx.hasCodexAccount)
44
+ return false;
45
+ if (!ctx.isOpenAIPath)
46
+ return false;
47
+ if (ctx.forcedProvider === 'claude' || ctx.forcedProvider === 'openai')
48
+ return false;
49
+ return ctx.forcedProvider === 'codex' || isCodexModel(ctx.model, ctx.codexModels);
50
+ },
51
+ };
22
52
  /**
23
53
  * OpenAI-compat backend adapter. Claims a request under exactly the condition
24
54
  * the request handler reroutes on: a configured backend, an OpenAI-shape
@@ -33,7 +63,7 @@ export const openaiAdapter = {
33
63
  return false;
34
64
  if (!ctx.isOpenAIPath)
35
65
  return false;
36
- if (ctx.forcedProvider === 'claude')
66
+ if (ctx.forcedProvider === 'claude' || ctx.forcedProvider === 'codex')
37
67
  return false;
38
68
  return ctx.forcedProvider === 'openai' || isOpenAIModel(ctx.model);
39
69
  },
@@ -51,7 +81,7 @@ export const claudeAdapter = {
51
81
  return true;
52
82
  },
53
83
  };
54
- export const DEFAULT_ADAPTERS = [openaiAdapter, claudeAdapter];
84
+ export const DEFAULT_ADAPTERS = [codexAdapter, openaiAdapter, claudeAdapter];
55
85
  /**
56
86
  * Resolve the routing decision. Offers the request to adapters in priority
57
87
  * order and takes the first primary claim; the Claude adapter always claims, so
package/dist/proxy.d.ts CHANGED
@@ -78,7 +78,7 @@ export declare function selectModelOverride(incomingModel: string, modelOverride
78
78
  */
79
79
  export declare function buildPoolFallbackBody(body: Buffer, fallbackModel: string): Buffer | null;
80
80
  export declare function parseProviderPrefix(model: string): {
81
- provider: 'openai' | 'claude';
81
+ provider: 'openai' | 'claude' | 'codex';
82
82
  model: string;
83
83
  } | null;
84
84
  /**
@@ -255,10 +255,16 @@ export declare const OPENAI_MODELS_LIST: {
255
255
  /**
256
256
  * Whether dario must have a Claude login to start. False (an empty pool is
257
257
  * expected, not a fatal "run dario login") for the modes that serve requests
258
- * without the Claude OAuth pool: admin-bootstrap, upstream-api-key, and
259
- * --no-claude-auth. Pure so the startup gate is unit-testable.
258
+ * without the Claude OAuth pool: admin-bootstrap, upstream-api-key,
259
+ * --no-claude-auth, and a stored Codex/ChatGPT-subscription account.
260
+ *
261
+ * The codex case (dario#1137) is the one a user hits without asking for it: a
262
+ * ChatGPT-only user has stored an account and has real capacity to serve
263
+ * requests, so demanding a Claude login — or a `--no-claude-auth` flag they
264
+ * have no reason to know about — is dario refusing to start over a credential
265
+ * it will never use. Pure so the startup gate is unit-testable.
260
266
  */
261
- export declare function requiresClaudeLogin(poolSize: number, adminEnabled: boolean, hasUpstreamApiKey: boolean, noClaudeAuth: boolean): boolean;
267
+ export declare function requiresClaudeLogin(poolSize: number, adminEnabled: boolean, hasUpstreamApiKey: boolean, noClaudeAuth: boolean, hasCodexAccount?: boolean): boolean;
262
268
  interface ProxyOptions {
263
269
  port?: number;
264
270
  host?: string;
package/dist/proxy.js CHANGED
@@ -20,6 +20,8 @@ import { loadAllAccounts, loadAccount, saveAccount, refreshAccountToken, resyncL
20
20
  import { handleAdminRequest } from './admin-api.js';
21
21
  import { createTokenBucket } from './rate-limit.js';
22
22
  import { getOpenAIBackend, isOpenAIModel, forwardToOpenAI } from './openai-backend.js';
23
+ import { forwardToCodex, getCodexModelSlugs, CODEX_BACKEND_BASE_URL } from './codex-backend.js';
24
+ import { listCodexAccountAliases, hasAnyCodexAccount, selectCodexAccount, getFreshCodexAccount } from './codex-accounts.js';
23
25
  import { route as routeProvider } from './provider-adapter.js';
24
26
  import { RequestQueue, QueueFullError, QueueTimeoutError, DEFAULT_MAX_CONCURRENT, DEFAULT_MAX_QUEUED, DEFAULT_QUEUE_TIMEOUT_MS } from './request-queue.js';
25
27
  import { redactSecrets } from './redact.js';
@@ -312,6 +314,8 @@ const PROVIDER_PREFIXES = {
312
314
  groq: 'openai',
313
315
  compat: 'openai',
314
316
  local: 'openai',
317
+ codex: 'codex',
318
+ chatgpt: 'codex',
315
319
  claude: 'claude',
316
320
  anthropic: 'claude',
317
321
  };
@@ -891,11 +895,17 @@ export const OPENAI_MODELS_LIST = buildOpenAIModelsList(withLongContextVariants(
891
895
  /**
892
896
  * Whether dario must have a Claude login to start. False (an empty pool is
893
897
  * expected, not a fatal "run dario login") for the modes that serve requests
894
- * without the Claude OAuth pool: admin-bootstrap, upstream-api-key, and
895
- * --no-claude-auth. Pure so the startup gate is unit-testable.
898
+ * without the Claude OAuth pool: admin-bootstrap, upstream-api-key,
899
+ * --no-claude-auth, and a stored Codex/ChatGPT-subscription account.
900
+ *
901
+ * The codex case (dario#1137) is the one a user hits without asking for it: a
902
+ * ChatGPT-only user has stored an account and has real capacity to serve
903
+ * requests, so demanding a Claude login — or a `--no-claude-auth` flag they
904
+ * have no reason to know about — is dario refusing to start over a credential
905
+ * it will never use. Pure so the startup gate is unit-testable.
896
906
  */
897
- export function requiresClaudeLogin(poolSize, adminEnabled, hasUpstreamApiKey, noClaudeAuth) {
898
- return poolSize === 0 && !adminEnabled && !hasUpstreamApiKey && !noClaudeAuth;
907
+ export function requiresClaudeLogin(poolSize, adminEnabled, hasUpstreamApiKey, noClaudeAuth, hasCodexAccount = false) {
908
+ return poolSize === 0 && !adminEnabled && !hasUpstreamApiKey && !noClaudeAuth && !hasCodexAccount;
899
909
  }
900
910
  /**
901
911
  * Append a JSON-ND line to the proxy log file. No-op when stream is
@@ -1211,6 +1221,18 @@ export async function startProxy(opts = {}) {
1211
1221
  if (openaiBackend) {
1212
1222
  console.log(` OpenAI-compat backend: ${openaiBackend.name} → ${openaiBackend.baseUrl}`);
1213
1223
  }
1224
+ // Codex/ChatGPT-subscription accounts — the "altman" engine (dario#1009).
1225
+ // This startup probe only decides what to PRINT and whether a Claude login
1226
+ // is required to boot; routing re-asks hasAnyCodexAccount() per request
1227
+ // (dario#1138), so an account stored by `dario codex add` against an
1228
+ // already-running proxy is picked up without a restart — `dario login`
1229
+ // restarts the proxy by convention, `dario codex add` does not. The
1230
+ // credentials themselves are re-read per request too (they rotate on
1231
+ // refresh).
1232
+ const startupCodexAliases = await listCodexAccountAliases();
1233
+ if (startupCodexAliases.length > 0) {
1234
+ console.log(` Codex accounts: ${startupCodexAliases.join(', ')} → ${CODEX_BACKEND_BASE_URL}`);
1235
+ }
1214
1236
  // Pool-exhausted fallback (strictly opt-in). When the Claude pool can't
1215
1237
  // serve — every seat rate-limited or in auth cool-down — OpenAI-shape
1216
1238
  // requests (/v1/chat/completions) are re-pointed at the configured
@@ -1395,13 +1417,15 @@ export async function startProxy(opts = {}) {
1395
1417
  // - admin bootstrap (#599): starts empty, returns a clean 503 until an
1396
1418
  // account is added over the admin API;
1397
1419
  // - upstream-api-key mode: OAuth + pool are bypassed, requests carry
1398
- // x-api-key, so an empty pool is expected.
1420
+ // x-api-key, so an empty pool is expected;
1421
+ // - a stored Codex/ChatGPT-subscription account (dario#1137): that user
1422
+ // never had a Claude login and doesn't need one to be served.
1399
1423
  // Otherwise an empty pool means the login back-fill found no credentials —
1400
1424
  // the user hasn't logged in. Preserve the single-account self-heal there: a
1401
1425
  // dead-but-refreshable token (a container restarted right after a normal
1402
1426
  // expiry, gap #1) is refreshed, then back-filled so the recovered login
1403
1427
  // becomes the pool-of-one it should be — rather than crash-looping on exit(1).
1404
- if (requiresClaudeLogin(pool.size, adminEnabled, !!upstreamApiKey, opts.noClaudeAuth ?? false)) {
1428
+ if (requiresClaudeLogin(pool.size, adminEnabled, !!upstreamApiKey, opts.noClaudeAuth ?? false, startupCodexAliases.length > 0)) {
1405
1429
  const single = await resolveSingleAccountStartupStatus();
1406
1430
  if (!single.authenticated) {
1407
1431
  console.error('[dario] Not authenticated. Run `dario login` first.');
@@ -2143,7 +2167,23 @@ export async function startProxy(opts = {}) {
2143
2167
  // shadows a real id still applies at request time).
2144
2168
  const advertised = withLongContextVariants(catalog.bases);
2145
2169
  const aliasNames = Object.keys(modelAliases).filter((n) => !advertised.includes(n));
2146
- const body = JSON.stringify(buildOpenAIModelsList(advertised.concat(aliasNames)));
2170
+ // Codex/ChatGPT-subscription slugs the backend lists for the stored
2171
+ // account (dario#1010), so a client's model picker can discover the
2172
+ // models that account may actually use. Discovery is cached and never
2173
+ // throws, so /v1/models keeps its "always answers" property.
2174
+ let codexNames = [];
2175
+ if (await hasAnyCodexAccount()) {
2176
+ const stored = await selectCodexAccount();
2177
+ if (stored) {
2178
+ const creds = await getFreshCodexAccount(stored).catch(() => stored);
2179
+ codexNames = (await getCodexModelSlugs(creds))
2180
+ .filter((s) => !advertised.includes(s) && !aliasNames.includes(s));
2181
+ }
2182
+ }
2183
+ // Codex slugs are served by the ChatGPT subscription, not Anthropic, so
2184
+ // they advertise `owned_by: "openai"` (dario#1137).
2185
+ const codexOwners = Object.fromEntries(codexNames.map((s) => [s, 'openai']));
2186
+ const body = JSON.stringify(buildOpenAIModelsList(advertised.concat(aliasNames, codexNames), codexOwners));
2147
2187
  res.writeHead(200, { ...JSON_HEADERS, 'Access-Control-Allow-Origin': corsOrigin });
2148
2188
  res.end(body);
2149
2189
  return;
@@ -2242,30 +2282,46 @@ export async function startProxy(opts = {}) {
2242
2282
  // the first version of this forwarded nothing (dario#885).
2243
2283
  let genuineCCRequest = false;
2244
2284
  try {
2245
- // Select an account by headroom (v5.0: the pool is the one credential
2246
- // model, so every OAuth request selects from it — a plain `dario login`
2247
- // is a pool of one). Upstream-api-key mode is the sole path with no pool
2248
- // account. Inside-request 429/auth failover retries the next-best account
2249
- // before surfacing an error to the client (see the dispatch loop below).
2285
+ // Account selection is DEFERRED to `selectPoolAccount()` below, after the
2286
+ // provider decision (dario#1137). Selecting here meant the empty-pool 503
2287
+ // fired BEFORE the codex adapter was ever consulted, so a
2288
+ // ChatGPT-subscription-only user whose empty Claude pool is legitimate,
2289
+ // not a setup error got "No account configured" for every request that
2290
+ // should have gone to their subscription. A pool account is now required
2291
+ // only once the routing block has declined the request, i.e. when it
2292
+ // really is Claude's. Inside-request 429/auth failover retries the
2293
+ // next-best account before surfacing an error (see the dispatch loop).
2294
+ // `null as PoolAccount | null` rather than a `: PoolAccount | null` annotation:
2295
+ // every assignment happens inside the selectPoolAccount closure below, so
2296
+ // control-flow analysis narrows the annotated form to `null` for the whole
2297
+ // rest of the handler and `poolAccount?.alias` fails to compile. The `as`
2298
+ // form gives the declared type without seeding a narrowing.
2250
2299
  let poolAccount = null;
2251
- let accessToken;
2252
- if (upstreamApiKey) {
2253
- // Per-token API-key mode: no OAuth, no pool selection. `poolAccount`
2254
- // stays null, so every pool-failover retry below is skipped; the
2255
- // x-api-key is set on the outbound headers instead of a Bearer.
2256
- accessToken = '';
2257
- }
2258
- else {
2300
+ let accessToken = '';
2301
+ /**
2302
+ * Take a Claude pool account for this request. Returns false when it has
2303
+ * already answered the client (empty or fully drained pool with no viable
2304
+ * fallback) the caller must return immediately. Leaves `poolAccount`
2305
+ * null without answering in the pool-exhausted-fallback case, which the
2306
+ * fallback dispatch right below picks up.
2307
+ */
2308
+ const selectPoolAccount = () => {
2309
+ if (upstreamApiKey) {
2310
+ // Per-token API-key mode: no OAuth, no pool selection. `poolAccount`
2311
+ // stays null, so every pool-failover retry below is skipped; the
2312
+ // x-api-key is set on the outbound headers instead of a Bearer.
2313
+ return true;
2314
+ }
2259
2315
  // Pool is the one credential model (v5.0): a plain `dario login` is a
2260
2316
  // pool of one, so every OAuth request selects from the pool.
2261
2317
  poolAccount = pool.select();
2262
2318
  if (!poolAccount) {
2263
2319
  // Pool-exhausted fallback: when armed, the pool HAS accounts (all
2264
2320
  // drained / cooling), and the client speaks OpenAI shape, defer —
2265
- // the fallback dispatch below the body read re-points the request
2266
- // at the openai-compat backend. An EMPTY pool still 503s: that's
2267
- // a setup error the operator needs to see, not traffic to quietly
2268
- // re-bill somewhere else.
2321
+ // the fallback dispatch below re-points the request at the
2322
+ // openai-compat backend. An EMPTY pool still 503s: that's a setup
2323
+ // error the operator needs to see, not traffic to quietly re-bill
2324
+ // somewhere else.
2269
2325
  const fallbackViable = poolFallbackModel !== null && openaiBackend !== null
2270
2326
  && isOpenAI && pool.size > 0;
2271
2327
  if (!fallbackViable) {
@@ -2286,11 +2342,12 @@ export async function startProxy(opts = {}) {
2286
2342
  error: 'No accounts available in pool',
2287
2343
  message: 'all accounts are rate-limited or in auth cool-down; retry shortly',
2288
2344
  }));
2289
- return;
2345
+ return false;
2290
2346
  }
2291
2347
  }
2292
2348
  accessToken = poolAccount?.accessToken ?? '';
2293
- }
2349
+ return true;
2350
+ };
2294
2351
  // Client-side session key (constant per request) for the rotation registry
2295
2352
  // — consulted at body-build, at the outbound header, and on each mid-request
2296
2353
  // failover rewrite so all three agree on the selected account's session.
@@ -2418,18 +2475,54 @@ export async function startProxy(opts = {}) {
2418
2475
  // (forcedProvider === 'openai' || isOpenAIModel(model))`), consolidated so
2419
2476
  // the routing rule is testable and lives in one place. `openaiBackend`
2420
2477
  // stays in the guard for TS narrowing (route already implies it non-null).
2478
+ //
2479
+ // `codex` joins as a third provider (dario#1009/#1010): an OpenAI-shape
2480
+ // request naming a model the ChatGPT backend LISTS for the stored account
2481
+ // (or carrying a `codex:`/`chatgpt:` prefix) is served from that
2482
+ // subscription, via the chat/completions⇄Responses translation in
2483
+ // codex-backend.ts. It ranks above the openai adapter so a listed slug
2484
+ // reaches the subscription even when an API-key backend is configured too.
2421
2485
  if (body.length > 0) {
2422
2486
  try {
2423
2487
  const peek = JSON.parse(body.toString());
2424
2488
  const rawModel = (peek.model || '').toString();
2489
+ // Credentials are re-read per request (not cached at startup) because
2490
+ // a refresh rotates them on disk; getFreshCodexAccount refreshes when
2491
+ // inside the expiry buffer, collapsing concurrent refreshes per alias.
2492
+ // Resolved BEFORE routing because the routable model set is whatever
2493
+ // THIS account's backend lists — discovery is cached, so the common
2494
+ // case is a map lookup, not a request.
2495
+ let codexCreds = null;
2496
+ let codexModels = [];
2497
+ // Presence is re-asked here rather than read from a startup flag so
2498
+ // an account added while the proxy runs routes on the very next
2499
+ // request; the absent answer is cached ~30s, so an idle proxy with
2500
+ // no codex account is not stat-ing the filesystem per request.
2501
+ if (isOpenAI && await hasAnyCodexAccount()) {
2502
+ const stored = await selectCodexAccount();
2503
+ if (stored) {
2504
+ codexCreds = await getFreshCodexAccount(stored);
2505
+ codexModels = await getCodexModelSlugs(codexCreds);
2506
+ }
2507
+ }
2425
2508
  const decision = routeProvider({
2426
2509
  isOpenAIPath: isOpenAI,
2427
2510
  model: rawModel,
2428
2511
  forcedProvider,
2429
2512
  hasOpenAIBackend: openaiBackend !== null,
2513
+ hasCodexAccount: codexCreds !== null,
2514
+ codexModels,
2430
2515
  poolFallbackModel,
2431
2516
  poolSize: pool.size,
2432
2517
  });
2518
+ if (rawModel && codexCreds && decision.provider === 'codex') {
2519
+ if (verbose) {
2520
+ console.log(`[dario] #${requestCount} ${req.method} ${urlPath} (model: ${rawModel}) → codex account ${codexCreds.alias}`);
2521
+ }
2522
+ requestCount++;
2523
+ await forwardToCodex(req, res, body, codexCreds, corsOrigin, SECURITY_HEADERS, upstreamTimeoutMs, verbose);
2524
+ return;
2525
+ }
2433
2526
  if (rawModel && openaiBackend && decision.provider === 'openai') {
2434
2527
  if (verbose) {
2435
2528
  console.log(`[dario] #${requestCount} ${req.method} ${urlPath} (model: ${rawModel}) → openai backend`);
@@ -2441,6 +2534,13 @@ export async function startProxy(opts = {}) {
2441
2534
  }
2442
2535
  catch { /* not JSON — fall through to existing path */ }
2443
2536
  }
2537
+ // Claude's turn: the routing block above declined this request, so it
2538
+ // needs a pool account. Selecting HERE and not before the body read is
2539
+ // the fix for dario#1137 — a ChatGPT-subscription-only user has a
2540
+ // legitimately empty Claude pool, and the empty-pool 503 used to answer
2541
+ // codex-bound requests before the adapter was ever consulted.
2542
+ if (!selectPoolAccount())
2543
+ return;
2444
2544
  // Pool-exhausted fallback dispatch. In OAuth mode poolAccount can only
2445
2545
  // be null here when the selection above deferred to this path (armed
2446
2546
  // fallback + drained pool + OpenAI-shape request): swap the model and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "5.5.83",
3
+ "version": "5.5.85",
4
4
  "description": "Use your Claude Pro/Max subscription in any tool — Cursor, Cline, Aider, the Agent SDK, your scripts — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint.",
5
5
  "type": "module",
6
6
  "bin": {