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.
Files changed (33) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +23 -1
  3. package/dist/server/app.js +10 -1
  4. package/dist/server/auth/middleware.js +37 -1
  5. package/dist/server/db/index.js +5 -0
  6. package/dist/server/db/migrate.js +38 -5
  7. package/dist/server/db/repositories/codex-accounts.js +187 -0
  8. package/dist/server/db/schema.js +44 -3
  9. package/dist/server/gateway/runner.js +76 -11
  10. package/dist/server/providers/codex-autostart.js +98 -0
  11. package/dist/server/providers/codex-import.js +156 -0
  12. package/dist/server/providers/codex-oauth.js +77 -0
  13. package/dist/server/providers/codex-refresh.js +165 -0
  14. package/dist/server/providers/codex-usage.js +192 -0
  15. package/dist/server/providers/codex.js +186 -0
  16. package/dist/server/providers/index.js +5 -0
  17. package/dist/server/routes/admin/auth.js +4 -1
  18. package/dist/server/routes/admin/codex.js +331 -0
  19. package/dist/server/routes/admin/models.js +20 -3
  20. package/dist/server/routes/admin/providers.js +63 -20
  21. package/dist/server/routes/admin/requests.js +1 -0
  22. package/dist/server/routes/admin.js +12 -0
  23. package/dist/server/routing/capabilities.js +1 -0
  24. package/dist/server/routing/combo.js +20 -12
  25. package/dist/server/upstream/client.js +54 -38
  26. package/dist/web/assets/index-CkDJqDcS.js +391 -0
  27. package/dist/web/assets/index-Drmqi8MO.css +1 -0
  28. package/dist/web/index.html +2 -2
  29. package/migrations/0005_codex_accounts.sql +105 -0
  30. package/migrations/0006_codex_usage.sql +9 -0
  31. package/package.json +5 -1
  32. package/dist/web/assets/index-C5h2WXK5.css +0 -1
  33. 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 done = false;
149
- while (!done) {
150
- const { value, done: streamDone } = await reader.read();
151
- done = streamDone;
152
- if (done)
153
- break;
154
- buffer += decoder.decode(value, { stream: true });
155
- // Process complete SSE events (split on blank line)
156
- let idx;
157
- while ((idx = buffer.indexOf('\n\n')) >= 0) {
158
- const rawEvent = buffer.slice(0, idx);
159
- buffer = buffer.slice(idx + 2);
160
- const lines = rawEvent.split('\n');
161
- let data = '';
162
- let event = 'message';
163
- for (const line of lines) {
164
- if (line.startsWith('data:'))
165
- data = line.slice(5).trimStart();
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),