@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/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
- // Parsed body, shared between the provider-prefix detection below and the
2779
- // template-build block further down so the same bytes are not JSON.parsed
2780
- // twice per request (#642-audit). Mutations in the prefix block re-serialize
2781
- // `body` FROM this object, so it always represents the current body.
2782
- let parsedBody = null;
2783
- if (body.length > 0) {
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 = JSON.parse(body.toString());
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 = JSON.parse(body.toString());
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
- cacheReadTokens: 0, cacheCreateTokens: 0, thinkingTokens: 0,
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
- # cordon does not publish an image yet; build it from the repo.
28
- build: https://github.com/askalf/cordon.git
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.30",
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",