@askalf/dario 6.0.1 → 6.0.2
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 +2 -2
- package/dist/claude-model.d.ts +12 -0
- package/dist/claude-model.js +70 -0
- package/dist/codex-backend.d.ts +2 -1
- package/dist/codex-backend.js +26 -9
- package/dist/proxy.js +6 -1
- package/docs/multi-account-pool.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -177,7 +177,7 @@ That is a **chain**, read left to right, and each provider takes the first entry
|
|
|
177
177
|
|
|
178
178
|
A single-entry chain is one-way and means what it always meant, so an existing config is unaffected. Failover is entirely opt-in: without `--pool-fallback`, a drained pool still returns its honest 429/503.
|
|
179
179
|
|
|
180
|
-
The Claude entry
|
|
180
|
+
The Claude entry has to be a model the pool can actually serve. "Not a GPT model" is not the same thing, and swapping in a typo would trade a recoverable 429 for an unrecoverable 404 — so each entry is checked positively against the live model catalog and skipped if it fails, letting the real error surface. Canonical ids (`claude-sonnet-5`), long-context variants (`claude-sonnet-5[1m]`), catalog shorthands (`opus`, `sonnet1m`) and explicit provider prefixes (`claude:opus`, `anthropic:sonnet`) all qualify.
|
|
181
181
|
|
|
182
182
|
Only a **429 or 5xx** fails over. A 400 surfaces to you, because a bad request that fails over just reproduces itself on the other provider and buries the real cause.
|
|
183
183
|
|
|
@@ -303,7 +303,7 @@ The split isn't live, but it was announced once on short notice and could return
|
|
|
303
303
|
|
|
304
304
|
| Signal | Status |
|
|
305
305
|
|---|---|
|
|
306
|
-
| Source | **~29k** lines of TypeScript across **
|
|
306
|
+
| Source | **~29k** lines of TypeScript across **59** files — auditable in a weekend (v5 removed shim; the pool is the one code path) |
|
|
307
307
|
| Dependencies | **0 runtime.** Verify: `npm ls --production` |
|
|
308
308
|
| Provenance | Every release [SLSA-attested](https://www.npmjs.com/package/@askalf/dario) via GitHub Actions + Sigstore |
|
|
309
309
|
| Scanning | [CodeQL](https://github.com/askalf/dario/actions/workflows/codeql.yml) on every push and weekly |
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Maps a bare name to a canonical id, or returns it unchanged. */
|
|
2
|
+
export type ModelResolver = (model: string) => string;
|
|
3
|
+
/**
|
|
4
|
+
* The canonical id the Claude pool would serve `model` as, or null when it
|
|
5
|
+
* cannot serve it. `bases` is the catalog base set — `getCachedBases()` at a
|
|
6
|
+
* call site, a fixture in a test. `resolve` is the alias pipeline; the default
|
|
7
|
+
* knows only catalog family shorthands, so pass the proxy's full resolver
|
|
8
|
+
* (operator aliases + pinned aliases) where one exists.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveClaudeServable(model: string, bases?: readonly string[], resolve?: ModelResolver): string | null;
|
|
11
|
+
/** Whether the Claude pool can serve `model`. See resolveClaudeServable. */
|
|
12
|
+
export declare function isClaudeServableModel(model: string, bases?: readonly string[], resolve?: ModelResolver): boolean;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Can the Claude pool actually serve this model name?" — the positive test
|
|
3
|
+
* the reverse half of failover needs, and WHAT it resolves to.
|
|
4
|
+
*
|
|
5
|
+
* Chain selection used to answer this by elimination: anything absent from the
|
|
6
|
+
* Codex slugs was handed to the Claude pool. That is not a capability test, it
|
|
7
|
+
* is a default, and it is wrong for every name that belongs to neither
|
|
8
|
+
* provider. `--pool-fallback=gpt-5.6-sol,gpt-4o` on a rate-limited
|
|
9
|
+
* subscription re-pointed the request at Anthropic as `gpt-4o` and turned a
|
|
10
|
+
* 429 into a 400 — and a typo'd slug (`claude-sonnet-6`, `gtp-5.6-sol`) did
|
|
11
|
+
* the same, silently, because a misspelling is also "absent from the slugs".
|
|
12
|
+
* A discovery outage makes it worse: `getCodexModelSlugs` degrades to an EMPTY
|
|
13
|
+
* set, at which point elimination calls EVERY chain entry Claude-servable.
|
|
14
|
+
*
|
|
15
|
+
* So the rule is positive, in two steps that mirror the request path:
|
|
16
|
+
*
|
|
17
|
+
* 1. RESOLVE the entry the way a request model is resolved — an explicit
|
|
18
|
+
* `claude:` / `anthropic:` prefix is stripped, then operator
|
|
19
|
+
* `--model-alias` values, pinned aliases (`opus48`) and catalog family
|
|
20
|
+
* shorthands (`opus`, `sonnet1m`) map to a canonical id. The caller
|
|
21
|
+
* supplies that resolver so classification and forwarding cannot
|
|
22
|
+
* disagree.
|
|
23
|
+
* 2. VALIDATE the resolved id against the catalog base set. A `claude-`
|
|
24
|
+
* prefix on its own proves nothing — `claude-sonnet-6` has the prefix and
|
|
25
|
+
* does not exist — so the id (sans `[1m]`) must be a base the pool can
|
|
26
|
+
* forward. An explicit provider prefix can force the route; it cannot
|
|
27
|
+
* make an unknown model servable.
|
|
28
|
+
*
|
|
29
|
+
* The RESOLVED id is what the caller swaps into the body. The swap happens
|
|
30
|
+
* after the proxy's own alias pass, so returning the entry as written would
|
|
31
|
+
* send an alias upstream raw and Anthropic would 400 it.
|
|
32
|
+
*
|
|
33
|
+
* Pure over the base set it is handed, so the whole selection stays testable
|
|
34
|
+
* without a socket. `bases` defaults to the baked catalog, never to empty: a
|
|
35
|
+
* cold catalog must still admit real ids, and must still refuse typos.
|
|
36
|
+
*/
|
|
37
|
+
import { BAKED_BASE_MODELS, resolveAliasAgainst } from './model-catalog.js';
|
|
38
|
+
/** Provider prefixes that force the Claude path (mirrors proxy.ts's PROVIDER_PREFIXES). */
|
|
39
|
+
const CLAUDE_PREFIXES = new Set(['claude', 'anthropic']);
|
|
40
|
+
/**
|
|
41
|
+
* The canonical id the Claude pool would serve `model` as, or null when it
|
|
42
|
+
* cannot serve it. `bases` is the catalog base set — `getCachedBases()` at a
|
|
43
|
+
* call site, a fixture in a test. `resolve` is the alias pipeline; the default
|
|
44
|
+
* knows only catalog family shorthands, so pass the proxy's full resolver
|
|
45
|
+
* (operator aliases + pinned aliases) where one exists.
|
|
46
|
+
*/
|
|
47
|
+
export function resolveClaudeServable(model, bases = BAKED_BASE_MODELS, resolve) {
|
|
48
|
+
let m = model.trim().toLowerCase();
|
|
49
|
+
if (!m)
|
|
50
|
+
return null;
|
|
51
|
+
const idx = m.indexOf(':');
|
|
52
|
+
if (idx > 0) {
|
|
53
|
+
const prefix = m.slice(0, idx);
|
|
54
|
+
// A recognized non-Claude prefix is a definite NO, not a fall-through to
|
|
55
|
+
// the name test below — `openai:claude-sonnet-5` is the operator pointing
|
|
56
|
+
// somewhere else on purpose.
|
|
57
|
+
if (!CLAUDE_PREFIXES.has(prefix))
|
|
58
|
+
return null;
|
|
59
|
+
m = m.slice(idx + 1);
|
|
60
|
+
if (!m)
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const resolved = (resolve ? resolve(m) : (resolveAliasAgainst(m, bases) ?? m)).trim().toLowerCase();
|
|
64
|
+
const base = resolved.endsWith('[1m]') ? resolved.slice(0, -4) : resolved;
|
|
65
|
+
return bases.some((b) => b.toLowerCase() === base) ? resolved : null;
|
|
66
|
+
}
|
|
67
|
+
/** Whether the Claude pool can serve `model`. See resolveClaudeServable. */
|
|
68
|
+
export function isClaudeServableModel(model, bases = BAKED_BASE_MODELS, resolve) {
|
|
69
|
+
return resolveClaudeServable(model, bases, resolve) !== null;
|
|
70
|
+
}
|
package/dist/codex-backend.d.ts
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
25
25
|
import type { CodexAccountCredentials } from './codex-accounts.js';
|
|
26
|
+
import { type ModelResolver } from './claude-model.js';
|
|
26
27
|
export declare const CODEX_BACKEND_BASE_URL: string;
|
|
27
28
|
/**
|
|
28
29
|
* Client version sent on the model-discovery call. The backend REQUIRES the
|
|
@@ -85,7 +86,7 @@ export declare function isCodexModel(model: string, slugs: readonly string[]): b
|
|
|
85
86
|
* written before this release behave identically.
|
|
86
87
|
*/
|
|
87
88
|
export declare function pickCodexFallback(models: readonly string[], slugs: readonly string[]): string | null;
|
|
88
|
-
export declare function pickClaudeFallback(models: readonly string[], slugs: readonly string[]): string | null;
|
|
89
|
+
export declare function pickClaudeFallback(models: readonly string[], slugs: readonly string[], bases?: readonly string[], resolve?: ModelResolver): string | null;
|
|
89
90
|
/**
|
|
90
91
|
* Pull `chatgpt_account_id` out of the id_token's `https://api.openai.com/auth`
|
|
91
92
|
* claim. Payload only — this is reading our own token for a routing header, not
|
package/dist/codex-backend.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { anthropicToResponsesRequest, createResponsesSSEParser, formatResponsesAnthropicSSE, createAnthropicMessageAssembler, responsesStreamToAnthropicSSE, } from './anthropic-responses-translate.js';
|
|
2
|
+
import { resolveClaudeServable } from './claude-model.js';
|
|
3
|
+
import { BAKED_BASE_MODELS } from './model-catalog.js';
|
|
2
4
|
export const CODEX_BACKEND_BASE_URL = process.env.DARIO_CODEX_BASE_URL || 'https://chatgpt.com/backend-api/codex';
|
|
3
5
|
/** Originator string the codex CLI identifies itself with. */
|
|
4
6
|
const CODEX_ORIGINATOR = 'codex_cli_rs';
|
|
@@ -109,21 +111,36 @@ export function isCodexModel(model, slugs) {
|
|
|
109
111
|
export function pickCodexFallback(models, slugs) {
|
|
110
112
|
return models.find(m => isCodexModel(m, slugs)) ?? null;
|
|
111
113
|
}
|
|
112
|
-
export function pickClaudeFallback(models, slugs) {
|
|
114
|
+
export function pickClaudeFallback(models, slugs, bases = BAKED_BASE_MODELS, resolve) {
|
|
113
115
|
// "Not a codex slug" is NOT the same as "the Claude pool can serve it". A
|
|
114
116
|
// typo, a retired model, or an entry meant for some third provider would all
|
|
115
117
|
// pass that test, and the request would be swapped to a model Anthropic 404s
|
|
116
118
|
// on — trading a recoverable 429 for an unrecoverable 404, which is strictly
|
|
117
119
|
// worse than not failing over at all.
|
|
118
120
|
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
121
|
+
// v6.0.0 shipped this as `/^claude/i`, which was the right DIRECTION (fail
|
|
122
|
+
// closed) and the wrong TEST. It rejected `anthropic:opus`, where the operator
|
|
123
|
+
// named the provider explicitly, and every catalog shorthand (`opus`,
|
|
124
|
+
// `sonnet1m`) — so a legitimate chain entry silently never failed over, which
|
|
125
|
+
// is the same class of quiet wrongness it was written to prevent. It also did
|
|
126
|
+
// nothing about the case where discovery degrades and `slugs` arrives EMPTY,
|
|
127
|
+
// at which point elimination calls every entry Claude-servable.
|
|
122
128
|
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
129
|
+
// `isClaudeServableModel` is the positive capability test instead, resolved
|
|
130
|
+
// against the live catalog, so aliases work and the limitation the old comment
|
|
131
|
+
// documented as accepted is simply gone.
|
|
132
|
+
//
|
|
133
|
+
// Returns the RESOLVED canonical id, not the entry as written: the caller
|
|
134
|
+
// swaps it into the body after the proxy's own alias pass has already run,
|
|
135
|
+
// so an alias returned raw would reach Anthropic unresolved and 400.
|
|
136
|
+
for (const m of models) {
|
|
137
|
+
if (isCodexModel(m, slugs))
|
|
138
|
+
continue;
|
|
139
|
+
const resolved = resolveClaudeServable(m, bases, resolve);
|
|
140
|
+
if (resolved)
|
|
141
|
+
return resolved;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
127
144
|
}
|
|
128
145
|
/**
|
|
129
146
|
* Pull `chatgpt_account_id` out of the id_token's `https://api.openai.com/auth`
|
|
@@ -718,7 +735,7 @@ export async function forwardToCodex(req, res, body, creds, corsOrigin, security
|
|
|
718
735
|
}
|
|
719
736
|
catch { /* already closed */ }
|
|
720
737
|
}
|
|
721
|
-
report(502, null,
|
|
738
|
+
report(502, null, clientWantsStream, model);
|
|
722
739
|
return true;
|
|
723
740
|
}
|
|
724
741
|
finally {
|
package/dist/proxy.js
CHANGED
|
@@ -2706,7 +2706,12 @@ export async function startProxy(opts = {}) {
|
|
|
2706
2706
|
// and pick the request back up on the Claude path below. Before
|
|
2707
2707
|
// this, a rate-limited ChatGPT plan was terminal for a gpt-bound
|
|
2708
2708
|
// request even with an idle Claude pool sitting right beside it.
|
|
2709
|
-
|
|
2709
|
+
// Resolve chain entries exactly as a request model is resolved —
|
|
2710
|
+
// operator --model-alias first, then pinned + catalog aliases — so
|
|
2711
|
+
// `backup` or `opus48` in the chain works the way it does on a
|
|
2712
|
+
// request, and the classifier validates the id that would actually
|
|
2713
|
+
// be forwarded.
|
|
2714
|
+
const claudeTarget = pickClaudeFallback(poolFallbackModels, codexModels, getCachedBases(), (m) => resolveClaudeAlias(applyModelAlias(m, modelAliases) ?? m));
|
|
2710
2715
|
const canDefer = claudeTarget !== null && pool.size > 0 && !upstreamApiKey;
|
|
2711
2716
|
const codexReq = requestCount;
|
|
2712
2717
|
const served = await forwardToCodex(req, res, body, codexCreds, corsOrigin, SECURITY_HEADERS, upstreamTimeoutMs, verbose, isOpenAI ? 'openai' : 'anthropic', fetch, canDefer,
|
|
@@ -59,7 +59,7 @@ Neither subscription hitting its ceiling can take the deployment down on its own
|
|
|
59
59
|
Deliberate limits:
|
|
60
60
|
|
|
61
61
|
- **Only a 429 or 5xx fails over.** A 400 surfaces to the client. A bad request that fails over just reproduces itself on the other provider and buries the real cause.
|
|
62
|
-
- **The Claude entry
|
|
62
|
+
- **The Claude entry is validated positively, against the live catalog.** "Not a codex slug" would also match a typo, or a model meant for a third provider, and swapping that in trades a recoverable 429 for an unrecoverable 404. Worse, when model discovery degrades the codex slug list arrives EMPTY, at which point elimination would call *every* entry Claude-servable. So each entry is tested for what it is rather than what it isn't: canonical ids, `[1m]` variants, catalog shorthands (`opus`, `sonnet1m`) and explicit `claude:` / `anthropic:` prefixes all qualify; anything else is skipped and the real error surfaces.
|
|
63
63
|
- **The api-key backend is still OpenAI-shape only.** There is no Messages translation on that route. A Codex account has one, which is why it is preferred.
|
|
64
64
|
- **Never silent.** Every substituted response carries `x-dario-pool-fallback: <model>`. A quietly swapped model is exactly the surprise this project exists to avoid.
|
|
65
65
|
- **Empty pool still errors.** A pool with zero accounts is a setup mistake (`dario login` never ran); that returns the usual 503 rather than silently re-billing every request to another provider.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.2",
|
|
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": {
|