aweshare 0.5.1 → 0.5.6
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/README.ai.md +7 -6
- package/README.md +30 -13
- package/apps/agent/dist/cli.js +1 -1
- package/apps/agent/dist/config.d.ts +1 -1
- package/apps/agent/dist/config.js +11 -0
- package/apps/agent/dist/config.js.map +1 -1
- package/apps/agent/dist/connection.js +10 -5
- package/apps/agent/dist/connection.js.map +1 -1
- package/apps/agent/dist/doctor.js +27 -7
- package/apps/agent/dist/doctor.js.map +1 -1
- package/apps/agent/dist/table.d.ts +2 -0
- package/apps/agent/dist/table.js +7 -4
- package/apps/agent/dist/table.js.map +1 -1
- package/apps/consumer/dist/cli.js +1 -1
- package/apps/consumer/dist/cli.js.map +1 -1
- package/apps/consumer/dist/table.d.ts +2 -0
- package/apps/consumer/dist/table.js +4 -1
- package/apps/consumer/dist/table.js.map +1 -1
- package/apps/hub/dist/cli.js +119 -128
- package/apps/hub/dist/cli.js.map +1 -1
- package/apps/hub/dist/consumer.d.ts +1 -0
- package/apps/hub/dist/consumer.js +4 -2
- package/apps/hub/dist/consumer.js.map +1 -1
- package/apps/hub/dist/db.d.ts +9 -1
- package/apps/hub/dist/db.js +10 -2
- package/apps/hub/dist/db.js.map +1 -1
- package/apps/hub/dist/local.d.ts +12 -2
- package/apps/hub/dist/local.js +26 -7
- package/apps/hub/dist/local.js.map +1 -1
- package/apps/hub/dist/meter.d.ts +2 -1
- package/apps/hub/dist/meter.js +21 -10
- package/apps/hub/dist/meter.js.map +1 -1
- package/apps/hub/dist/sanitize.js +8 -0
- package/apps/hub/dist/sanitize.js.map +1 -1
- package/apps/hub/dist/table.d.ts +10 -3
- package/apps/hub/dist/table.js +44 -16
- package/apps/hub/dist/table.js.map +1 -1
- package/package.json +2 -1
- package/packages/producer-core/dist/adapters.d.ts +3 -0
- package/packages/producer-core/dist/adapters.js +52 -1
- package/packages/producer-core/dist/adapters.js.map +1 -1
- package/packages/producer-core/dist/catalog.d.ts +4 -0
- package/packages/producer-core/dist/catalog.js +30 -7
- package/packages/producer-core/dist/catalog.js.map +1 -1
- package/packages/producer-core/dist/credentials.d.ts +25 -0
- package/packages/producer-core/dist/credentials.js +86 -0
- package/packages/producer-core/dist/credentials.js.map +1 -0
- package/packages/producer-core/dist/health.d.ts +25 -2
- package/packages/producer-core/dist/health.js +77 -4
- package/packages/producer-core/dist/health.js.map +1 -1
- package/packages/producer-core/dist/index.d.ts +2 -0
- package/packages/producer-core/dist/index.js +2 -0
- package/packages/producer-core/dist/index.js.map +1 -1
- package/packages/producer-core/dist/job.js +22 -7
- package/packages/producer-core/dist/job.js.map +1 -1
- package/packages/producer-core/dist/probe.d.ts +1 -1
- package/packages/producer-core/dist/probe.js +26 -2
- package/packages/producer-core/dist/probe.js.map +1 -1
- package/packages/producer-core/dist/proxy.d.ts +21 -0
- package/packages/producer-core/dist/proxy.js +77 -0
- package/packages/producer-core/dist/proxy.js.map +1 -0
- package/packages/protocol/dist/errors.d.ts +1 -0
- package/packages/protocol/dist/errors.js +1 -0
- package/packages/protocol/dist/errors.js.map +1 -1
- package/packages/protocol/dist/messages.d.ts +9 -0
- package/packages/protocol/dist/messages.js +15 -0
- package/packages/protocol/dist/messages.js.map +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readCodexCredentials } from './credentials.js';
|
|
1
2
|
/**
|
|
2
3
|
* Rebuild the upstream request from the dispatch init.
|
|
3
4
|
* The hub already stripped consumer credentials; the upstream key is
|
|
@@ -12,7 +13,20 @@ export function buildUpstreamRequest(backend, msg) {
|
|
|
12
13
|
url.searchParams.set(key, value);
|
|
13
14
|
}
|
|
14
15
|
const headers = { ...msg.headers };
|
|
15
|
-
|
|
16
|
+
stripEdgeHeaders(headers);
|
|
17
|
+
if (backend.login === 'codex') {
|
|
18
|
+
// the account login replaces an upstream key: strip any auth the consumer
|
|
19
|
+
// sent (belt-and-suspenders — the hub already stripped its own tokens)
|
|
20
|
+
// and inject the header recipe the Codex CLI itself sends
|
|
21
|
+
delete headers.authorization;
|
|
22
|
+
delete headers['x-api-key'];
|
|
23
|
+
const { accessToken, accountId } = readCodexCredentials();
|
|
24
|
+
headers.authorization = `Bearer ${accessToken}`;
|
|
25
|
+
headers['chatgpt-account-id'] = accountId;
|
|
26
|
+
headers['openai-beta'] = 'responses=experimental';
|
|
27
|
+
headers.originator = 'codex_cli_rs';
|
|
28
|
+
}
|
|
29
|
+
else if (backend.protocol === 'anthropic') {
|
|
16
30
|
if (backend.apiKey)
|
|
17
31
|
headers['x-api-key'] = backend.apiKey;
|
|
18
32
|
headers['anthropic-version'] = headers['anthropic-version'] ?? '2023-06-01';
|
|
@@ -22,4 +36,41 @@ export function buildUpstreamRequest(backend, msg) {
|
|
|
22
36
|
}
|
|
23
37
|
return { url: url.toString(), headers };
|
|
24
38
|
}
|
|
39
|
+
/** Exact edge-injected header names to strip (prefix families handled in code). */
|
|
40
|
+
const EDGE_INJECTED = new Set(['cdn-loop', 'true-client-ip', 'x-real-ip']);
|
|
41
|
+
/**
|
|
42
|
+
* Drop edge/CDN headers the hub's own fronting proxy (e.g. Cloudflare) added to
|
|
43
|
+
* the consumer request. They describe the hub's ingress, not this producer's
|
|
44
|
+
* egress — forwarded upstream they read as spoofed (cdn-loop/cf-*) and the
|
|
45
|
+
* upstream's edge answers with a 403 challenge page.
|
|
46
|
+
*/
|
|
47
|
+
function stripEdgeHeaders(headers) {
|
|
48
|
+
for (const key of Object.keys(headers)) {
|
|
49
|
+
const lower = key.toLowerCase();
|
|
50
|
+
if (EDGE_INJECTED.has(lower) || lower.startsWith('cf-') || lower.startsWith('x-forwarded-')) {
|
|
51
|
+
delete headers[key];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Normalize a JSON request to the subset chatgpt's Codex backend accepts.
|
|
56
|
+
* Non-JSON bodies pass through untouched — the upstream rejects those itself. */
|
|
57
|
+
export function prepareCodexBody(body) {
|
|
58
|
+
if (body.byteLength === 0)
|
|
59
|
+
return body;
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(Buffer.from(body).toString('utf8'));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return body;
|
|
66
|
+
}
|
|
67
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
68
|
+
return body;
|
|
69
|
+
const request = parsed;
|
|
70
|
+
request.store = false;
|
|
71
|
+
// OpenCode's @ai-sdk/openai emits this standard Responses field, but the
|
|
72
|
+
// account backend rejects it. Codex CLI omits it as well.
|
|
73
|
+
delete request.max_output_tokens;
|
|
74
|
+
return Buffer.from(JSON.stringify(parsed), 'utf8');
|
|
75
|
+
}
|
|
25
76
|
//# sourceMappingURL=adapters.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapters.js","sourceRoot":"","sources":["../src/adapters.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"adapters.js","sourceRoot":"","sources":["../src/adapters.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type Protocol } from 'aweshare-protocol';
|
|
2
|
+
/** CLI logins a backend can authenticate with (the producer machine's own login). */
|
|
3
|
+
export type LoginProvider = 'codex';
|
|
2
4
|
export interface Backend {
|
|
3
5
|
id: string;
|
|
4
6
|
protocol: Protocol;
|
|
@@ -10,6 +12,8 @@ export interface Backend {
|
|
|
10
12
|
*/
|
|
11
13
|
baseUrl: string;
|
|
12
14
|
keyRef?: string;
|
|
15
|
+
/** Account auth instead of an upstream key; mutually exclusive with keyRef. */
|
|
16
|
+
login?: LoginProvider;
|
|
13
17
|
}
|
|
14
18
|
export interface CatalogOffering {
|
|
15
19
|
alias: string;
|
|
@@ -31,24 +31,47 @@ export function parseCatalog(raw, secrets) {
|
|
|
31
31
|
// parse, don't prefix-match: a malformed-but-http-prefixed URL (unclosed
|
|
32
32
|
// IPv6 bracket, embedded space) would otherwise pass and only explode at
|
|
33
33
|
// request time inside new URL()
|
|
34
|
-
let
|
|
34
|
+
let parsedBaseUrl;
|
|
35
35
|
if (/^https?:\/\//.test(baseUrl)) {
|
|
36
36
|
try {
|
|
37
|
-
new URL(baseUrl);
|
|
38
|
-
baseUrlValid = true;
|
|
37
|
+
parsedBaseUrl = new URL(baseUrl);
|
|
39
38
|
}
|
|
40
39
|
catch {
|
|
41
|
-
|
|
40
|
+
parsedBaseUrl = undefined;
|
|
42
41
|
}
|
|
43
42
|
}
|
|
44
|
-
if (!
|
|
45
|
-
problems.push(`backend '${id}': baseUrl must be a valid http(s) URL, got '${baseUrl || '(missing)'}'`);
|
|
43
|
+
if (!parsedBaseUrl) {
|
|
44
|
+
problems.push(`backend '${id || '?'}': baseUrl must be a valid http(s) URL, got '${baseUrl || '(missing)'}'`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (b.login !== undefined && b.login !== 'codex') {
|
|
48
|
+
problems.push(`backend '${id}': login must be "codex" (the only CLI login supported)`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const login = b.login === 'codex' ? 'codex' : undefined;
|
|
52
|
+
if (login === 'codex' && protocol !== 'responses') {
|
|
53
|
+
problems.push(`backend '${id}': codex login protocol must be "responses"`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const normalizedPath = parsedBaseUrl.pathname.replace(/\/+$/, '');
|
|
57
|
+
if (login === 'codex' &&
|
|
58
|
+
(parsedBaseUrl.origin !== 'https://chatgpt.com' ||
|
|
59
|
+
normalizedPath !== '/backend-api/codex' ||
|
|
60
|
+
parsedBaseUrl.username !== '' ||
|
|
61
|
+
parsedBaseUrl.password !== '' ||
|
|
62
|
+
parsedBaseUrl.search !== '' ||
|
|
63
|
+
parsedBaseUrl.hash !== '')) {
|
|
64
|
+
problems.push(`backend '${id}': codex login baseUrl must be https://chatgpt.com/backend-api/codex so account credentials are sent only to the official upstream`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (login !== undefined && keyRef !== undefined) {
|
|
68
|
+
problems.push(`backend '${id}': login and keyRef are mutually exclusive — an account login replaces an upstream key`);
|
|
46
69
|
continue;
|
|
47
70
|
}
|
|
48
71
|
if (keyRef && !(keyRef in secrets)) {
|
|
49
72
|
problems.push(`backend '${id}': keyRef '${keyRef}' not found in secrets.json`);
|
|
50
73
|
}
|
|
51
|
-
backends.push({ id, protocol: protocol, baseUrl, keyRef });
|
|
74
|
+
backends.push({ id, protocol: protocol, baseUrl, keyRef, login });
|
|
52
75
|
}
|
|
53
76
|
const backendIds = new Set(backends.map((b) => b.id));
|
|
54
77
|
const protocolOf = new Map(backends.map((b) => [b.id, b.protocol]));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.js","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAiB,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"catalog.js","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAiB,MAAM,mBAAmB,CAAA;AAqCvF;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAC1B,GAA4B,EAC5B,OAA+B;IAE/B,MAAM,QAAQ,GAAa,EAAE,CAAA;IAE7B,MAAM,QAAQ,GAAc,EAAE,CAAA;IAC9B,MAAM,cAAc,GAAG,IAAI,GAAG,EAAU,CAAA;IACxC,IAAI,GAAG,CAAC,QAAQ,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC/D,QAAQ,CAAC,IAAI,CAAC,sCAAsC,CAAC,CAAA;IACvD,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAE,GAAG,CAAC,QAAsC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QAC/F,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;QAC/C,MAAM,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAA;QAC3B,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAA;QAC9D,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAA;QAClE,IAAI,CAAC,EAAE;YAAE,QAAQ,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAA;aAC5C,IAAI,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,QAAQ,CAAC,IAAI,CAAC,0BAA0B,EAAE,GAAG,CAAC,CAAA;;YAC1E,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QAC3B,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAoB,CAAC,EAAE,CAAC;YAC9C,QAAQ,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,GAAG,yDAAyD,CAAC,CAAA;YAC7F,SAAQ;QACV,CAAC;QACD,yEAAyE;QACzE,yEAAyE;QACzE,gCAAgC;QAChC,IAAI,aAA8B,CAAA;QAClC,IAAI,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YACjC,IAAI,CAAC;gBACH,aAAa,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAA;YAClC,CAAC;YAAC,MAAM,CAAC;gBACP,aAAa,GAAG,SAAS,CAAA;YAC3B,CAAC;QACH,CAAC;QACD,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,QAAQ,CAAC,IAAI,CACX,YAAY,EAAE,IAAI,GAAG,gDAAgD,OAAO,IAAI,WAAW,GAAG,CAC/F,CAAA;YACD,SAAQ;QACV,CAAC;QACD,IAAI,CAAC,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,CAAC,KAAK,KAAK,OAAO,EAAE,CAAC;YACjD,QAAQ,CAAC,IAAI,CAAC,YAAY,EAAE,yDAAyD,CAAC,CAAA;YACtF,SAAQ;QACV,CAAC;QACD,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAA;QACvD,IAAI,KAAK,KAAK,OAAO,IAAI,QAAQ,KAAK,WAAW,EAAE,CAAC;YAClD,QAAQ,CAAC,IAAI,CAAC,YAAY,EAAE,6CAA6C,CAAC,CAAA;YAC1E,SAAQ;QACV,CAAC;QACD,MAAM,cAAc,GAAG,aAAa,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;QACjE,IACE,KAAK,KAAK,OAAO;YACjB,CAAC,aAAa,CAAC,MAAM,KAAK,qBAAqB;gBAC7C,cAAc,KAAK,oBAAoB;gBACvC,aAAa,CAAC,QAAQ,KAAK,EAAE;gBAC7B,aAAa,CAAC,QAAQ,KAAK,EAAE;gBAC7B,aAAa,CAAC,MAAM,KAAK,EAAE;gBAC3B,aAAa,CAAC,IAAI,KAAK,EAAE,CAAC,EAC5B,CAAC;YACD,QAAQ,CAAC,IAAI,CACX,YAAY,EAAE,oIAAoI,CACnJ,CAAA;YACD,SAAQ;QACV,CAAC;QACD,IAAI,KAAK,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YAChD,QAAQ,CAAC,IAAI,CACX,YAAY,EAAE,wFAAwF,CACvG,CAAA;YACD,SAAQ;QACV,CAAC;QACD,IAAI,MAAM,IAAI,CAAC,CAAC,MAAM,IAAI,OAAO,CAAC,EAAE,CAAC;YACnC,QAAQ,CAAC,IAAI,CAAC,YAAY,EAAE,cAAc,MAAM,6BAA6B,CAAC,CAAA;QAChF,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,QAAoB,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAA;IAC/E,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IACrD,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAA;IACnE,MAAM,SAAS,GAAsB,EAAE,CAAA;IACvC,IAAI,GAAG,CAAC,SAAS,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;QACjE,QAAQ,CAAC,IAAI,CAAC,yCAAyC,CAAC,CAAA;IAC1D,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;QAC1C,CAAC,CAAE,GAAG,CAAC,SAAuC;QAC9C,CAAC,CAAC,EAAE,EAAE,CAAC;QACP,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAA;QACxD,MAAM,aAAa,GAAG,OAAO,CAAC,CAAC,aAAa,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAA;QAChF,+EAA+E;QAC/E,IAAI,CAAC,CAAC,cAAc,KAAK,SAAS,EAAE,CAAC;YACnC,QAAQ,CAAC,IAAI,CACX,aAAa,KAAK,uFAAuF,CAC1G,CAAA;YACD,SAAQ;QACV,CAAC;QACD,MAAM,qBAAqB,GACzB,CAAC,CAAC,qBAAqB,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAA;QAC7E,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;YAChD,QAAQ,CAAC,IAAI,CACX,4BAA4B,KAAK,6FAA6F,CAC/H,CAAA;YACD,SAAQ;QACV,CAAC;QACD,0EAA0E;QAC1E,sEAAsE;QACtE,MAAM,WAAW,GACf,CAAC,CAAC,QAAQ,KAAK,SAAS;YACtB,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC;gBACzB,CAAC,CAAE,CAAC,CAAC,QAAsB;gBAC3B,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;YAChB,CAAC,CAAC,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ;gBAC7B,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;gBACb,CAAC,CAAC,EAAE,CAAA;QACV,IAAI,CAAC,CAAC,QAAQ,KAAK,SAAS,IAAI,CAAC,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YACxD,QAAQ,CAAC,IAAI,CAAC,aAAa,KAAK,6CAA6C,CAAC,CAAA;YAC9E,SAAQ;QACV,CAAC;QACD,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,EAAE,CAAC;YAC/E,QAAQ,CAAC,IAAI,CAAC,aAAa,KAAK,uDAAuD,CAAC,CAAA;YACxF,SAAQ;QACV,CAAC;QACD,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAW,CAAC,CAAC,EAAE,CAAC;YAC1D,QAAQ,CAAC,IAAI,CACX,aAAa,KAAK,uBAAuB,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAW,CAAC,CAAC,CAAC,GAAG,CAC1G,CAAA;YACD,SAAQ;QACV,CAAC;QACD,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAW,CAAC,CAAC,CAAC,CAAA;QAC9E,IAAI,SAAS,CAAC,IAAI,KAAK,WAAW,CAAC,MAAM,EAAE,CAAC;YAC1C,QAAQ,CAAC,IAAI,CACX,aAAa,KAAK,0GAA0G,CAC7H,CAAA;YACD,SAAQ;QACV,CAAC;QACD,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,QAAQ,CAAC,IAAI,CAAC,aAAa,KAAK,0DAA0D,CAAC,CAAA;YAC3F,SAAQ;QACV,CAAC;QACD,IACE,CAAC,MAAM,CAAC,SAAS,CAAC,qBAAqB,CAAC;YACxC,qBAAqB,GAAG,CAAC;YACzB,qBAAqB,GAAG,EAAE,EAC1B,CAAC;YACD,QAAQ,CAAC,IAAI,CAAC,aAAa,KAAK,mDAAmD,CAAC,CAAA;YACpF,SAAQ;QACV,CAAC;QACD,MAAM,kBAAkB,GACtB,CAAC,CAAC,kBAAkB,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAA;QAC/E,IACE,kBAAkB,KAAK,SAAS;YAChC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,kBAAkB,CAAC,IAAI,kBAAkB,GAAG,CAAC,IAAI,kBAAkB,GAAG,EAAE,CAAC,EAC5F,CAAC;YACD,QAAQ,CAAC,IAAI,CAAC,aAAa,KAAK,gDAAgD,CAAC,CAAA;YACjF,SAAQ;QACV,CAAC;QACD,MAAM,WAAW,GAAG,CAAC,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAA;QACnF,IAAI,WAAW,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,IAAI,WAAW,GAAG,CAAC,CAAC,EAAE,CAAC;YACrF,QAAQ,CAAC,IAAI,CAAC,aAAa,KAAK,wDAAwD,CAAC,CAAA;YACzF,SAAQ;QACV,CAAC;QACD,KAAK,MAAM,OAAO,IAAI,WAAuB,EAAE,CAAC;YAC9C,SAAS,CAAC,IAAI,CAAC;gBACb,KAAK;gBACL,OAAO;gBACP,aAAa;gBACb,qBAAqB;gBACrB,kBAAkB;gBAClB,WAAW;aACZ,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,QAAQ,EAAE,CAAA;AACvD,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** What a login backend needs from the producer machine's own CLI login. */
|
|
2
|
+
export interface CodexCredentials {
|
|
3
|
+
accessToken: string;
|
|
4
|
+
accountId: string;
|
|
5
|
+
}
|
|
6
|
+
/** One unreadable CLI login; the message is user-ready and secret-free. */
|
|
7
|
+
export declare class CredentialError extends Error {
|
|
8
|
+
}
|
|
9
|
+
export declare function codexAuthPath(env?: NodeJS.ProcessEnv): string;
|
|
10
|
+
/**
|
|
11
|
+
* (access_token, account_id) from Codex's auth.json; a bare tokens block is
|
|
12
|
+
* accepted too. CredentialError when the shape is not recognized — the
|
|
13
|
+
* message never contains file content.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseCodexAuth(text: string): CodexCredentials;
|
|
16
|
+
/**
|
|
17
|
+
* The codex login, cached per auth.json path: served straight from the cache
|
|
18
|
+
* while mtime/size are unchanged, re-read from disk otherwise or after
|
|
19
|
+
* invalidateCodexCredentials() (the 401 path — a rotation can land inside
|
|
20
|
+
* the stat granularity).
|
|
21
|
+
*/
|
|
22
|
+
export declare function readCodexCredentials(env?: NodeJS.ProcessEnv): CodexCredentials;
|
|
23
|
+
/** Drop the cached login(s): the next read goes to disk. Called after an
|
|
24
|
+
* upstream 401 on a login backend. */
|
|
25
|
+
export declare function invalidateCodexCredentials(): void;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
/** One unreadable CLI login; the message is user-ready and secret-free. */
|
|
5
|
+
export class CredentialError extends Error {
|
|
6
|
+
}
|
|
7
|
+
export function codexAuthPath(env = process.env) {
|
|
8
|
+
// Codex itself reads CODEX_HOME, so honoring it reads the same login the
|
|
9
|
+
// local CLI would use.
|
|
10
|
+
return join(env.CODEX_HOME ?? join(homedir(), '.codex'), 'auth.json');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* (access_token, account_id) from Codex's auth.json; a bare tokens block is
|
|
14
|
+
* accepted too. CredentialError when the shape is not recognized — the
|
|
15
|
+
* message never contains file content.
|
|
16
|
+
*/
|
|
17
|
+
export function parseCodexAuth(text) {
|
|
18
|
+
let payload;
|
|
19
|
+
try {
|
|
20
|
+
payload = JSON.parse(text);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw new CredentialError('credential format not recognized — run `codex login` on this machine and retry');
|
|
24
|
+
}
|
|
25
|
+
const tokens = payload !== null && typeof payload === 'object' && 'tokens' in payload
|
|
26
|
+
? payload.tokens
|
|
27
|
+
: payload;
|
|
28
|
+
const field = (name) => tokens !== null && typeof tokens === 'object' && name in tokens
|
|
29
|
+
? tokens[name]
|
|
30
|
+
: undefined;
|
|
31
|
+
const accessToken = field('access_token');
|
|
32
|
+
const accountId = field('account_id');
|
|
33
|
+
if (typeof accessToken !== 'string' ||
|
|
34
|
+
!accessToken ||
|
|
35
|
+
typeof accountId !== 'string' ||
|
|
36
|
+
!accountId) {
|
|
37
|
+
throw new CredentialError('credential format not recognized — run `codex login` on this machine and retry');
|
|
38
|
+
}
|
|
39
|
+
return { accessToken, accountId };
|
|
40
|
+
}
|
|
41
|
+
const cache = new Map();
|
|
42
|
+
/**
|
|
43
|
+
* The codex login, cached per auth.json path: served straight from the cache
|
|
44
|
+
* while mtime/size are unchanged, re-read from disk otherwise or after
|
|
45
|
+
* invalidateCodexCredentials() (the 401 path — a rotation can land inside
|
|
46
|
+
* the stat granularity).
|
|
47
|
+
*/
|
|
48
|
+
export function readCodexCredentials(env = process.env) {
|
|
49
|
+
const path = codexAuthPath(env);
|
|
50
|
+
let st;
|
|
51
|
+
try {
|
|
52
|
+
// stat before reading: a write landing between the two leaves the cache
|
|
53
|
+
// keyed on the older stat, so the next call re-reads — never serves the
|
|
54
|
+
// cached login over a newer file unnoticed
|
|
55
|
+
st = statSync(path);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
|
|
59
|
+
throw new CredentialError(`the Codex login not found at ${path} — log in with \`codex login\` and retry`);
|
|
60
|
+
}
|
|
61
|
+
throw new CredentialError(`cannot read the Codex login at ${path}: ${err instanceof Error ? err.message : err}`);
|
|
62
|
+
}
|
|
63
|
+
const hit = cache.get(path);
|
|
64
|
+
if (hit && hit.stat.mtimeMs === st.mtimeMs && hit.stat.size === st.size) {
|
|
65
|
+
return hit.creds;
|
|
66
|
+
}
|
|
67
|
+
let text;
|
|
68
|
+
try {
|
|
69
|
+
text = readFileSync(path, 'utf8');
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
throw new CredentialError(`cannot read the Codex login at ${path}: ${err instanceof Error ? err.message : err}`);
|
|
73
|
+
}
|
|
74
|
+
if (!text.trim()) {
|
|
75
|
+
throw new CredentialError(`the Codex login at ${path} is empty — log in with \`codex login\` and retry`);
|
|
76
|
+
}
|
|
77
|
+
const creds = parseCodexAuth(text);
|
|
78
|
+
cache.set(path, { stat: { mtimeMs: st.mtimeMs, size: st.size }, creds });
|
|
79
|
+
return creds;
|
|
80
|
+
}
|
|
81
|
+
/** Drop the cached login(s): the next read goes to disk. Called after an
|
|
82
|
+
* upstream 401 on a login backend. */
|
|
83
|
+
export function invalidateCodexCredentials() {
|
|
84
|
+
cache.clear();
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAQhC,2EAA2E;AAC3E,MAAM,OAAO,eAAgB,SAAQ,KAAK;CAAG;AAE7C,MAAM,UAAU,aAAa,CAAC,MAAyB,OAAO,CAAC,GAAG;IAChE,yEAAyE;IACzE,uBAAuB;IACvB,OAAO,IAAI,CAAC,GAAG,CAAC,UAAU,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,EAAE,WAAW,CAAC,CAAA;AACvE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,IAAI,OAAgB,CAAA;IACpB,IAAI,CAAC;QACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,eAAe,CACvB,gFAAgF,CACjF,CAAA;IACH,CAAC;IACD,MAAM,MAAM,GACV,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,QAAQ,IAAI,OAAO;QACpE,CAAC,CAAE,OAAmC,CAAC,MAAM;QAC7C,CAAC,CAAC,OAAO,CAAA;IACb,MAAM,KAAK,GAAG,CAAC,IAAY,EAAW,EAAE,CACtC,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,IAAI,IAAI,MAAM;QAC7D,CAAC,CAAE,MAAkC,CAAC,IAAI,CAAC;QAC3C,CAAC,CAAC,SAAS,CAAA;IACf,MAAM,WAAW,GAAG,KAAK,CAAC,cAAc,CAAC,CAAA;IACzC,MAAM,SAAS,GAAG,KAAK,CAAC,YAAY,CAAC,CAAA;IACrC,IACE,OAAO,WAAW,KAAK,QAAQ;QAC/B,CAAC,WAAW;QACZ,OAAO,SAAS,KAAK,QAAQ;QAC7B,CAAC,SAAS,EACV,CAAC;QACD,MAAM,IAAI,eAAe,CACvB,gFAAgF,CACjF,CAAA;IACH,CAAC;IACD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,CAAA;AACnC,CAAC;AAOD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAA;AAE3C;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACvE,MAAM,IAAI,GAAG,aAAa,CAAC,GAAG,CAAC,CAAA;IAC/B,IAAI,EAA+B,CAAA;IACnC,IAAI,CAAC;QACH,wEAAwE;QACxE,wEAAwE;QACxE,2CAA2C;QAC3C,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAA;IACrB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,KAAK,IAAI,MAAM,IAAI,GAAG,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC9F,MAAM,IAAI,eAAe,CACvB,gCAAgC,IAAI,0CAA0C,CAC/E,CAAA;QACH,CAAC;QACD,MAAM,IAAI,eAAe,CACvB,kCAAkC,IAAI,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,CACtF,CAAA;IACH,CAAC;IACD,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAC3B,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,KAAK,EAAE,CAAC,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC;QACxE,OAAO,GAAG,CAAC,KAAK,CAAA;IAClB,CAAC;IACD,IAAI,IAAY,CAAA;IAChB,IAAI,CAAC;QACH,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IACnC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,eAAe,CACvB,kCAAkC,IAAI,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,CACtF,CAAA;IACH,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,eAAe,CACvB,sBAAsB,IAAI,mDAAmD,CAC9E,CAAA;IACH,CAAC;IACD,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,CAAA;IAClC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,EAAE,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;IACxE,OAAO,KAAK,CAAA;AACd,CAAC;AAED;sCACsC;AACtC,MAAM,UAAU,0BAA0B;IACxC,KAAK,CAAC,KAAK,EAAE,CAAA;AACf,CAAC"}
|
|
@@ -1,15 +1,38 @@
|
|
|
1
1
|
import type { BackendStatus } from 'aweshare-protocol';
|
|
2
|
+
import type { Logger } from 'pino';
|
|
2
3
|
export type FailureReason = 'AUTH' | 'QUOTA' | 'HTTP' | 'NETWORK' | 'CANCELLED';
|
|
4
|
+
/** Ladder transition a record() caused, for logging: entering degraded,
|
|
5
|
+
* clearing it, or nothing. */
|
|
6
|
+
export type HealthTransition = 'degraded' | 'recovered' | null;
|
|
3
7
|
/**
|
|
4
8
|
* Per-backend health state. Degraded backends are reported in heartbeats and
|
|
5
|
-
* the hub stops dispatching to their offerings until they recover.
|
|
9
|
+
* the hub stops dispatching to their offerings until they recover. The
|
|
10
|
+
* ladder is two rungs — healthy -> degraded, latched until a success — which
|
|
11
|
+
* for a relay is what awewarm's deeper ladder collapses to: its intermediate
|
|
12
|
+
* rungs trade probe expense against recovery speed, and a relay's probes are
|
|
13
|
+
* cheap pings, not quota-consuming activations. What does carry over from
|
|
14
|
+
* awewarm is the escalating probe schedule above: the longer a backend has
|
|
15
|
+
* been continuously degraded, the quieter its recovery probes become, down
|
|
16
|
+
* to a daily last gasp that never fully gives up.
|
|
6
17
|
*/
|
|
7
18
|
export declare class HealthGate {
|
|
19
|
+
private readonly now;
|
|
8
20
|
private consecutive;
|
|
9
21
|
private degraded;
|
|
10
|
-
|
|
22
|
+
private degradedSince;
|
|
23
|
+
private probeDelayMs;
|
|
24
|
+
private probeNotBefore;
|
|
25
|
+
/** The clock is injected so tests can walk the cap ladder without waiting. */
|
|
26
|
+
constructor(now?: () => number);
|
|
27
|
+
record(backendId: string, ok: boolean, reason?: FailureReason): HealthTransition;
|
|
28
|
+
/** Epoch ms before which a recovery probe for this backend should be
|
|
29
|
+
* skipped (backoff); 0 when no failed probe is being held back. */
|
|
30
|
+
probeNotBeforeMs(backendId: string): number;
|
|
11
31
|
isDegraded(backendId: string): boolean;
|
|
12
32
|
statusMap(): Record<string, BackendStatus>;
|
|
13
33
|
}
|
|
34
|
+
/** Log a ladder transition where it happened — the request path or a probe
|
|
35
|
+
* loop — so "why is it still degraded" is answerable from the logs. */
|
|
36
|
+
export declare function logHealthTransition(log: Logger, backendId: string, transition: HealthTransition, detail?: string): void;
|
|
14
37
|
/** Map an upstream HTTP status to a failure class (ok responses pass null). */
|
|
15
38
|
export declare function classifyStatus(status: number): FailureReason | null;
|
|
@@ -1,26 +1,89 @@
|
|
|
1
1
|
/** AUTH/QUOTA consecutive failures >= threshold -> degraded until a success. */
|
|
2
2
|
const DEGRADE_THRESHOLD = 2;
|
|
3
|
+
// Recovery-probe backoff, awewarm-style: while degraded, each failed probe
|
|
4
|
+
// doubles the wait before the next one, so an exhausted quota isn't pinged at
|
|
5
|
+
// the base cadence for hours on end. Any success clears the degraded state
|
|
6
|
+
// and the backoff with it. Unlike awewarm — whose probes are quota-consuming
|
|
7
|
+
// activations that must back off a full window — a relay's probes are free
|
|
8
|
+
// pings, so the cap escalates with how long the backend has stayed degraded
|
|
9
|
+
// rather than jumping straight to window-length silence:
|
|
10
|
+
// < 15h cap 5min covers ~3 GLM 5h-window rolls: refresh picked up fast
|
|
11
|
+
// < 45h cap 1h not a window roll anymore — likely needs a human
|
|
12
|
+
// < 105h cap 6h long dead; quiet but still self-healing
|
|
13
|
+
// else cap 24h daily last gasp, keeps auto-recovery alive forever
|
|
14
|
+
const PROBE_BACKOFF_BASE_MS = 30_000;
|
|
15
|
+
const PROBE_FALLBACK_CAP_MS = 24 * 3_600_000;
|
|
16
|
+
const PROBE_CAP_SCHEDULE = [
|
|
17
|
+
{ afterMs: 15 * 3_600_000, capMs: 5 * 60_000 },
|
|
18
|
+
{ afterMs: 45 * 3_600_000, capMs: 3_600_000 },
|
|
19
|
+
{ afterMs: 105 * 3_600_000, capMs: 6 * 3_600_000 },
|
|
20
|
+
{ afterMs: Number.POSITIVE_INFINITY, capMs: PROBE_FALLBACK_CAP_MS },
|
|
21
|
+
];
|
|
3
22
|
/**
|
|
4
23
|
* Per-backend health state. Degraded backends are reported in heartbeats and
|
|
5
|
-
* the hub stops dispatching to their offerings until they recover.
|
|
24
|
+
* the hub stops dispatching to their offerings until they recover. The
|
|
25
|
+
* ladder is two rungs — healthy -> degraded, latched until a success — which
|
|
26
|
+
* for a relay is what awewarm's deeper ladder collapses to: its intermediate
|
|
27
|
+
* rungs trade probe expense against recovery speed, and a relay's probes are
|
|
28
|
+
* cheap pings, not quota-consuming activations. What does carry over from
|
|
29
|
+
* awewarm is the escalating probe schedule above: the longer a backend has
|
|
30
|
+
* been continuously degraded, the quieter its recovery probes become, down
|
|
31
|
+
* to a daily last gasp that never fully gives up.
|
|
6
32
|
*/
|
|
7
33
|
export class HealthGate {
|
|
34
|
+
now;
|
|
8
35
|
consecutive = new Map();
|
|
9
36
|
degraded = new Map();
|
|
37
|
+
degradedSince = new Map();
|
|
38
|
+
probeDelayMs = new Map();
|
|
39
|
+
probeNotBefore = new Map();
|
|
40
|
+
/** The clock is injected so tests can walk the cap ladder without waiting. */
|
|
41
|
+
constructor(now = Date.now) {
|
|
42
|
+
this.now = now;
|
|
43
|
+
}
|
|
10
44
|
record(backendId, ok, reason) {
|
|
11
45
|
if (ok) {
|
|
12
46
|
this.consecutive.delete(backendId);
|
|
13
|
-
this.degraded.delete(backendId);
|
|
14
|
-
|
|
47
|
+
const wasDegraded = this.degraded.delete(backendId);
|
|
48
|
+
this.degradedSince.delete(backendId);
|
|
49
|
+
this.probeDelayMs.delete(backendId);
|
|
50
|
+
this.probeNotBefore.delete(backendId);
|
|
51
|
+
return wasDegraded ? 'recovered' : null;
|
|
15
52
|
}
|
|
16
53
|
if (reason === undefined || reason === 'CANCELLED')
|
|
17
|
-
return;
|
|
54
|
+
return null;
|
|
55
|
+
// failures recorded while already degraded are probe outcomes (admission
|
|
56
|
+
// 503s before dialing, so real traffic can't add any) — back the next
|
|
57
|
+
// probe off. The failure that causes the degradation does not: the first
|
|
58
|
+
// probe should stay immediate so a just-refreshed quota or a fresh login
|
|
59
|
+
// is picked up on the next tick.
|
|
60
|
+
const wasDegraded = this.degraded.has(backendId);
|
|
18
61
|
const current = this.consecutive.get(backendId);
|
|
19
62
|
const count = current?.reason === reason ? current.count + 1 : 1;
|
|
20
63
|
this.consecutive.set(backendId, { reason, count });
|
|
64
|
+
let transition = null;
|
|
21
65
|
if ((reason === 'AUTH' || reason === 'QUOTA') && count >= DEGRADE_THRESHOLD) {
|
|
66
|
+
if (!wasDegraded) {
|
|
67
|
+
transition = 'degraded';
|
|
68
|
+
this.degradedSince.set(backendId, this.now());
|
|
69
|
+
}
|
|
22
70
|
this.degraded.set(backendId, { state: 'degraded', reason });
|
|
23
71
|
}
|
|
72
|
+
if (wasDegraded) {
|
|
73
|
+
const since = this.degradedSince.get(backendId) ?? this.now();
|
|
74
|
+
// The final tier is Infinity, so the fallback is only a type-safe guard.
|
|
75
|
+
const cap = PROBE_CAP_SCHEDULE.find((tier) => this.now() - since < tier.afterMs)?.capMs ??
|
|
76
|
+
PROBE_FALLBACK_CAP_MS;
|
|
77
|
+
const delay = Math.min((this.probeDelayMs.get(backendId) ?? PROBE_BACKOFF_BASE_MS / 2) * 2, cap);
|
|
78
|
+
this.probeDelayMs.set(backendId, delay);
|
|
79
|
+
this.probeNotBefore.set(backendId, this.now() + delay);
|
|
80
|
+
}
|
|
81
|
+
return transition;
|
|
82
|
+
}
|
|
83
|
+
/** Epoch ms before which a recovery probe for this backend should be
|
|
84
|
+
* skipped (backoff); 0 when no failed probe is being held back. */
|
|
85
|
+
probeNotBeforeMs(backendId) {
|
|
86
|
+
return this.probeNotBefore.get(backendId) ?? 0;
|
|
24
87
|
}
|
|
25
88
|
isDegraded(backendId) {
|
|
26
89
|
return this.degraded.has(backendId);
|
|
@@ -32,6 +95,16 @@ export class HealthGate {
|
|
|
32
95
|
return out;
|
|
33
96
|
}
|
|
34
97
|
}
|
|
98
|
+
/** Log a ladder transition where it happened — the request path or a probe
|
|
99
|
+
* loop — so "why is it still degraded" is answerable from the logs. */
|
|
100
|
+
export function logHealthTransition(log, backendId, transition, detail) {
|
|
101
|
+
if (transition === 'degraded') {
|
|
102
|
+
log.warn(`backend ${backendId} degraded${detail ? ` (${detail})` : ''}`);
|
|
103
|
+
}
|
|
104
|
+
else if (transition === 'recovered') {
|
|
105
|
+
log.info(`backend ${backendId} recovered${detail ? ` — ${detail}` : ''}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
35
108
|
/** Map an upstream HTTP status to a failure class (ok responses pass null). */
|
|
36
109
|
export function classifyStatus(status) {
|
|
37
110
|
if (status < 400)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"health.js","sourceRoot":"","sources":["../src/health.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"health.js","sourceRoot":"","sources":["../src/health.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,UAAU,CAAA;AACxB,cAAc,YAAY,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,aAAa,CAAA;AAC3B,cAAc,UAAU,CAAA;AACxB,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { buildUpstreamRequest } from './adapters.js';
|
|
2
|
-
import {
|
|
1
|
+
import { buildUpstreamRequest, prepareCodexBody, } from './adapters.js';
|
|
2
|
+
import { CredentialError, invalidateCodexCredentials } from './credentials.js';
|
|
3
|
+
import { classifyStatus, logHealthTransition } from './health.js';
|
|
4
|
+
import { codexNetworkHint, fetchUpstream } from './proxy.js';
|
|
3
5
|
/** Thrown by a sink whose transport died (e.g. a closed tunnel socket): the
|
|
4
6
|
* job stops silently — no health record, no error frame nobody would read. */
|
|
5
7
|
export class SinkClosedError extends Error {
|
|
@@ -39,18 +41,23 @@ export class UpstreamJob {
|
|
|
39
41
|
// inside the try: a malformed URL must flow to sink.error like any other
|
|
40
42
|
// failure — execute() must never reject
|
|
41
43
|
const { url, headers } = buildUpstreamRequest(backend, init);
|
|
42
|
-
const
|
|
44
|
+
const body = backend.login !== undefined ? prepareCodexBody(init.body) : init.body;
|
|
45
|
+
const resp = await fetchUpstream(backend, url, {
|
|
43
46
|
method: init.method,
|
|
44
47
|
headers,
|
|
45
48
|
// GET/HEAD must carry no body, even an empty one
|
|
46
|
-
body:
|
|
49
|
+
body: body.byteLength > 0 ? Buffer.from(body) : undefined,
|
|
47
50
|
signal: this.controller.signal,
|
|
48
51
|
redirect: 'manual',
|
|
49
52
|
});
|
|
50
53
|
clearTimeout(connectTimer);
|
|
51
54
|
log.debug(`request ${id} upstream ${backend.id} -> ${resp.status}`);
|
|
52
55
|
const reason = classifyStatus(resp.status);
|
|
53
|
-
health.record(backend.id, reason === null, reason ?? undefined);
|
|
56
|
+
logHealthTransition(log, backend.id, health.record(backend.id, reason === null, reason ?? undefined), `upstream ${resp.status}`);
|
|
57
|
+
// a 401 on a login backend means the token rotated or expired; drop the
|
|
58
|
+
// cached login so the next request re-reads auth.json
|
|
59
|
+
if (resp.status === 401 && backend.login !== undefined)
|
|
60
|
+
invalidateCodexCredentials();
|
|
54
61
|
if (this.aborted)
|
|
55
62
|
return;
|
|
56
63
|
sink.head(resp.status, pickHeaders(resp));
|
|
@@ -70,10 +77,18 @@ export class UpstreamJob {
|
|
|
70
77
|
clearTimeout(connectTimer);
|
|
71
78
|
if (this.aborted || err instanceof SinkClosedError)
|
|
72
79
|
return;
|
|
80
|
+
if (err instanceof CredentialError) {
|
|
81
|
+
// an unreadable login is an auth failure, not a network one: two in a
|
|
82
|
+
// row degrade the backend like any other AUTH loss
|
|
83
|
+
log.warn(`request ${id} upstream ${backend.id}: ${err.message}`);
|
|
84
|
+
logHealthTransition(log, backend.id, health.record(backend.id, false, 'AUTH'));
|
|
85
|
+
sink.error('BACKEND_LOGIN', err.message);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
73
88
|
const timedOut = err instanceof ConnectTimeoutError;
|
|
74
89
|
log.warn(`request ${id} upstream ${backend.id} failed: ${err instanceof Error ? err.message : err}`);
|
|
75
|
-
health.record(backend.id, false, 'NETWORK');
|
|
76
|
-
sink.error(timedOut ? 'BACKEND_TIMEOUT' : 'BACKEND_UNREACHABLE', `upstream ${backend.id} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
90
|
+
logHealthTransition(log, backend.id, health.record(backend.id, false, 'NETWORK'), err instanceof Error ? err.message : undefined);
|
|
91
|
+
sink.error(timedOut ? 'BACKEND_TIMEOUT' : 'BACKEND_UNREACHABLE', `upstream ${backend.id} failed: ${err instanceof Error ? err.message : String(err)}${codexNetworkHint(backend)}`);
|
|
77
92
|
}
|
|
78
93
|
}
|
|
79
94
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"job.js","sourceRoot":"","sources":["../src/job.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"job.js","sourceRoot":"","sources":["../src/job.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,4 +3,4 @@ export declare function probeTarget(backend: ResolvedBackend): {
|
|
|
3
3
|
url: string;
|
|
4
4
|
headers: Record<string, string>;
|
|
5
5
|
};
|
|
6
|
-
export declare function probeBody(model: string, protocol?: 'openai' | 'anthropic' | 'responses'): Record<string, unknown>;
|
|
6
|
+
export declare function probeBody(model: string, protocol?: 'openai' | 'anthropic' | 'responses', login?: boolean): Record<string, unknown>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readCodexCredentials } from './credentials.js';
|
|
1
2
|
export function probeTarget(backend) {
|
|
2
3
|
const base = backend.baseUrl.replace(/\/+$/, '');
|
|
3
4
|
const path = backend.protocol === 'anthropic'
|
|
@@ -6,7 +7,16 @@ export function probeTarget(backend) {
|
|
|
6
7
|
? '/responses'
|
|
7
8
|
: '/chat/completions';
|
|
8
9
|
const headers = { 'content-type': 'application/json' };
|
|
9
|
-
if (backend.
|
|
10
|
+
if (backend.login === 'codex') {
|
|
11
|
+
// same recipe buildUpstreamRequest injects, so a green probe means a
|
|
12
|
+
// working login, not a working key path
|
|
13
|
+
const { accessToken, accountId } = readCodexCredentials();
|
|
14
|
+
headers.authorization = `Bearer ${accessToken}`;
|
|
15
|
+
headers['chatgpt-account-id'] = accountId;
|
|
16
|
+
headers['openai-beta'] = 'responses=experimental';
|
|
17
|
+
headers.originator = 'codex_cli_rs';
|
|
18
|
+
}
|
|
19
|
+
else if (backend.protocol === 'anthropic') {
|
|
10
20
|
if (backend.apiKey)
|
|
11
21
|
headers['x-api-key'] = backend.apiKey;
|
|
12
22
|
headers['anthropic-version'] = '2023-06-01';
|
|
@@ -16,13 +26,27 @@ export function probeTarget(backend) {
|
|
|
16
26
|
}
|
|
17
27
|
return { url: base + path, headers };
|
|
18
28
|
}
|
|
19
|
-
export function probeBody(model, protocol = 'openai') {
|
|
29
|
+
export function probeBody(model, protocol = 'openai', login = false) {
|
|
20
30
|
if (protocol === 'responses') {
|
|
31
|
+
// chatgpt's codex backend rejects stream:false ("Stream must be set to
|
|
32
|
+
// true") and max_output_tokens — a 400 probe would keep a degraded login
|
|
33
|
+
// backend stuck forever, so login probes speak the shape it accepts
|
|
34
|
+
if (login) {
|
|
35
|
+
return {
|
|
36
|
+
model,
|
|
37
|
+
input: [{ role: 'user', content: 'ping' }],
|
|
38
|
+
stream: true,
|
|
39
|
+
store: false,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
21
42
|
return {
|
|
22
43
|
model,
|
|
23
44
|
input: [{ role: 'user', content: 'ping' }],
|
|
24
45
|
max_output_tokens: 1,
|
|
25
46
|
stream: false,
|
|
47
|
+
// never persist a throwaway probe; chatgpt's responses backend also
|
|
48
|
+
// requires store:false on login accounts
|
|
49
|
+
store: false,
|
|
26
50
|
};
|
|
27
51
|
}
|
|
28
52
|
return { model, messages: [{ role: 'user', content: 'ping' }], max_tokens: 1, stream: false };
|