@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.
- package/README.md +31 -2
- package/dist/cli.js +110 -1
- package/dist/codex-accounts.d.ts +60 -0
- package/dist/codex-accounts.js +260 -0
- package/dist/codex-backend.d.ts +107 -0
- package/dist/codex-backend.js +455 -0
- package/dist/codex-oauth.d.ts +31 -0
- package/dist/codex-oauth.js +114 -0
- package/dist/model-catalog.d.ts +9 -2
- package/dist/model-catalog.js +10 -3
- package/dist/provider-adapter.d.ts +24 -3
- package/dist/provider-adapter.js +34 -4
- package/dist/proxy.d.ts +10 -4
- package/dist/proxy.js +126 -26
- package/package.json +1 -1
|
@@ -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
|
|
19
|
-
* is a consolidation of the existing decision, not a
|
|
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
|
package/dist/provider-adapter.js
CHANGED
|
@@ -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
|
|
19
|
-
* is a consolidation of the existing decision, not a
|
|
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,
|
|
259
|
-
* --no-claude-auth
|
|
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,
|
|
895
|
-
* --no-claude-auth
|
|
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
|
-
|
|
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
|
-
//
|
|
2246
|
-
//
|
|
2247
|
-
//
|
|
2248
|
-
//
|
|
2249
|
-
//
|
|
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
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
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
|
|
2266
|
-
//
|
|
2267
|
-
//
|
|
2268
|
-
//
|
|
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.
|
|
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": {
|