@askalf/dario 5.5.84 → 5.5.86

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);
@@ -228,6 +228,7 @@ export function createResponsesTranslator(model) {
228
228
  let usage = null;
229
229
  const toolCalls = new Map();
230
230
  let nextToolIndex = 0;
231
+ let roleSent = false;
231
232
  const frame = (delta, finish) => `data: ${JSON.stringify({
232
233
  id,
233
234
  object: 'chat.completion.chunk',
@@ -235,6 +236,25 @@ export function createResponsesTranslator(model) {
235
236
  model,
236
237
  choices: [{ index: 0, delta, finish_reason: finish }],
237
238
  })}\n\n`;
239
+ /**
240
+ * Prefix the role-only opening frame the first time we emit anything.
241
+ *
242
+ * The reference OpenAI stream always opens with
243
+ * `delta: {"role":"assistant","content":""}` before any content, and SDK
244
+ * accumulators (openai-node's ChatCompletionStream, and every harness built
245
+ * on it — Cursor, Continue, Aider) use that frame to OPEN the assistant
246
+ * message. Without it the assembled message has no role, which reads fine in
247
+ * a terminal and then fails when the harness sends that history back.
248
+ * Emitted at `response.created` in the normal case; the flag makes every
249
+ * other branch safe too, so a stream that somehow starts with a text delta
250
+ * still cannot put content on the wire before the role.
251
+ */
252
+ const opened = (rest) => {
253
+ if (roleSent)
254
+ return rest;
255
+ roleSent = true;
256
+ return frame({ role: 'assistant', content: '' }, null) + rest;
257
+ };
238
258
  return {
239
259
  /** Feed one raw SSE line. Returns the line to forward, or null. */
240
260
  chunk(line) {
@@ -255,14 +275,14 @@ export function createResponsesTranslator(model) {
255
275
  const r = e.response;
256
276
  if (r?.id)
257
277
  id = `chatcmpl-${r.id.replace(/^resp_/, '')}`;
258
- return null;
278
+ return opened('');
259
279
  }
260
280
  if (type === 'response.output_text.delta') {
261
281
  const d = typeof e.delta === 'string' ? e.delta : '';
262
282
  if (!d)
263
283
  return null;
264
284
  text += d;
265
- return frame({ content: d }, null);
285
+ return opened(frame({ content: d }, null));
266
286
  }
267
287
  if (type === 'response.output_item.added') {
268
288
  const item = e.item;
@@ -276,7 +296,7 @@ export function createResponsesTranslator(model) {
276
296
  args: '',
277
297
  };
278
298
  toolCalls.set(key, acc);
279
- return frame({ tool_calls: [{ index: acc.index, id: acc.id, type: 'function', function: { name: acc.name, arguments: '' } }] }, null);
299
+ return opened(frame({ tool_calls: [{ index: acc.index, id: acc.id, type: 'function', function: { name: acc.name, arguments: '' } }] }, null));
280
300
  }
281
301
  if (type === 'response.function_call_arguments.delta') {
282
302
  const key = String(e.item_id ?? '');
@@ -285,7 +305,7 @@ export function createResponsesTranslator(model) {
285
305
  if (!acc || !d)
286
306
  return null;
287
307
  acc.args += d;
288
- return frame({ tool_calls: [{ index: acc.index, function: { arguments: d } }] }, null);
308
+ return opened(frame({ tool_calls: [{ index: acc.index, function: { arguments: d } }] }, null));
289
309
  }
290
310
  if (type === 'response.completed' || type === 'response.incomplete') {
291
311
  const r = e.response;
@@ -297,7 +317,7 @@ export function createResponsesTranslator(model) {
297
317
  total_tokens: (u.input_tokens ?? 0) + (u.output_tokens ?? 0),
298
318
  };
299
319
  }
300
- return `${frame({}, toolCalls.size > 0 ? 'tool_calls' : 'stop')}data: [DONE]\n\n`;
320
+ return opened(`${frame({}, toolCalls.size > 0 ? 'tool_calls' : 'stop')}data: [DONE]\n\n`);
301
321
  }
302
322
  return null;
303
323
  },
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.86",
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": {