@askalf/dario 5.5.82 → 5.5.84
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/cc-template-data.json +3 -3
- package/dist/cli.js +108 -1
- package/dist/codex-accounts.d.ts +57 -0
- package/dist/codex-accounts.js +227 -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 +123 -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, 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,14 @@ 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
|
+
// Loaded once at startup the same way the openai-compat backend is; the
|
|
1226
|
+
// credentials themselves are re-read per request (they rotate on refresh),
|
|
1227
|
+
// this only answers "is the codex route available at all" for routing.
|
|
1228
|
+
let hasCodexAccount = (await listCodexAccountAliases()).length > 0;
|
|
1229
|
+
if (hasCodexAccount) {
|
|
1230
|
+
console.log(` Codex accounts: ${(await listCodexAccountAliases()).join(', ')} → ${CODEX_BACKEND_BASE_URL}`);
|
|
1231
|
+
}
|
|
1214
1232
|
// Pool-exhausted fallback (strictly opt-in). When the Claude pool can't
|
|
1215
1233
|
// serve — every seat rate-limited or in auth cool-down — OpenAI-shape
|
|
1216
1234
|
// requests (/v1/chat/completions) are re-pointed at the configured
|
|
@@ -1395,13 +1413,15 @@ export async function startProxy(opts = {}) {
|
|
|
1395
1413
|
// - admin bootstrap (#599): starts empty, returns a clean 503 until an
|
|
1396
1414
|
// account is added over the admin API;
|
|
1397
1415
|
// - upstream-api-key mode: OAuth + pool are bypassed, requests carry
|
|
1398
|
-
// x-api-key, so an empty pool is expected
|
|
1416
|
+
// x-api-key, so an empty pool is expected;
|
|
1417
|
+
// - a stored Codex/ChatGPT-subscription account (dario#1137): that user
|
|
1418
|
+
// never had a Claude login and doesn't need one to be served.
|
|
1399
1419
|
// Otherwise an empty pool means the login back-fill found no credentials —
|
|
1400
1420
|
// the user hasn't logged in. Preserve the single-account self-heal there: a
|
|
1401
1421
|
// dead-but-refreshable token (a container restarted right after a normal
|
|
1402
1422
|
// expiry, gap #1) is refreshed, then back-filled so the recovered login
|
|
1403
1423
|
// 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)) {
|
|
1424
|
+
if (requiresClaudeLogin(pool.size, adminEnabled, !!upstreamApiKey, opts.noClaudeAuth ?? false, hasCodexAccount)) {
|
|
1405
1425
|
const single = await resolveSingleAccountStartupStatus();
|
|
1406
1426
|
if (!single.authenticated) {
|
|
1407
1427
|
console.error('[dario] Not authenticated. Run `dario login` first.');
|
|
@@ -2143,7 +2163,23 @@ export async function startProxy(opts = {}) {
|
|
|
2143
2163
|
// shadows a real id still applies at request time).
|
|
2144
2164
|
const advertised = withLongContextVariants(catalog.bases);
|
|
2145
2165
|
const aliasNames = Object.keys(modelAliases).filter((n) => !advertised.includes(n));
|
|
2146
|
-
|
|
2166
|
+
// Codex/ChatGPT-subscription slugs the backend lists for the stored
|
|
2167
|
+
// account (dario#1010), so a client's model picker can discover the
|
|
2168
|
+
// models that account may actually use. Discovery is cached and never
|
|
2169
|
+
// throws, so /v1/models keeps its "always answers" property.
|
|
2170
|
+
let codexNames = [];
|
|
2171
|
+
if (hasCodexAccount) {
|
|
2172
|
+
const stored = await selectCodexAccount();
|
|
2173
|
+
if (stored) {
|
|
2174
|
+
const creds = await getFreshCodexAccount(stored).catch(() => stored);
|
|
2175
|
+
codexNames = (await getCodexModelSlugs(creds))
|
|
2176
|
+
.filter((s) => !advertised.includes(s) && !aliasNames.includes(s));
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
// Codex slugs are served by the ChatGPT subscription, not Anthropic, so
|
|
2180
|
+
// they advertise `owned_by: "openai"` (dario#1137).
|
|
2181
|
+
const codexOwners = Object.fromEntries(codexNames.map((s) => [s, 'openai']));
|
|
2182
|
+
const body = JSON.stringify(buildOpenAIModelsList(advertised.concat(aliasNames, codexNames), codexOwners));
|
|
2147
2183
|
res.writeHead(200, { ...JSON_HEADERS, 'Access-Control-Allow-Origin': corsOrigin });
|
|
2148
2184
|
res.end(body);
|
|
2149
2185
|
return;
|
|
@@ -2242,30 +2278,46 @@ export async function startProxy(opts = {}) {
|
|
|
2242
2278
|
// the first version of this forwarded nothing (dario#885).
|
|
2243
2279
|
let genuineCCRequest = false;
|
|
2244
2280
|
try {
|
|
2245
|
-
//
|
|
2246
|
-
//
|
|
2247
|
-
//
|
|
2248
|
-
//
|
|
2249
|
-
//
|
|
2281
|
+
// Account selection is DEFERRED to `selectPoolAccount()` below, after the
|
|
2282
|
+
// provider decision (dario#1137). Selecting here meant the empty-pool 503
|
|
2283
|
+
// fired BEFORE the codex adapter was ever consulted, so a
|
|
2284
|
+
// ChatGPT-subscription-only user — whose empty Claude pool is legitimate,
|
|
2285
|
+
// not a setup error — got "No account configured" for every request that
|
|
2286
|
+
// should have gone to their subscription. A pool account is now required
|
|
2287
|
+
// only once the routing block has declined the request, i.e. when it
|
|
2288
|
+
// really is Claude's. Inside-request 429/auth failover retries the
|
|
2289
|
+
// next-best account before surfacing an error (see the dispatch loop).
|
|
2290
|
+
// `null as PoolAccount | null` rather than a `: PoolAccount | null` annotation:
|
|
2291
|
+
// every assignment happens inside the selectPoolAccount closure below, so
|
|
2292
|
+
// control-flow analysis narrows the annotated form to `null` for the whole
|
|
2293
|
+
// rest of the handler and `poolAccount?.alias` fails to compile. The `as`
|
|
2294
|
+
// form gives the declared type without seeding a narrowing.
|
|
2250
2295
|
let poolAccount = null;
|
|
2251
|
-
let accessToken;
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2296
|
+
let accessToken = '';
|
|
2297
|
+
/**
|
|
2298
|
+
* Take a Claude pool account for this request. Returns false when it has
|
|
2299
|
+
* already answered the client (empty or fully drained pool with no viable
|
|
2300
|
+
* fallback) — the caller must return immediately. Leaves `poolAccount`
|
|
2301
|
+
* null without answering in the pool-exhausted-fallback case, which the
|
|
2302
|
+
* fallback dispatch right below picks up.
|
|
2303
|
+
*/
|
|
2304
|
+
const selectPoolAccount = () => {
|
|
2305
|
+
if (upstreamApiKey) {
|
|
2306
|
+
// Per-token API-key mode: no OAuth, no pool selection. `poolAccount`
|
|
2307
|
+
// stays null, so every pool-failover retry below is skipped; the
|
|
2308
|
+
// x-api-key is set on the outbound headers instead of a Bearer.
|
|
2309
|
+
return true;
|
|
2310
|
+
}
|
|
2259
2311
|
// Pool is the one credential model (v5.0): a plain `dario login` is a
|
|
2260
2312
|
// pool of one, so every OAuth request selects from the pool.
|
|
2261
2313
|
poolAccount = pool.select();
|
|
2262
2314
|
if (!poolAccount) {
|
|
2263
2315
|
// Pool-exhausted fallback: when armed, the pool HAS accounts (all
|
|
2264
2316
|
// drained / cooling), and the client speaks OpenAI shape, defer —
|
|
2265
|
-
// the fallback dispatch below
|
|
2266
|
-
//
|
|
2267
|
-
//
|
|
2268
|
-
//
|
|
2317
|
+
// the fallback dispatch below re-points the request at the
|
|
2318
|
+
// openai-compat backend. An EMPTY pool still 503s: that's a setup
|
|
2319
|
+
// error the operator needs to see, not traffic to quietly re-bill
|
|
2320
|
+
// somewhere else.
|
|
2269
2321
|
const fallbackViable = poolFallbackModel !== null && openaiBackend !== null
|
|
2270
2322
|
&& isOpenAI && pool.size > 0;
|
|
2271
2323
|
if (!fallbackViable) {
|
|
@@ -2286,11 +2338,12 @@ export async function startProxy(opts = {}) {
|
|
|
2286
2338
|
error: 'No accounts available in pool',
|
|
2287
2339
|
message: 'all accounts are rate-limited or in auth cool-down; retry shortly',
|
|
2288
2340
|
}));
|
|
2289
|
-
return;
|
|
2341
|
+
return false;
|
|
2290
2342
|
}
|
|
2291
2343
|
}
|
|
2292
2344
|
accessToken = poolAccount?.accessToken ?? '';
|
|
2293
|
-
|
|
2345
|
+
return true;
|
|
2346
|
+
};
|
|
2294
2347
|
// Client-side session key (constant per request) for the rotation registry
|
|
2295
2348
|
// — consulted at body-build, at the outbound header, and on each mid-request
|
|
2296
2349
|
// failover rewrite so all three agree on the selected account's session.
|
|
@@ -2418,18 +2471,55 @@ export async function startProxy(opts = {}) {
|
|
|
2418
2471
|
// (forcedProvider === 'openai' || isOpenAIModel(model))`), consolidated so
|
|
2419
2472
|
// the routing rule is testable and lives in one place. `openaiBackend`
|
|
2420
2473
|
// stays in the guard for TS narrowing (route already implies it non-null).
|
|
2474
|
+
//
|
|
2475
|
+
// `codex` joins as a third provider (dario#1009/#1010): an OpenAI-shape
|
|
2476
|
+
// request naming a model the ChatGPT backend LISTS for the stored account
|
|
2477
|
+
// (or carrying a `codex:`/`chatgpt:` prefix) is served from that
|
|
2478
|
+
// subscription, via the chat/completions⇄Responses translation in
|
|
2479
|
+
// codex-backend.ts. It ranks above the openai adapter so a listed slug
|
|
2480
|
+
// reaches the subscription even when an API-key backend is configured too.
|
|
2421
2481
|
if (body.length > 0) {
|
|
2422
2482
|
try {
|
|
2423
2483
|
const peek = JSON.parse(body.toString());
|
|
2424
2484
|
const rawModel = (peek.model || '').toString();
|
|
2485
|
+
// Credentials are re-read per request (not cached at startup) because
|
|
2486
|
+
// a refresh rotates them on disk; getFreshCodexAccount refreshes when
|
|
2487
|
+
// inside the expiry buffer, collapsing concurrent refreshes per alias.
|
|
2488
|
+
// Resolved BEFORE routing because the routable model set is whatever
|
|
2489
|
+
// THIS account's backend lists — discovery is cached, so the common
|
|
2490
|
+
// case is a map lookup, not a request.
|
|
2491
|
+
let codexCreds = null;
|
|
2492
|
+
let codexModels = [];
|
|
2493
|
+
if (hasCodexAccount && isOpenAI) {
|
|
2494
|
+
const stored = await selectCodexAccount();
|
|
2495
|
+
if (stored) {
|
|
2496
|
+
codexCreds = await getFreshCodexAccount(stored);
|
|
2497
|
+
codexModels = await getCodexModelSlugs(codexCreds);
|
|
2498
|
+
}
|
|
2499
|
+
else {
|
|
2500
|
+
// Accounts disappeared since startup — re-arm the routing flag so
|
|
2501
|
+
// later requests skip the codex path entirely.
|
|
2502
|
+
hasCodexAccount = false;
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2425
2505
|
const decision = routeProvider({
|
|
2426
2506
|
isOpenAIPath: isOpenAI,
|
|
2427
2507
|
model: rawModel,
|
|
2428
2508
|
forcedProvider,
|
|
2429
2509
|
hasOpenAIBackend: openaiBackend !== null,
|
|
2510
|
+
hasCodexAccount: codexCreds !== null,
|
|
2511
|
+
codexModels,
|
|
2430
2512
|
poolFallbackModel,
|
|
2431
2513
|
poolSize: pool.size,
|
|
2432
2514
|
});
|
|
2515
|
+
if (rawModel && codexCreds && decision.provider === 'codex') {
|
|
2516
|
+
if (verbose) {
|
|
2517
|
+
console.log(`[dario] #${requestCount} ${req.method} ${urlPath} (model: ${rawModel}) → codex account ${codexCreds.alias}`);
|
|
2518
|
+
}
|
|
2519
|
+
requestCount++;
|
|
2520
|
+
await forwardToCodex(req, res, body, codexCreds, corsOrigin, SECURITY_HEADERS, upstreamTimeoutMs, verbose);
|
|
2521
|
+
return;
|
|
2522
|
+
}
|
|
2433
2523
|
if (rawModel && openaiBackend && decision.provider === 'openai') {
|
|
2434
2524
|
if (verbose) {
|
|
2435
2525
|
console.log(`[dario] #${requestCount} ${req.method} ${urlPath} (model: ${rawModel}) → openai backend`);
|
|
@@ -2441,6 +2531,13 @@ export async function startProxy(opts = {}) {
|
|
|
2441
2531
|
}
|
|
2442
2532
|
catch { /* not JSON — fall through to existing path */ }
|
|
2443
2533
|
}
|
|
2534
|
+
// Claude's turn: the routing block above declined this request, so it
|
|
2535
|
+
// needs a pool account. Selecting HERE and not before the body read is
|
|
2536
|
+
// the fix for dario#1137 — a ChatGPT-subscription-only user has a
|
|
2537
|
+
// legitimately empty Claude pool, and the empty-pool 503 used to answer
|
|
2538
|
+
// codex-bound requests before the adapter was ever consulted.
|
|
2539
|
+
if (!selectPoolAccount())
|
|
2540
|
+
return;
|
|
2444
2541
|
// Pool-exhausted fallback dispatch. In OAuth mode poolAccount can only
|
|
2445
2542
|
// be null here when the selection above deferred to this path (armed
|
|
2446
2543
|
// 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.84",
|
|
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": {
|