@askalf/dario 6.0.30 → 6.0.32
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 +400 -214
- package/dist/analytics.d.ts +24 -0
- package/dist/analytics.js +30 -2
- package/dist/anthropic-responses-translate.d.ts +21 -0
- package/dist/anthropic-responses-translate.js +43 -6
- package/dist/claude-model.js +11 -1
- package/dist/codex-backend.d.ts +62 -28
- package/dist/codex-backend.js +129 -10
- package/dist/proxy.js +62 -10
- package/dist/upstream-rejection.d.ts +8 -0
- package/dist/upstream-rejection.js +8 -0
- package/docs/integrations/cordon.md +2 -2
- package/package.json +4 -2
package/dist/proxy.js
CHANGED
|
@@ -23,6 +23,8 @@ import { handleAdminRequest } from './admin-api.js';
|
|
|
23
23
|
import { createTokenBucket } from './rate-limit.js';
|
|
24
24
|
import { getOpenAIBackend, isOpenAIModel, forwardToOpenAI } from './openai-backend.js';
|
|
25
25
|
import { forwardToCodex, getCodexModelSlugs, peekCodexModelSlugs, pickCodexFallback, pickClaudeTarget, CODEX_BACKEND_BASE_URL } from './codex-backend.js';
|
|
26
|
+
import { isClaudeServableModel } from './claude-model.js';
|
|
27
|
+
import { MODEL_UNROUTABLE } from './upstream-rejection.js';
|
|
26
28
|
import { readCompareTarget, teeResponse, runCompare, writeCompareRecord, COMPARE_RESULT_HEADER } from './compare.js';
|
|
27
29
|
import { listCodexAccountAliases, loadAllCodexAccounts, codexAccountNeedsRefresh, hasAnyCodexAccount, selectCodexAccount, getFreshCodexAccount, getCodexRefreshFailure, CodexCredentialsUnavailableError } from './codex-accounts.js';
|
|
28
30
|
import { route as routeProvider } from './provider-adapter.js';
|
|
@@ -2726,6 +2728,11 @@ export async function startProxy(opts = {}) {
|
|
|
2726
2728
|
// wire shape, and no upstream round-trip for a request nothing can serve.
|
|
2727
2729
|
// Upstreams do the same (Anthropic: "The request body is not valid
|
|
2728
2730
|
// JSON"; OpenAI: "We could not parse the JSON body of your request").
|
|
2731
|
+
//
|
|
2732
|
+
// The object this guard parses is kept as `parsedBody` and reused by the
|
|
2733
|
+
// provider-prefix block and the template build below, so the bytes are
|
|
2734
|
+
// JSON.parsed once per request (#642-audit; second-read finding on #1231).
|
|
2735
|
+
let parsedBody = null;
|
|
2729
2736
|
{
|
|
2730
2737
|
let invalid = null;
|
|
2731
2738
|
if (body.length === 0)
|
|
@@ -2740,6 +2747,8 @@ export async function startProxy(opts = {}) {
|
|
|
2740
2747
|
const v = JSON.parse(text);
|
|
2741
2748
|
if (v === null || typeof v !== 'object' || Array.isArray(v))
|
|
2742
2749
|
invalid = 'request body must be a JSON object';
|
|
2750
|
+
else
|
|
2751
|
+
parsedBody = v;
|
|
2743
2752
|
}
|
|
2744
2753
|
catch (err) {
|
|
2745
2754
|
invalid = `request body is not valid JSON: ${err instanceof Error ? err.message : String(err)}`;
|
|
@@ -2775,15 +2784,14 @@ export async function startProxy(opts = {}) {
|
|
|
2775
2784
|
// recognizes through its own Anthropic gateway, bypassing localhost).
|
|
2776
2785
|
let forcedProvider = cliProviderOverride;
|
|
2777
2786
|
let requestEffort; // dario#419 — per-request effort parsed from a model-name suffix (model:high / model-high)
|
|
2778
|
-
//
|
|
2779
|
-
// template-build block further down so the same bytes are not
|
|
2780
|
-
// twice per request (#642-audit). Mutations in the prefix block
|
|
2781
|
-
// `body` FROM this object, so it always represents the current
|
|
2782
|
-
|
|
2783
|
-
if (
|
|
2787
|
+
// `parsedBody` was parsed by the invalid-body guard above and is shared
|
|
2788
|
+
// with the template-build block further down so the same bytes are not
|
|
2789
|
+
// JSON.parsed twice per request (#642-audit). Mutations in the prefix block
|
|
2790
|
+
// re-serialize `body` FROM this object, so it always represents the current
|
|
2791
|
+
// body.
|
|
2792
|
+
if (parsedBody !== null) {
|
|
2784
2793
|
try {
|
|
2785
|
-
const parsed =
|
|
2786
|
-
parsedBody = parsed;
|
|
2794
|
+
const parsed = parsedBody;
|
|
2787
2795
|
// User-defined aliases first — before provider-prefix parsing, so
|
|
2788
2796
|
// an alias target carrying a prefix (`my-fast` → `openai:gpt-4o`)
|
|
2789
2797
|
// retargets the backend through the existing machinery below.
|
|
@@ -2927,7 +2935,7 @@ export async function startProxy(opts = {}) {
|
|
|
2927
2935
|
// reaches the subscription even when an API-key backend is configured too.
|
|
2928
2936
|
if (body.length > 0) {
|
|
2929
2937
|
try {
|
|
2930
|
-
const peek =
|
|
2938
|
+
const peek = (parsedBody ?? {}); // parsed once by the invalid-body guard; `body` is re-serialized from it
|
|
2931
2939
|
const rawModel = (peek.model || '').toString();
|
|
2932
2940
|
const requestPoolFallbackModels = selectPoolFallbackModels(poolFallbackSpec, rawModel);
|
|
2933
2941
|
const requestPoolFallbackModel = requestPoolFallbackModels[0] ?? null;
|
|
@@ -3040,7 +3048,9 @@ export async function startProxy(opts = {}) {
|
|
|
3040
3048
|
account: o.alias,
|
|
3041
3049
|
model: o.model || rawModel || 'codex',
|
|
3042
3050
|
inputTokens: o.inputTokens, outputTokens: o.outputTokens,
|
|
3043
|
-
|
|
3051
|
+
// Anthropic convention, like every other row: inputTokens is
|
|
3052
|
+
// net of the cached prefix, which sits in cacheReadTokens.
|
|
3053
|
+
cacheReadTokens: o.cacheReadTokens, cacheCreateTokens: o.cacheCreateTokens, thinkingTokens: 0,
|
|
3044
3054
|
// No Anthropic rate-limit headers on this path; the claim
|
|
3045
3055
|
// names the engine and is subscription billing, so the
|
|
3046
3056
|
// overage guard (#288) leaves it alone.
|
|
@@ -3051,8 +3061,14 @@ export async function startProxy(opts = {}) {
|
|
|
3051
3061
|
ts: new Date().toISOString(), req: codexReq,
|
|
3052
3062
|
method: req.method ?? '', path: urlPath, model: o.model || rawModel || undefined,
|
|
3053
3063
|
status: o.status, latency_ms: o.latencyMs, in_tokens: o.inputTokens, out_tokens: o.outputTokens,
|
|
3064
|
+
cache_read: o.cacheReadTokens, cache_create: o.cacheCreateTokens,
|
|
3054
3065
|
claim: CODEX_CLAIM, bucket: 'subscription', account: o.alias, stream: o.stream,
|
|
3055
3066
|
});
|
|
3067
|
+
if (verbose)
|
|
3068
|
+
console.log(formatUsageLogLine(codexReq, {
|
|
3069
|
+
inputTokens: o.inputTokens, outputTokens: o.outputTokens,
|
|
3070
|
+
cacheReadTokens: o.cacheReadTokens, cacheCreateTokens: o.cacheCreateTokens,
|
|
3071
|
+
}));
|
|
3056
3072
|
},
|
|
3057
3073
|
// Cool codex on a rate limit only — a 5xx or an unreachable backend
|
|
3058
3074
|
// is an outage, and parking a provider for that would keep it out
|
|
@@ -3109,6 +3125,42 @@ export async function startProxy(opts = {}) {
|
|
|
3109
3125
|
await forwardToOpenAI(req, res, body, openaiBackend, corsOrigin, SECURITY_HEADERS, upstreamTimeoutMs, verbose);
|
|
3110
3126
|
return;
|
|
3111
3127
|
}
|
|
3128
|
+
// dario#1236 — the Claude adapter claims by default, so a model NO
|
|
3129
|
+
// provider lists used to reach api.anthropic.com verbatim and come
|
|
3130
|
+
// back as Anthropic's 404 `model: gpt-5.6-sol`, attributed to
|
|
3131
|
+
// whichever seat sent it, after spending a pool request. Ask the
|
|
3132
|
+
// positive question the failover chain already asks (claude-model.ts)
|
|
3133
|
+
// and refuse locally instead.
|
|
3134
|
+
//
|
|
3135
|
+
// Deliberately NOT refused: a `claude-*` name the catalog does not
|
|
3136
|
+
// know (the live catalog can lag a model by a fetch, and on a cold
|
|
3137
|
+
// start it is the baked list — Anthropic's own 404 stays
|
|
3138
|
+
// authoritative for those); a request under a server-wide
|
|
3139
|
+
// --model/--fast-model override, which replaces the name; upstream
|
|
3140
|
+
// API-key mode, which has no pool to protect and may reach models the
|
|
3141
|
+
// OAuth catalog never lists; and an OpenAI-shape name the legacy
|
|
3142
|
+
// OPENAI_MODEL_MAP translates to a Claude model.
|
|
3143
|
+
if (rawModel && decision.provider === 'claude' && !upstreamApiKey && !modelOverride && !fastModelOverride
|
|
3144
|
+
&& !(isOpenAI && OPENAI_MODEL_MAP[rawModel])
|
|
3145
|
+
&& !/^claude-/i.test(rawModel.trim())
|
|
3146
|
+
&& !isClaudeServableModel(rawModel, getCachedBases(), (m) => resolveClaudeAlias(applyModelAlias(m, modelAliases) ?? m))) {
|
|
3147
|
+
const consulted = [
|
|
3148
|
+
codexCreds || codexUnavailable
|
|
3149
|
+
? `codex account ${(codexCreds ?? codexUnavailable).alias} (${codexModels.length} listed slug${codexModels.length === 1 ? '' : 's'})`
|
|
3150
|
+
: 'no codex account',
|
|
3151
|
+
openaiBackend ? `openai backend ${openaiBackend.name}${isOpenAI ? '' : ' (OpenAI path only)'}` : 'no openai backend',
|
|
3152
|
+
`claude catalog (${getCachedBases().length} bases)`,
|
|
3153
|
+
].join(', ');
|
|
3154
|
+
if (verbose)
|
|
3155
|
+
console.log(`[dario] #${requestCount} ${req.method} ${urlPath} (model: ${rawModel}) no provider lists ${rawModel}; refusing — consulted ${consulted}`);
|
|
3156
|
+
requestCount++;
|
|
3157
|
+
const message = `no provider lists model "${rawModel}" (consulted ${consulted}); refused locally rather than forwarded to the Claude pool, which would 404 it after spending a request`;
|
|
3158
|
+
res.writeHead(400, { ...JSON_HEADERS, 'x-dario-upstream-rejection': MODEL_UNROUTABLE });
|
|
3159
|
+
res.end(JSON.stringify(isOpenAI
|
|
3160
|
+
? { error: { message, type: 'invalid_request_error', param: 'model', code: 'model_not_found' } }
|
|
3161
|
+
: { type: 'error', error: { type: 'invalid_request_error', message } }));
|
|
3162
|
+
return;
|
|
3163
|
+
}
|
|
3112
3164
|
}
|
|
3113
3165
|
catch { /* not JSON — fall through to existing path */ }
|
|
3114
3166
|
}
|
|
@@ -3,6 +3,14 @@ export interface UpstreamRejection {
|
|
|
3
3
|
class: UpstreamRejectionClass;
|
|
4
4
|
marker: 'billing_required' | 'rate_limited' | 'credential_rejected' | 'upstream_rejected';
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* `x-dario-upstream-rejection` value for a request dario refused LOCALLY
|
|
8
|
+
* because no provider lists its model (dario#1236). Not a classification of
|
|
9
|
+
* an upstream answer — there was no upstream request, which is the point — but
|
|
10
|
+
* it rides the same header so a fleet log or tracker reads one field for
|
|
11
|
+
* every "this request was not served" verdict.
|
|
12
|
+
*/
|
|
13
|
+
export declare const MODEL_UNROUTABLE = "model_unroutable";
|
|
6
14
|
/** Classify subscription entitlement failures separately from temporary quota exhaustion. */
|
|
7
15
|
export declare function classifyUpstreamRejection(status: number, body: string): UpstreamRejection;
|
|
8
16
|
/** Operator action paired with the failure class. Never suggest credential churn for billing. */
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `x-dario-upstream-rejection` value for a request dario refused LOCALLY
|
|
3
|
+
* because no provider lists its model (dario#1236). Not a classification of
|
|
4
|
+
* an upstream answer — there was no upstream request, which is the point — but
|
|
5
|
+
* it rides the same header so a fleet log or tracker reads one field for
|
|
6
|
+
* every "this request was not served" verdict.
|
|
7
|
+
*/
|
|
8
|
+
export const MODEL_UNROUTABLE = 'model_unroutable';
|
|
1
9
|
/** Classify subscription entitlement failures separately from temporary quota exhaustion. */
|
|
2
10
|
export function classifyUpstreamRejection(status, body) {
|
|
3
11
|
const normalized = body.toLowerCase();
|
|
@@ -24,8 +24,8 @@ services:
|
|
|
24
24
|
expose: ["3456"]
|
|
25
25
|
|
|
26
26
|
cordon:
|
|
27
|
-
#
|
|
28
|
-
|
|
27
|
+
# multi-arch, Sigstore-attested: gh attestation verify oci://ghcr.io/askalf/cordon:v0.2.0 --repo askalf/cordon
|
|
28
|
+
image: ghcr.io/askalf/cordon:v0.2.0
|
|
29
29
|
environment:
|
|
30
30
|
ANTHROPIC_BASE: http://dario:3456
|
|
31
31
|
OPENAI_BASE: http://dario:3456
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.32",
|
|
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": {
|
|
@@ -40,7 +40,9 @@
|
|
|
40
40
|
"check:overage": "node scripts/check-overage-live.mjs",
|
|
41
41
|
"cch:calibrate": "node scripts/cch-calibrate.mjs",
|
|
42
42
|
"fix:pkg": "node -e \"const fs=require('fs');fs.writeFileSync('package.json',JSON.stringify(JSON.parse(fs.readFileSync('package.json','utf-8')),null,2)+'\\n')\"",
|
|
43
|
-
"audit:tui": "node tools/tui-audit/audit.mjs"
|
|
43
|
+
"audit:tui": "node tools/tui-audit/audit.mjs",
|
|
44
|
+
"readme:assets": "node scripts/readme/hero.mjs && node scripts/readme/terminal.mjs && node scripts/readme/tui.mjs",
|
|
45
|
+
"check:readme": "node scripts/check-readme-line-count.mjs && node scripts/check-readme-links.mjs"
|
|
44
46
|
},
|
|
45
47
|
"keywords": [
|
|
46
48
|
"llm",
|