@askalf/dario 6.8.8 → 6.8.9

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.
@@ -67,6 +67,15 @@ export declare function _resetCodexRefreshFailuresForTest(): void;
67
67
  * a misleading "run `dario login`" answer to the client.
68
68
  */
69
69
  export declare function getFreshCodexAccount(creds: CodexAccountCredentials): Promise<CodexAccountCredentials>;
70
+ /**
71
+ * Refresh REGARDLESS of the clock, for the one caller that knows better than
72
+ * the clock does: the backend answered 401 on a token dario still believes in
73
+ * (dario#1338 shape, 2026-09-17 — a stored token valid for another day, every
74
+ * request on the seat rejected for six hours because nothing ever asked for a
75
+ * new one). Same single-flight and same failure cool-down as the clock path:
76
+ * a dead refresh token must not become a token-endpoint storm, one per request.
77
+ */
78
+ export declare function forceRefreshCodexAccount(creds: CodexAccountCredentials): Promise<CodexAccountCredentials>;
70
79
  /** Record that `alias` declined, for as long as the upstream asked. */
71
80
  export declare function noteCodexDecline(alias: string, retryAfterMs?: number | null): number;
72
81
  /** A seat that just served is not rate-limited — clear it. */
@@ -279,6 +279,21 @@ export async function getFreshCodexAccount(creds) {
279
279
  noteRefreshRecovered(creds.alias);
280
280
  return creds;
281
281
  }
282
+ return refreshNow(creds);
283
+ }
284
+ /**
285
+ * Refresh REGARDLESS of the clock, for the one caller that knows better than
286
+ * the clock does: the backend answered 401 on a token dario still believes in
287
+ * (dario#1338 shape, 2026-09-17 — a stored token valid for another day, every
288
+ * request on the seat rejected for six hours because nothing ever asked for a
289
+ * new one). Same single-flight and same failure cool-down as the clock path:
290
+ * a dead refresh token must not become a token-endpoint storm, one per request.
291
+ */
292
+ export async function forceRefreshCodexAccount(creds) {
293
+ return refreshNow(creds);
294
+ }
295
+ /** The refresh itself: one in flight per alias, and a remembered failure short-circuits. */
296
+ async function refreshNow(creds) {
282
297
  const existing = inflightRefresh.get(creds.alias);
283
298
  if (existing)
284
299
  return existing;
@@ -278,6 +278,30 @@ export declare function buildCodexHeaders(creds: CodexAccountCredentials): Recor
278
278
  * into a buffered response object is not built yet. A non-streaming client
279
279
  * gets a 400 saying so.
280
280
  */
281
+ /**
282
+ * Is this status the backend saying the CREDENTIAL is no good, rather than the
283
+ * request or the quota? 401 and 403 both arrive that way from the Responses
284
+ * API — a revoked session, a token rotated by another client, an account whose
285
+ * plan changed underneath us.
286
+ */
287
+ export declare function isCodexAuthFailure(status: number): boolean;
288
+ /**
289
+ * One forced refresh after an auth failure, then the caller retries once.
290
+ *
291
+ * `getFreshCodexAccount` refreshes on the CLOCK, so a token dario believes in
292
+ * is never re-fetched no matter how many times upstream rejects it. On
293
+ * 2026-09-17 that took the fleet's only fallback seat down for six hours: the
294
+ * stored token was valid until the next day, the backend answered 401 to every
295
+ * request, and each one was handed to the client unchanged — no refresh, no
296
+ * cool-down, no failover.
297
+ *
298
+ * Returns the fresh credentials when a retry is worth making, or null when it
299
+ * is not: no refresh token to spend, the refresh failed (its own cool-down then
300
+ * governs — the seat is reported unavailable and the chain moves on), or the
301
+ * token came back byte-identical, in which case retrying only reproduces the
302
+ * same 401.
303
+ */
304
+ export declare function refreshAfterCodexAuthFailure(creds: CodexAccountCredentials, verbose: boolean): Promise<CodexAccountCredentials | null>;
281
305
  export declare function forwardResponsesToCodex(res: ServerResponse, body: Record<string, unknown>, creds: CodexAccountCredentials, corsOrigin: string, securityHeaders: Record<string, string>, upstreamTimeoutMs: number, verbose: boolean, fetchImpl?: typeof fetch, onDone?: (outcome: CodexForwardOutcome) => void,
282
306
  /** Mirrors forwardToCodex. A 429 or 5xx is the SEAT saying no, and the
283
307
  * caller needs to know which seat and for how long — without it the pool
@@ -22,6 +22,7 @@
22
22
  * translation in both directions, including SSE.
23
23
  */
24
24
  import { createHash } from 'node:crypto';
25
+ import { forceRefreshCodexAccount } from './codex-accounts.js';
25
26
  import { anthropicToResponsesRequest, anthropicUsageFromResponses, createResponsesSSEParser, formatResponsesAnthropicSSE, createAnthropicMessageAssembler, responsesStreamToAnthropicSSE, } from './anthropic-responses-translate.js';
26
27
  import { resolveClaudeTarget } from './claude-model.js';
27
28
  import { parseEffortSuffix } from './effort.js';
@@ -759,6 +760,48 @@ export function buildCodexHeaders(creds) {
759
760
  * into a buffered response object is not built yet. A non-streaming client
760
761
  * gets a 400 saying so.
761
762
  */
763
+ /**
764
+ * Is this status the backend saying the CREDENTIAL is no good, rather than the
765
+ * request or the quota? 401 and 403 both arrive that way from the Responses
766
+ * API — a revoked session, a token rotated by another client, an account whose
767
+ * plan changed underneath us.
768
+ */
769
+ export function isCodexAuthFailure(status) {
770
+ return status === 401 || status === 403;
771
+ }
772
+ /**
773
+ * One forced refresh after an auth failure, then the caller retries once.
774
+ *
775
+ * `getFreshCodexAccount` refreshes on the CLOCK, so a token dario believes in
776
+ * is never re-fetched no matter how many times upstream rejects it. On
777
+ * 2026-09-17 that took the fleet's only fallback seat down for six hours: the
778
+ * stored token was valid until the next day, the backend answered 401 to every
779
+ * request, and each one was handed to the client unchanged — no refresh, no
780
+ * cool-down, no failover.
781
+ *
782
+ * Returns the fresh credentials when a retry is worth making, or null when it
783
+ * is not: no refresh token to spend, the refresh failed (its own cool-down then
784
+ * governs — the seat is reported unavailable and the chain moves on), or the
785
+ * token came back byte-identical, in which case retrying only reproduces the
786
+ * same 401.
787
+ */
788
+ export async function refreshAfterCodexAuthFailure(creds, verbose) {
789
+ if (!creds.refreshToken)
790
+ return null;
791
+ try {
792
+ const fresh = await forceRefreshCodexAccount(creds);
793
+ if (fresh.accessToken === creds.accessToken)
794
+ return null;
795
+ if (verbose)
796
+ console.log(`[dario] codex account ${creds.alias}: upstream rejected a stored token — refreshed, retrying once`);
797
+ return fresh;
798
+ }
799
+ catch (err) {
800
+ console.warn(`[dario] codex account ${creds.alias}: upstream rejected its token and the refresh failed `
801
+ + `(${err instanceof Error ? err.message : String(err)}) — re-add the seat with \`dario add altman ${creds.alias}\``);
802
+ return null;
803
+ }
804
+ }
762
805
  export async function forwardResponsesToCodex(res, body, creds, corsOrigin, securityHeaders, upstreamTimeoutMs, verbose, fetchImpl = fetch, onDone,
763
806
  /** Mirrors forwardToCodex. A 429 or 5xx is the SEAT saying no, and the
764
807
  * caller needs to know which seat and for how long — without it the pool
@@ -808,14 +851,26 @@ deferOnUnavailable = false) {
808
851
  try {
809
852
  if (verbose)
810
853
  console.log(`[dario] → codex backend (responses passthrough): ${target} (model: ${model})`);
811
- const upstream = await fetchImpl(target, { method: 'POST', headers: buildCodexHeaders(creds), body: JSON.stringify(upstreamBody), signal: abort.signal });
854
+ let activeCreds = creds;
855
+ let upstream = await fetchImpl(target, { method: 'POST', headers: buildCodexHeaders(activeCreds), body: JSON.stringify(upstreamBody), signal: abort.signal });
856
+ if (isCodexAuthFailure(upstream.status)) {
857
+ await upstream.text().catch(() => ''); // release the rejected response before retrying
858
+ const fresh = await refreshAfterCodexAuthFailure(activeCreds, verbose);
859
+ if (fresh) {
860
+ activeCreds = fresh;
861
+ upstream = await fetchImpl(target, { method: 'POST', headers: buildCodexHeaders(activeCreds), body: JSON.stringify(upstreamBody), signal: abort.signal });
862
+ }
863
+ }
812
864
  if (!upstream.ok || !upstream.body) {
813
865
  const detail = await upstream.text().catch(() => '');
814
866
  if (verbose)
815
867
  console.error(`[dario] codex backend ${upstream.status}: ${detail.slice(0, 300)}`);
816
868
  // Same rule as the Messages path: a 429 or a 5xx is the seat declining,
817
- // and that is true whether or not anything is waiting to take over.
818
- const unavailable = upstream.status === 429 || upstream.status >= 500;
869
+ // and that is true whether or not anything is waiting to take over. An
870
+ // auth failure that survived the forced refresh above joins them: the
871
+ // seat cannot serve until someone re-adds it, so cool it and let the
872
+ // chain move on instead of handing the client a 401 it cannot act on.
873
+ const unavailable = upstream.status === 429 || upstream.status >= 500 || isCodexAuthFailure(upstream.status);
819
874
  if (unavailable) {
820
875
  try {
821
876
  onDecline?.({ status: upstream.status, retryAfterMs: parseRetryAfterMs(upstream.headers.get('retry-after')), alias: creds.alias });
@@ -1050,12 +1105,28 @@ midstream) {
1050
1105
  try {
1051
1106
  if (verbose)
1052
1107
  console.log(`[dario] → codex backend: ${target} (model: ${model})`);
1053
- const upstream = await fetchImpl(target, {
1108
+ let activeCreds = creds;
1109
+ let upstream = await fetchImpl(target, {
1054
1110
  method: 'POST',
1055
- headers: buildCodexHeaders(creds),
1111
+ headers: buildCodexHeaders(activeCreds),
1056
1112
  body: JSON.stringify(scrubbed),
1057
1113
  signal: abort.signal,
1058
1114
  });
1115
+ // An auth failure on a token the clock still trusts: refresh it once and
1116
+ // ask again, before any of the decline/report machinery below runs.
1117
+ if (isCodexAuthFailure(upstream.status)) {
1118
+ await upstream.text().catch(() => ''); // release the rejected response before retrying
1119
+ const fresh = await refreshAfterCodexAuthFailure(activeCreds, verbose);
1120
+ if (fresh) {
1121
+ activeCreds = fresh;
1122
+ upstream = await fetchImpl(target, {
1123
+ method: 'POST',
1124
+ headers: buildCodexHeaders(activeCreds),
1125
+ body: JSON.stringify(scrubbed),
1126
+ signal: abort.signal,
1127
+ });
1128
+ }
1129
+ }
1059
1130
  if (!upstream.ok) {
1060
1131
  const detail = await upstream.text().catch(() => '');
1061
1132
  if (verbose)
@@ -1073,7 +1144,12 @@ midstream) {
1073
1144
  // cue to fail over, not something to hand the client. A 4xx that is our
1074
1145
  // own fault (a bad body, an unsupported parameter) is NOT: failing over
1075
1146
  // would just reproduce it somewhere else and hide the real error.
1076
- const unavailable = upstream.status === 429 || upstream.status >= 500;
1147
+ // An auth failure that survived the forced refresh above counts as the
1148
+ // seat declining, not as the client's error: nothing the caller sends
1149
+ // will fix a revoked token, and a 401 relayed to Claude Code reads as an
1150
+ // outage (2026-09-17, six hours of it). Cooling it also stops selection
1151
+ // from handing the same dead seat the next request.
1152
+ const unavailable = upstream.status === 429 || upstream.status >= 500 || isCodexAuthFailure(upstream.status);
1077
1153
  // The seat said no, and that is true whether or not a fallback exists
1078
1154
  // to defer to. Recording it outside the defer branch is what lets the
1079
1155
  // POOL rotate on a deployment with no --pool-fallback configured: with
@@ -496,7 +496,7 @@ export declare function detectDrift(t: TemplateData, installedOverride?: string
496
496
  */
497
497
  export declare const SUPPORTED_CC_RANGE: {
498
498
  readonly min: "1.0.0";
499
- readonly maxTested: "2.1.274";
499
+ readonly maxTested: "2.1.275";
500
500
  };
501
501
  /**
502
502
  * Compare two dotted-numeric version strings. Returns negative if `a<b`,
@@ -1194,7 +1194,7 @@ export function detectDrift(t, installedOverride) {
1194
1194
  */
1195
1195
  export const SUPPORTED_CC_RANGE = {
1196
1196
  min: '1.0.0',
1197
- maxTested: '2.1.274',
1197
+ maxTested: '2.1.275',
1198
1198
  };
1199
1199
  /**
1200
1200
  * Compare two dotted-numeric version strings. Returns negative if `a<b`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "6.8.8",
3
+ "version": "6.8.9",
4
4
  "description": "Use your Claude and ChatGPT subscriptions in Cursor, Cline, Aider, Claude Code and the Agent SDK — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint: either plan answers either wire shape, with automatic failover when one hits its limit.",
5
5
  "type": "module",
6
6
  "bin": {