@askalf/dario 5.5.84 → 5.5.85

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/cli.js CHANGED
@@ -1138,6 +1138,8 @@ async function codex() {
1138
1138
  await completeAddCodexAccount(alias, code, codeVerifier);
1139
1139
  console.log('');
1140
1140
  console.log(` Added Codex account "${alias}".`);
1141
+ console.log(' A proxy that is already running picks it up within ~30s —');
1142
+ console.log(' no restart needed (dario#1138).');
1141
1143
  console.log('');
1142
1144
  }
1143
1145
  catch (err) {
@@ -6,6 +6,9 @@ export interface CodexAccountCredentials {
6
6
  idToken?: string;
7
7
  }
8
8
  export declare function listCodexAccountAliases(): Promise<string[]>;
9
+ export declare function hasAnyCodexAccount(nowMs?: number): Promise<boolean>;
10
+ /** Drop the negative cache so a test doesn't have to sleep out its TTL. */
11
+ export declare function _resetCodexPresenceCacheForTest(): void;
9
12
  export declare function loadCodexAccount(alias: string): Promise<CodexAccountCredentials | null>;
10
13
  export declare function loadAllCodexAccounts(): Promise<CodexAccountCredentials[]>;
11
14
  export declare function saveCodexAccount(creds: CodexAccountCredentials): Promise<void>;
@@ -43,6 +43,35 @@ export async function listCodexAccountAliases() {
43
43
  return [];
44
44
  }
45
45
  }
46
+ /**
47
+ * "Is the codex route available at all" — the routing question, asked per
48
+ * request rather than once at startup (dario#1138).
49
+ *
50
+ * The proxy used to resolve this a single time while booting, so an account
51
+ * stored by `dario codex add` against an ALREADY-RUNNING proxy stayed
52
+ * invisible until a restart: /v1/models advertised no gpt slugs and a listed
53
+ * slug fell through to the Claude path as an unknown model. `dario login`
54
+ * restarts the proxy by convention, `dario codex add` does not.
55
+ *
56
+ * Cheap enough to ask often — a readdir of a directory holding at most a
57
+ * handful of files — but an idle proxy still shouldn't hit the filesystem on
58
+ * every request, so the NEGATIVE answer is cached briefly. The positive one
59
+ * isn't cached: the caller goes on to read the credentials anyway, and a
60
+ * `codex remove` has to take effect immediately.
61
+ */
62
+ const CODEX_PRESENCE_NEGATIVE_TTL_MS = 30_000;
63
+ let codexAbsentUntil = 0;
64
+ export async function hasAnyCodexAccount(nowMs = Date.now()) {
65
+ if (nowMs < codexAbsentUntil)
66
+ return false;
67
+ const present = (await listCodexAccountAliases()).length > 0;
68
+ codexAbsentUntil = present ? 0 : nowMs + CODEX_PRESENCE_NEGATIVE_TTL_MS;
69
+ return present;
70
+ }
71
+ /** Drop the negative cache so a test doesn't have to sleep out its TTL. */
72
+ export function _resetCodexPresenceCacheForTest() {
73
+ codexAbsentUntil = 0;
74
+ }
46
75
  export async function loadCodexAccount(alias) {
47
76
  const path = safeAliasPath(alias);
48
77
  if (!path)
@@ -66,6 +95,10 @@ export async function saveCodexAccount(creds) {
66
95
  throw new Error(`invalid codex account alias: ${creds.alias}`);
67
96
  await ensureDir();
68
97
  await durableWriteFile(path, JSON.stringify(creds, null, 2), 0o600);
98
+ // An account stored in THIS process (`dario codex add` in a running admin
99
+ // path, or a test) must not be hidden by a negative cache entry taken
100
+ // moments earlier.
101
+ codexAbsentUntil = 0;
69
102
  }
70
103
  export async function removeCodexAccount(alias) {
71
104
  const path = safeAliasPath(alias);
package/dist/proxy.js CHANGED
@@ -21,7 +21,7 @@ import { handleAdminRequest } from './admin-api.js';
21
21
  import { createTokenBucket } from './rate-limit.js';
22
22
  import { getOpenAIBackend, isOpenAIModel, forwardToOpenAI } from './openai-backend.js';
23
23
  import { forwardToCodex, getCodexModelSlugs, CODEX_BACKEND_BASE_URL } from './codex-backend.js';
24
- import { listCodexAccountAliases, selectCodexAccount, getFreshCodexAccount } from './codex-accounts.js';
24
+ import { listCodexAccountAliases, hasAnyCodexAccount, selectCodexAccount, getFreshCodexAccount } from './codex-accounts.js';
25
25
  import { route as routeProvider } from './provider-adapter.js';
26
26
  import { RequestQueue, QueueFullError, QueueTimeoutError, DEFAULT_MAX_CONCURRENT, DEFAULT_MAX_QUEUED, DEFAULT_QUEUE_TIMEOUT_MS } from './request-queue.js';
27
27
  import { redactSecrets } from './redact.js';
@@ -1222,12 +1222,16 @@ export async function startProxy(opts = {}) {
1222
1222
  console.log(` OpenAI-compat backend: ${openaiBackend.name} → ${openaiBackend.baseUrl}`);
1223
1223
  }
1224
1224
  // Codex/ChatGPT-subscription accounts — the "altman" engine (dario#1009).
1225
- // Loaded once at startup the same way the openai-compat backend is; the
1226
- // credentials themselves are re-read per request (they rotate on refresh),
1227
- // this only answers "is the codex route available at all" for routing.
1228
- let hasCodexAccount = (await listCodexAccountAliases()).length > 0;
1229
- if (hasCodexAccount) {
1230
- console.log(` Codex accounts: ${(await listCodexAccountAliases()).join(', ')} → ${CODEX_BACKEND_BASE_URL}`);
1225
+ // This startup probe only decides what to PRINT and whether a Claude login
1226
+ // is required to boot; routing re-asks hasAnyCodexAccount() per request
1227
+ // (dario#1138), so an account stored by `dario codex add` against an
1228
+ // already-running proxy is picked up without a restart — `dario login`
1229
+ // restarts the proxy by convention, `dario codex add` does not. The
1230
+ // credentials themselves are re-read per request too (they rotate on
1231
+ // refresh).
1232
+ const startupCodexAliases = await listCodexAccountAliases();
1233
+ if (startupCodexAliases.length > 0) {
1234
+ console.log(` Codex accounts: ${startupCodexAliases.join(', ')} → ${CODEX_BACKEND_BASE_URL}`);
1231
1235
  }
1232
1236
  // Pool-exhausted fallback (strictly opt-in). When the Claude pool can't
1233
1237
  // serve — every seat rate-limited or in auth cool-down — OpenAI-shape
@@ -1421,7 +1425,7 @@ export async function startProxy(opts = {}) {
1421
1425
  // dead-but-refreshable token (a container restarted right after a normal
1422
1426
  // expiry, gap #1) is refreshed, then back-filled so the recovered login
1423
1427
  // becomes the pool-of-one it should be — rather than crash-looping on exit(1).
1424
- if (requiresClaudeLogin(pool.size, adminEnabled, !!upstreamApiKey, opts.noClaudeAuth ?? false, hasCodexAccount)) {
1428
+ if (requiresClaudeLogin(pool.size, adminEnabled, !!upstreamApiKey, opts.noClaudeAuth ?? false, startupCodexAliases.length > 0)) {
1425
1429
  const single = await resolveSingleAccountStartupStatus();
1426
1430
  if (!single.authenticated) {
1427
1431
  console.error('[dario] Not authenticated. Run `dario login` first.');
@@ -2168,7 +2172,7 @@ export async function startProxy(opts = {}) {
2168
2172
  // models that account may actually use. Discovery is cached and never
2169
2173
  // throws, so /v1/models keeps its "always answers" property.
2170
2174
  let codexNames = [];
2171
- if (hasCodexAccount) {
2175
+ if (await hasAnyCodexAccount()) {
2172
2176
  const stored = await selectCodexAccount();
2173
2177
  if (stored) {
2174
2178
  const creds = await getFreshCodexAccount(stored).catch(() => stored);
@@ -2490,17 +2494,16 @@ export async function startProxy(opts = {}) {
2490
2494
  // case is a map lookup, not a request.
2491
2495
  let codexCreds = null;
2492
2496
  let codexModels = [];
2493
- if (hasCodexAccount && isOpenAI) {
2497
+ // Presence is re-asked here rather than read from a startup flag so
2498
+ // an account added while the proxy runs routes on the very next
2499
+ // request; the absent answer is cached ~30s, so an idle proxy with
2500
+ // no codex account is not stat-ing the filesystem per request.
2501
+ if (isOpenAI && await hasAnyCodexAccount()) {
2494
2502
  const stored = await selectCodexAccount();
2495
2503
  if (stored) {
2496
2504
  codexCreds = await getFreshCodexAccount(stored);
2497
2505
  codexModels = await getCodexModelSlugs(codexCreds);
2498
2506
  }
2499
- else {
2500
- // Accounts disappeared since startup — re-arm the routing flag so
2501
- // later requests skip the codex path entirely.
2502
- hasCodexAccount = false;
2503
- }
2504
2507
  }
2505
2508
  const decision = routeProvider({
2506
2509
  isOpenAIPath: isOpenAI,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "5.5.84",
3
+ "version": "5.5.85",
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": {