@askalf/dario 6.8.10 → 6.8.11

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.
@@ -75,6 +75,7 @@
75
75
  * against it.
76
76
  */
77
77
  import type { IncomingMessage, ServerResponse } from 'node:http';
78
+ import type { CodexSeatState } from './codex-accounts.js';
78
79
  import { type KeyStore } from './keys.js';
79
80
  /** Persisted account metadata surfaced by `GET /admin/accounts`. */
80
81
  export interface AdminAccountRecord {
@@ -150,9 +151,10 @@ export interface AdminAccountLive {
150
151
  }
151
152
  /** An audited admin action — see `AdminDeps.audit`. Never carries secrets. */
152
153
  /** One stored ChatGPT seat as `GET /admin/codex/accounts` reports it. */
153
- export interface AdminCodexAccountRecord {
154
+ export interface AdminCodexAccountRecord extends CodexSeatState {
154
155
  alias: string;
155
156
  expiresAt: number;
157
+ /** The clock's opinion. `status` is the proxy's (dario#1343). */
156
158
  needsRefresh: boolean;
157
159
  }
158
160
  export interface AdminAuditEvent {
package/dist/admin-api.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { maskEmail } from './pool.js';
2
2
  import { timingSafeEqual } from 'node:crypto';
3
3
  import { startAddAccount, completeAddAccount, removeAccount, listAccountAliases, loadAccount, } from './accounts.js';
4
- import { startAddCodexAccount, completeAddCodexAccount, removeCodexAccount, loadAllCodexAccounts, listCodexAccountAliases, codexAccountNeedsRefresh, parseCodexManualPaste, } from './codex-accounts.js';
4
+ import { startAddCodexAccount, completeAddCodexAccount, removeCodexAccount, loadAllCodexAccounts, listCodexAccountAliases, codexAccountNeedsRefresh, codexSeatStatus, parseCodexManualPaste, } from './codex-accounts.js';
5
5
  import { parseManualPaste } from './oauth.js';
6
6
  import { grantAge } from './refresh-grant.js';
7
7
  import { createKey, revokeKey, rotateKey, parseExpiry, publicKey, KEY_NAME_RE } from './keys.js';
@@ -153,7 +153,7 @@ async function doCompleteCodexLogin(alias, rawCode, now, deps, remote) {
153
153
  async function listCodexAccountRecords() {
154
154
  const all = await loadAllCodexAccounts();
155
155
  return all
156
- .map((a) => ({ alias: a.alias, expiresAt: a.expiresAt, needsRefresh: codexAccountNeedsRefresh(a) }))
156
+ .map((a) => ({ alias: a.alias, expiresAt: a.expiresAt, needsRefresh: codexAccountNeedsRefresh(a), ...codexSeatStatus(a.alias) }))
157
157
  .sort((x, y) => x.alias.localeCompare(y.alias));
158
158
  }
159
159
  /** On-disk account inventory — the default `AdminDeps.listAccounts`. */
package/dist/cli.d.ts CHANGED
@@ -144,6 +144,28 @@ export interface LivePayload {
144
144
  * them, so a legacy payload can be driven straight through it in a test.
145
145
  */
146
146
  export declare function formatLiveAccountsListing(payload: LivePayload, port: number, now: number): string[];
147
+ /**
148
+ * `dario accounts list --live` — the running proxy's view of the pool
149
+ * (dario#1244): status with its countdown, the reading and its age, 429s
150
+ * answered, the organization, and which seats share a window. The on-disk
151
+ * listing knows none of that. Returns false when no proxy answered, so the
152
+ * caller falls back to the on-disk listing.
153
+ */
154
+ /** One seat as `GET /codex` reports it — the fields the live listing prints. */
155
+ export interface LiveCodexSeat {
156
+ alias: string;
157
+ expiresInMs: number;
158
+ requestCount: number;
159
+ status: 'ok' | 'cooling' | 'refresh-failed';
160
+ cooldownRemainingMs: number;
161
+ lastRefreshError: {
162
+ at: number;
163
+ status: number;
164
+ message: string;
165
+ } | null;
166
+ }
167
+ /** Lines for `dario codex list --live` — pure, so the shape is testable without a proxy. */
168
+ export declare function formatLiveCodexListing(accounts: readonly LiveCodexSeat[], port: number): string[];
147
169
  /**
148
170
  * Decide whether this module is being invoked as the CLI entry point or
149
171
  * imported as a library. Pure, exported for tests; the file-bottom uses
package/dist/cli.js CHANGED
@@ -1134,13 +1134,67 @@ export function formatLiveAccountsListing(payload, port, now) {
1134
1134
  lines.push('');
1135
1135
  return lines;
1136
1136
  }
1137
+ /** Lines for `dario codex list --live` — pure, so the shape is testable without a proxy. */
1138
+ export function formatLiveCodexListing(accounts, port) {
1139
+ const out = ['', ` dario — Codex accounts (live, from http://127.0.0.1:${port}/codex)`, ' ───────────────────────────────────────', ''];
1140
+ if (accounts.length === 0) {
1141
+ out.push(' No Codex accounts.', '');
1142
+ return out;
1143
+ }
1144
+ for (const a of accounts) {
1145
+ const mins = Math.floor(Math.max(0, a.expiresInMs) / 60000);
1146
+ const expiry = a.expiresInMs > 0 ? `${mins}m` : 'expired';
1147
+ let state;
1148
+ if (a.status === 'refresh-failed') {
1149
+ const e = a.lastRefreshError;
1150
+ state = `refresh-failed (${e ? `${e.status}: ${e.message}` : 'token endpoint refused'}) — re-add the seat`;
1151
+ }
1152
+ else if (a.status === 'cooling') {
1153
+ state = `cooling ${Math.ceil(a.cooldownRemainingMs / 1000)}s — the backend declined it; selection skips it until then`;
1154
+ }
1155
+ else {
1156
+ state = 'ok';
1157
+ }
1158
+ out.push(` ${a.alias.padEnd(20)} ${state}`);
1159
+ out.push(` ${''.padEnd(20)} token expires in ${expiry}, ${a.requestCount} request${a.requestCount === 1 ? '' : 's'} served`);
1160
+ }
1161
+ out.push('');
1162
+ return out;
1163
+ }
1137
1164
  /**
1138
- * `dario accounts list --live` — the running proxy's view of the pool
1139
- * (dario#1244): status with its countdown, the reading and its age, 429s
1140
- * answered, the organization, and which seats share a window. The on-disk
1141
- * listing knows none of that. Returns false when no proxy answered, so the
1142
- * caller falls back to the on-disk listing.
1165
+ * `dario codex list --live` — the running proxy's view of each seat (dario#1343):
1166
+ * what it will do with it, not what the clock says. The on-disk listing cannot
1167
+ * know that a seat is cooling or that its refresh was refused; only the process
1168
+ * that tried does. Returns false when no proxy answered, so the caller falls
1169
+ * back to the on-disk listing.
1143
1170
  */
1171
+ async function codexListLive() {
1172
+ const { loadConfig } = await import('./config-file.js');
1173
+ const fileCfg = loadConfig().config;
1174
+ const portArg = args.find(a => a.startsWith('--port='));
1175
+ const port = (portArg ? parseInt(portArg.split('=')[1], 10) : undefined)
1176
+ ?? (process.env['DARIO_PORT'] ? parseInt(process.env['DARIO_PORT'], 10) : undefined)
1177
+ ?? fileCfg.port ?? 3456;
1178
+ const headers = {};
1179
+ if (process.env['DARIO_API_KEY'])
1180
+ headers['x-api-key'] = process.env['DARIO_API_KEY'];
1181
+ let payload = null;
1182
+ try {
1183
+ const res = await fetch(`http://127.0.0.1:${port}/codex`, { headers, signal: AbortSignal.timeout(3000) });
1184
+ if (res.ok)
1185
+ payload = await res.json();
1186
+ else
1187
+ console.log(` (proxy on http://127.0.0.1:${port} answered ${res.status} to /codex — showing the on-disk listing)`);
1188
+ }
1189
+ catch (err) {
1190
+ console.log(` (no proxy on http://127.0.0.1:${port}: ${err instanceof Error ? err.message : String(err)} — showing the on-disk listing)`);
1191
+ }
1192
+ if (!payload || !Array.isArray(payload.accounts))
1193
+ return false;
1194
+ for (const line of formatLiveCodexListing(payload.accounts, port))
1195
+ console.log(line);
1196
+ return true;
1197
+ }
1144
1198
  async function accountsListLive() {
1145
1199
  const { loadConfig } = await import('./config-file.js');
1146
1200
  const fileCfg = loadConfig().config;
@@ -1444,6 +1498,10 @@ async function accounts() {
1444
1498
  */
1445
1499
  async function codex() {
1446
1500
  const sub = args[1];
1501
+ if ((!sub || sub === 'list') && args.includes('--live')) {
1502
+ if (await codexListLive())
1503
+ return;
1504
+ }
1447
1505
  if (!sub || sub === 'list') {
1448
1506
  const aliases = await listCodexAccountAliases();
1449
1507
  console.log('');
@@ -1467,6 +1525,8 @@ async function codex() {
1467
1525
  console.log(` ${a.alias.padEnd(20)} token expires in ${expiry}`);
1468
1526
  }
1469
1527
  console.log('');
1528
+ console.log(' (what the proxy will do with each seat — cooling, refresh refused — is `dario codex list --live` on a running proxy)');
1529
+ console.log('');
1470
1530
  return;
1471
1531
  }
1472
1532
  if (sub === 'add') {
@@ -51,8 +51,36 @@ export interface CodexRefreshFailure {
51
51
  message: string;
52
52
  }
53
53
  /** Last remembered refresh failure for an alias, or null. Read-only view for
54
- * the admin surface (`GET /codex`) — never triggers an upstream call. */
55
- export declare function getCodexRefreshFailure(alias: string): CodexRefreshFailure | null;
54
+ * the admin surface (`GET /codex`) — never triggers an upstream call.
55
+ *
56
+ * Remembered means the proxy would still refuse to retry: the same
57
+ * `now < retryAt` test `refreshNow` applies. Past that instant the next
58
+ * request will try the token endpoint again, so the failure is no longer what
59
+ * the proxy will do with the seat and must not be reported as such (review on
60
+ * dario#1343: the old read returned the entry until something overwrote it,
61
+ * which could tell an operator to re-add a seat that was about to recover).
62
+ * The expired entry is dropped here so every reader agrees. */
63
+ export declare function getCodexRefreshFailure(alias: string, now?: number): CodexRefreshFailure | null;
64
+ export type CodexSeatStatus = 'ok' | 'cooling' | 'refresh-failed';
65
+ export interface CodexSeatState {
66
+ /** What the proxy will do with this seat right now. */
67
+ status: CodexSeatStatus;
68
+ /** ms until a declined seat is offered again; 0 when it is not cooling. */
69
+ cooldownRemainingMs: number;
70
+ /** The last token-endpoint rejection still remembered (about a minute), or null. */
71
+ lastRefreshError: CodexRefreshFailure | null;
72
+ }
73
+ /**
74
+ * The seat as the proxy sees it, not as the clock sees it (dario#1343). A stored
75
+ * token can be a day from its stated expiry while the backend rejects every
76
+ * request made with it; `expiresAt` alone reported such a seat as healthy for
77
+ * six hours. Two in-memory facts say otherwise: a refresh the token endpoint
78
+ * refused (the seat needs re-adding), and a cool-down after the backend
79
+ * declined it (selection is skipping it). Read from memory only — no request,
80
+ * no refresh, no credential in the answer. An alias the proxy has never touched
81
+ * reads `ok`, which is the truth: nothing is known against it.
82
+ */
83
+ export declare function codexSeatStatus(alias: string, now?: number): CodexSeatState;
56
84
  /** Test seam — forget every remembered failure. */
57
85
  export declare function _resetCodexRefreshFailuresForTest(): void;
58
86
  /**
@@ -205,10 +205,40 @@ export class CodexCredentialsUnavailableError extends Error {
205
205
  const REFRESH_FAILURE_TTL_MS = 60 * 1000;
206
206
  const refreshFailures = new Map();
207
207
  /** Last remembered refresh failure for an alias, or null. Read-only view for
208
- * the admin surface (`GET /codex`) — never triggers an upstream call. */
209
- export function getCodexRefreshFailure(alias) {
208
+ * the admin surface (`GET /codex`) — never triggers an upstream call.
209
+ *
210
+ * Remembered means the proxy would still refuse to retry: the same
211
+ * `now < retryAt` test `refreshNow` applies. Past that instant the next
212
+ * request will try the token endpoint again, so the failure is no longer what
213
+ * the proxy will do with the seat and must not be reported as such (review on
214
+ * dario#1343: the old read returned the entry until something overwrote it,
215
+ * which could tell an operator to re-add a seat that was about to recover).
216
+ * The expired entry is dropped here so every reader agrees. */
217
+ export function getCodexRefreshFailure(alias, now = Date.now()) {
210
218
  const hit = refreshFailures.get(alias);
211
- return hit ? { at: hit.at, status: hit.status, message: hit.message } : null;
219
+ if (!hit)
220
+ return null;
221
+ if (now >= hit.retryAt) {
222
+ refreshFailures.delete(alias);
223
+ return null;
224
+ }
225
+ return { at: hit.at, status: hit.status, message: hit.message };
226
+ }
227
+ /**
228
+ * The seat as the proxy sees it, not as the clock sees it (dario#1343). A stored
229
+ * token can be a day from its stated expiry while the backend rejects every
230
+ * request made with it; `expiresAt` alone reported such a seat as healthy for
231
+ * six hours. Two in-memory facts say otherwise: a refresh the token endpoint
232
+ * refused (the seat needs re-adding), and a cool-down after the backend
233
+ * declined it (selection is skipping it). Read from memory only — no request,
234
+ * no refresh, no credential in the answer. An alias the proxy has never touched
235
+ * reads `ok`, which is the truth: nothing is known against it.
236
+ */
237
+ export function codexSeatStatus(alias, now = Date.now()) {
238
+ const lastRefreshError = getCodexRefreshFailure(alias, now);
239
+ const cooldownRemainingMs = codexCooldownRemainingMs(alias);
240
+ const status = lastRefreshError ? 'refresh-failed' : cooldownRemainingMs > 0 ? 'cooling' : 'ok';
241
+ return { status, cooldownRemainingMs, lastRefreshError };
212
242
  }
213
243
  /** Test seam — forget every remembered failure. */
214
244
  export function _resetCodexRefreshFailuresForTest() {
@@ -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.275";
499
+ readonly maxTested: "2.1.276";
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.275',
1197
+ maxTested: '2.1.276',
1198
1198
  };
1199
1199
  /**
1200
1200
  * Compare two dotted-numeric version strings. Returns negative if `a<b`,
package/dist/proxy.js CHANGED
@@ -45,7 +45,7 @@ import { responsesRequestToAnthropic, unsupportedOnClaudeError, ResponsesRequest
45
45
  import { isClaudeServableModel } from './claude-model.js';
46
46
  import { MODEL_UNROUTABLE } from './upstream-rejection.js';
47
47
  import { readCompareTarget, teeResponse, runCompare, writeCompareRecord, COMPARE_RESULT_HEADER } from './compare.js';
48
- import { listCodexAccountAliases, loadAllCodexAccounts, codexAccountNeedsRefresh, hasAnyCodexAccount, selectCodexAccount, selectCodexAccountExcluding, rebindCodexSticky, getFreshCodexAccount, noteCodexDecline, clearCodexDecline, allAliasesCooled, getCodexRefreshFailure, CodexCredentialsUnavailableError, resetCodexPresenceCache } from './codex-accounts.js';
48
+ import { listCodexAccountAliases, loadAllCodexAccounts, codexAccountNeedsRefresh, hasAnyCodexAccount, selectCodexAccount, selectCodexAccountExcluding, rebindCodexSticky, getFreshCodexAccount, noteCodexDecline, clearCodexDecline, allAliasesCooled, CodexCredentialsUnavailableError, resetCodexPresenceCache, codexSeatStatus, } from './codex-accounts.js';
49
49
  import { route as routeProvider } from './provider-adapter.js';
50
50
  import { selectPoolFallbackModels } from './pool-fallback-tier.js';
51
51
  import { RequestQueue, QueueFullError, QueueTimeoutError, DEFAULT_MAX_CONCURRENT, DEFAULT_MAX_QUEUED, DEFAULT_QUEUE_TIMEOUT_MS, resolveMaxConcurrent } from './request-queue.js';
@@ -2649,11 +2649,13 @@ export async function startProxy(opts = {}) {
2649
2649
  needsRefresh: codexAccountNeedsRefresh(a),
2650
2650
  models: peekCodexModelSlugs(a.alias) ?? [],
2651
2651
  requestCount: codexRequestCounts.get(a.alias) ?? 0,
2652
- // Why an account that LOOKS present is serving nothing: the last
2653
- // token-endpoint rejection, remembered in-process for a minute
2654
- // (DEV-179a412f). Read from memory only a status read never spends
2655
- // or exposes a credential, so there is no token in {at,status,message}.
2656
- lastRefreshError: getCodexRefreshFailure(a.alias),
2652
+ // Why an account that LOOKS present is serving nothing (dario#1343):
2653
+ // `status` is what the proxy will do with the seat, `cooldownRemainingMs`
2654
+ // how long selection skips it after the backend declined it, and
2655
+ // `lastRefreshError` the token-endpoint rejection remembered in-process
2656
+ // for a minute (DEV-179a412f). Read from memory only — a status read
2657
+ // never spends or exposes a credential, so there is no token in any of it.
2658
+ ...codexSeatStatus(a.alias),
2657
2659
  }));
2658
2660
  res.writeHead(200, JSON_HEADERS);
2659
2661
  res.end(JSON.stringify({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "6.8.10",
3
+ "version": "6.8.11",
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": {