@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.
- package/dist/admin-api.d.ts +3 -1
- package/dist/admin-api.js +2 -2
- package/dist/cli.d.ts +22 -0
- package/dist/cli.js +65 -5
- package/dist/codex-accounts.d.ts +30 -2
- package/dist/codex-accounts.js +33 -3
- package/dist/live-fingerprint.d.ts +1 -1
- package/dist/live-fingerprint.js +1 -1
- package/dist/proxy.js +8 -6
- package/package.json +1 -1
package/dist/admin-api.d.ts
CHANGED
|
@@ -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
|
|
1139
|
-
*
|
|
1140
|
-
*
|
|
1141
|
-
*
|
|
1142
|
-
*
|
|
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') {
|
package/dist/codex-accounts.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
/**
|
package/dist/codex-accounts.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
499
|
+
readonly maxTested: "2.1.276";
|
|
500
500
|
};
|
|
501
501
|
/**
|
|
502
502
|
* Compare two dotted-numeric version strings. Returns negative if `a<b`,
|
package/dist/live-fingerprint.js
CHANGED
|
@@ -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.
|
|
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,
|
|
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:
|
|
2653
|
-
//
|
|
2654
|
-
//
|
|
2655
|
-
//
|
|
2656
|
-
|
|
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.
|
|
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": {
|