ldrouter 1.16.3 → 1.16.5
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/CHANGELOG.md +58 -0
- package/README.md +23 -1
- package/dist/server/app.js +10 -1
- package/dist/server/auth/middleware.js +37 -1
- package/dist/server/db/index.js +5 -0
- package/dist/server/db/migrate.js +38 -5
- package/dist/server/db/repositories/codex-accounts.js +187 -0
- package/dist/server/db/schema.js +44 -3
- package/dist/server/gateway/runner.js +76 -11
- package/dist/server/providers/codex-autostart.js +98 -0
- package/dist/server/providers/codex-import.js +156 -0
- package/dist/server/providers/codex-oauth.js +77 -0
- package/dist/server/providers/codex-refresh.js +165 -0
- package/dist/server/providers/codex-usage.js +192 -0
- package/dist/server/providers/codex.js +186 -0
- package/dist/server/providers/index.js +5 -0
- package/dist/server/routes/admin/auth.js +4 -1
- package/dist/server/routes/admin/codex.js +331 -0
- package/dist/server/routes/admin/models.js +20 -3
- package/dist/server/routes/admin/providers.js +63 -20
- package/dist/server/routes/admin/requests.js +1 -0
- package/dist/server/routes/admin.js +12 -0
- package/dist/server/routing/capabilities.js +1 -0
- package/dist/server/routing/combo.js +20 -12
- package/dist/server/upstream/client.js +54 -38
- package/dist/web/assets/index-CkDJqDcS.js +391 -0
- package/dist/web/assets/index-Drmqi8MO.css +1 -0
- package/dist/web/index.html +2 -2
- package/migrations/0005_codex_accounts.sql +105 -0
- package/migrations/0006_codex_usage.sql +9 -0
- package/package.json +5 -1
- package/dist/web/assets/index-C5h2WXK5.css +0 -1
- package/dist/web/assets/index-CRnoua24.js +0 -335
|
@@ -2,10 +2,26 @@
|
|
|
2
2
|
import { decryptSecret, decryptCustomHeaders } from '../auth/crypto.js';
|
|
3
3
|
import { GatewayError } from '../errors.js';
|
|
4
4
|
import { buildHeaders, stripSlash } from '../providers/index.js';
|
|
5
|
+
import { redactString } from '../security/redact.js';
|
|
6
|
+
import { getCodexAccountForProvider, getCodexAccountById } from '../db/repositories/codex-accounts.js';
|
|
5
7
|
import { debugUpstream, errorLine, formatError, truncate } from '../logging/debug.js';
|
|
6
|
-
export function providerToUpstreamConfig(p) {
|
|
8
|
+
export function providerToUpstreamConfig(p, codexAccountId) {
|
|
7
9
|
let apiKey;
|
|
8
10
|
let customHeaders;
|
|
11
|
+
if (p.type === 'codex') {
|
|
12
|
+
const account = codexAccountId ? getCodexAccountById(codexAccountId) : getCodexAccountForProvider(p.id);
|
|
13
|
+
if (!account)
|
|
14
|
+
throw new GatewayError('authentication_error', 'No usable Codex account is configured', { status: 503 });
|
|
15
|
+
return {
|
|
16
|
+
type: 'codex', baseUrl: p.baseUrl, customHeaders: {},
|
|
17
|
+
connectTimeoutMs: p.connectTimeoutMs, firstTokenTimeoutMs: p.firstTokenTimeoutMs,
|
|
18
|
+
streamIdleTimeoutMs: p.streamIdleTimeoutMs, totalTimeoutMs: p.totalTimeoutMs,
|
|
19
|
+
codexAccountId: account.chatgptAccountId, accountRecordId: account.id,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (!p.encryptedApiKey || !p.apiKeyNonce) {
|
|
23
|
+
throw new GatewayError('invalid_request_error', 'Provider credentials are missing', { status: 501 });
|
|
24
|
+
}
|
|
9
25
|
try {
|
|
10
26
|
apiKey = decryptSecret({ ciphertext: p.encryptedApiKey, nonce: p.apiKeyNonce, version: p.apiKeyVersion });
|
|
11
27
|
}
|
|
@@ -58,7 +74,7 @@ export async function callUpstreamNonStreaming(cfg, url, payload, requestId = '-
|
|
|
58
74
|
try {
|
|
59
75
|
const res = await fetch(url, {
|
|
60
76
|
method: 'POST',
|
|
61
|
-
headers: { ...buildHeaders(cfg), 'content-type': 'application/json', accept: 'application/json' },
|
|
77
|
+
headers: { ...buildHeaders({ ...cfg, apiKey: cfg.apiKey ?? '' }), 'content-type': 'application/json', accept: 'application/json' },
|
|
62
78
|
body: JSON.stringify(payload),
|
|
63
79
|
signal: ctl.signal,
|
|
64
80
|
});
|
|
@@ -106,7 +122,7 @@ function logUpstreamResponse(requestId, status, statusText, headers, durationMs,
|
|
|
106
122
|
`upstreamRequestId=${extractUpstreamRequestId(headers) ?? 'null'}`,
|
|
107
123
|
]);
|
|
108
124
|
if (status < 200 || status >= 300) {
|
|
109
|
-
errorLine(requestId, 'UPSTREAM ERROR BODY', [truncate(body, 4000)]);
|
|
125
|
+
errorLine(requestId, 'UPSTREAM ERROR BODY', [redactString(truncate(body, 4000))]);
|
|
110
126
|
}
|
|
111
127
|
}
|
|
112
128
|
/**
|
|
@@ -128,7 +144,7 @@ export async function callUpstreamStreaming(cfg, url, payload, onChunk, requestI
|
|
|
128
144
|
try {
|
|
129
145
|
const res = await fetch(url, {
|
|
130
146
|
method: 'POST',
|
|
131
|
-
headers: { ...buildHeaders(cfg), 'content-type': 'application/json', accept: 'text/event-stream' },
|
|
147
|
+
headers: { ...buildHeaders({ ...cfg, apiKey: cfg.apiKey ?? '' }), 'content-type': 'application/json', accept: 'text/event-stream' },
|
|
132
148
|
body: JSON.stringify(payload),
|
|
133
149
|
signal: ctl.signal,
|
|
134
150
|
});
|
|
@@ -145,43 +161,43 @@ export async function callUpstreamStreaming(cfg, url, payload, onChunk, requestI
|
|
|
145
161
|
const decoder = new TextDecoder();
|
|
146
162
|
let buffer = '';
|
|
147
163
|
let isFirst = true;
|
|
148
|
-
let
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
else if (line.startsWith('event:'))
|
|
167
|
-
event = line.slice(6).trimStart();
|
|
168
|
-
}
|
|
169
|
-
if (data === '[DONE]') {
|
|
170
|
-
done = true;
|
|
171
|
-
break;
|
|
172
|
-
}
|
|
173
|
-
if (data) {
|
|
174
|
-
if (ttft === null) {
|
|
175
|
-
ttft = Date.now() - start;
|
|
176
|
-
if (firstTokenTimer)
|
|
177
|
-
clearTimeout(firstTokenTimer);
|
|
178
|
-
}
|
|
179
|
-
resetIdle();
|
|
180
|
-
onChunk({ data, event }, isFirst);
|
|
181
|
-
isFirst = false;
|
|
164
|
+
let streamDone = false;
|
|
165
|
+
const processEvent = (rawEvent) => {
|
|
166
|
+
const lines = rawEvent.split(/\r?\n/);
|
|
167
|
+
let data = '';
|
|
168
|
+
let event = 'message';
|
|
169
|
+
for (const line of lines) {
|
|
170
|
+
if (line.startsWith('data:'))
|
|
171
|
+
data = line.slice(5).trimStart();
|
|
172
|
+
else if (line.startsWith('event:'))
|
|
173
|
+
event = line.slice(6).trimStart();
|
|
174
|
+
}
|
|
175
|
+
if (data === '[DONE]')
|
|
176
|
+
return;
|
|
177
|
+
if (data) {
|
|
178
|
+
if (ttft === null) {
|
|
179
|
+
ttft = Date.now() - start;
|
|
180
|
+
if (firstTokenTimer)
|
|
181
|
+
clearTimeout(firstTokenTimer);
|
|
182
182
|
}
|
|
183
|
+
resetIdle();
|
|
184
|
+
onChunk({ data, event }, isFirst);
|
|
185
|
+
isFirst = false;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
while (!streamDone) {
|
|
189
|
+
const part = await reader.read();
|
|
190
|
+
streamDone = part.done;
|
|
191
|
+
buffer += decoder.decode(part.value ?? new Uint8Array(), { stream: streamDone });
|
|
192
|
+
let idx;
|
|
193
|
+
while ((idx = buffer.search(/\r?\n\r?\n/)) >= 0) {
|
|
194
|
+
const separatorLength = buffer[idx] === '\r' ? 4 : 2;
|
|
195
|
+
processEvent(buffer.slice(0, idx));
|
|
196
|
+
buffer = buffer.slice(idx + separatorLength);
|
|
183
197
|
}
|
|
184
198
|
}
|
|
199
|
+
if (buffer.trim())
|
|
200
|
+
processEvent(buffer.replace(/\r?\n$/, ''));
|
|
185
201
|
return {
|
|
186
202
|
headers: Object.fromEntries(res.headers.entries()),
|
|
187
203
|
upstreamRequestId: extractUpstreamRequestId(res.headers),
|