ldrouter 1.16.2 → 1.16.3
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 +9 -38
- package/README.md +1 -23
- package/dist/server/app.js +1 -10
- package/dist/server/auth/middleware.js +1 -37
- package/dist/server/db/index.js +0 -5
- package/dist/server/db/migrate.js +5 -38
- package/dist/server/db/schema.js +3 -44
- package/dist/server/errors.js +11 -0
- package/dist/server/gateway/runner.js +64 -126
- package/dist/server/protocols/anthropic.js +5 -3
- package/dist/server/protocols/canonical.js +29 -8
- package/dist/server/providers/index.js +25 -8
- package/dist/server/routes/admin/auth.js +1 -4
- package/dist/server/routes/admin/combos.js +98 -48
- package/dist/server/routes/admin/models.js +34 -24
- package/dist/server/routes/admin/providers.js +20 -63
- package/dist/server/routes/admin/requests.js +0 -1
- package/dist/server/routes/admin.js +0 -12
- package/dist/server/routes/gateway/anthropic.js +3 -3
- package/dist/server/routes/gateway/openai.js +5 -5
- package/dist/server/routing/capabilities.js +73 -14
- package/dist/server/routing/combo.js +20 -39
- package/dist/server/routing/resolver.js +16 -11
- package/dist/server/upstream/client.js +55 -61
- package/dist/web/assets/index-C5h2WXK5.css +1 -0
- package/dist/web/assets/index-CRnoua24.js +335 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -5
- package/dist/server/db/repositories/codex-accounts.js +0 -187
- package/dist/server/providers/codex-autostart.js +0 -98
- package/dist/server/providers/codex-import.js +0 -156
- package/dist/server/providers/codex-oauth.js +0 -77
- package/dist/server/providers/codex-refresh.js +0 -165
- package/dist/server/providers/codex-usage.js +0 -192
- package/dist/server/providers/codex.js +0 -186
- package/dist/server/routes/admin/codex.js +0 -331
- package/dist/web/assets/index-Coy-u6h8.css +0 -1
- package/dist/web/assets/index-qDG5c6aL.js +0 -386
- package/migrations/0005_codex_accounts.sql +0 -105
- package/migrations/0006_codex_usage.sql +0 -9
|
@@ -33,6 +33,35 @@ export function deriveRequiredCapabilities(req) {
|
|
|
33
33
|
responses: false,
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* The single table of "explicitly unsupported" checks. `modelMeets` and the
|
|
38
|
+
* rejection reporter both read it, so they can never disagree about a model.
|
|
39
|
+
* `reasoning` is deliberately absent: it is advisory metadata, because an
|
|
40
|
+
* upstream may support reasoning even when discovery cannot identify it.
|
|
41
|
+
*/
|
|
42
|
+
const CAPABILITY_CHECKS = [
|
|
43
|
+
{ flag: 'streaming', required: 'streaming', reason: 'streaming', label: 'no streaming' },
|
|
44
|
+
{ flag: 'tools', required: 'tools', reason: 'tools', label: 'no tool calling' },
|
|
45
|
+
{ flag: 'structured_output', required: 'structuredOutput', reason: 'structured_output', label: 'no structured output' },
|
|
46
|
+
{ flag: 'image_input', required: 'imageInput', reason: 'image_input', label: 'no image input' },
|
|
47
|
+
{ flag: 'audio_input', required: 'audioInput', reason: 'audio_input', label: 'no audio input' },
|
|
48
|
+
{ flag: 'responses', required: 'responses', reason: 'responses', label: 'no Responses API support' },
|
|
49
|
+
];
|
|
50
|
+
const NON_CAPABILITY_TEXT = {
|
|
51
|
+
model_not_found: 'model not found',
|
|
52
|
+
provider_not_found: 'provider not found',
|
|
53
|
+
provider_disabled: 'provider disabled',
|
|
54
|
+
model_disabled: 'model disabled',
|
|
55
|
+
upstream_unavailable: 'not available upstream',
|
|
56
|
+
circuit_open: 'provider circuit is open',
|
|
57
|
+
combo_disabled: 'combo disabled',
|
|
58
|
+
member_disabled: 'disabled in this combo',
|
|
59
|
+
};
|
|
60
|
+
const REASON_TEXT = {
|
|
61
|
+
...NON_CAPABILITY_TEXT,
|
|
62
|
+
...Object.fromEntries(CAPABILITY_CHECKS.map((c) => [c.reason, c.label])),
|
|
63
|
+
};
|
|
64
|
+
const CAPABILITY_REASONS = new Set(CAPABILITY_CHECKS.map((c) => c.reason));
|
|
36
65
|
/**
|
|
37
66
|
* Check if a model meets required capabilities.
|
|
38
67
|
* IMPORTANT: Treat undefined as "unknown" rather than "unsupported".
|
|
@@ -43,18 +72,48 @@ export function deriveRequiredCapabilities(req) {
|
|
|
43
72
|
* upstream may support it even when discovery cannot identify it.
|
|
44
73
|
*/
|
|
45
74
|
export function modelMeets(caps, req) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
75
|
+
return firstMissingCapability(caps, req) === null;
|
|
76
|
+
}
|
|
77
|
+
/** The first capability this model is KNOWN not to support (undefined = unknown
|
|
78
|
+
* caps never reject), or null when nothing required is explicitly missing. */
|
|
79
|
+
export function firstMissingCapability(caps, req) {
|
|
80
|
+
for (const c of CAPABILITY_CHECKS) {
|
|
81
|
+
if (req[c.required] && caps[c.flag] === false)
|
|
82
|
+
return c.reason;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* `vl/gpt-5.5` is reported to clients as `gpt-5.5`: the provider prefix is the
|
|
88
|
+
* gateway's bookkeeping, not the operator's or the API client's vocabulary.
|
|
89
|
+
*/
|
|
90
|
+
export function bareModelName(publicModelId) {
|
|
91
|
+
const i = publicModelId.indexOf('/');
|
|
92
|
+
return i === -1 ? publicModelId : publicModelId.slice(i + 1);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Turn per-model rejection reasons into one client-facing error that names every
|
|
96
|
+
* excluded model and why. The old blanket strings ("No combo member satisfies
|
|
97
|
+
* the request capabilities or availability", "No available model candidates")
|
|
98
|
+
* told the caller nothing it could act on — docs/13 §10 records that as a bug.
|
|
99
|
+
*
|
|
100
|
+
* A capability miss is deterministic, so it wins the status code (400); reasons
|
|
101
|
+
* of mere *availability* are still listed so nothing is hidden. When no
|
|
102
|
+
* capability was involved the targetis simply unavailable (502).
|
|
103
|
+
*/
|
|
104
|
+
export function describeRejections(target, rejected) {
|
|
105
|
+
const groups = new Map();
|
|
106
|
+
for (const r of rejected) {
|
|
107
|
+
const label = REASON_TEXT[r.reason] ?? r.reason;
|
|
108
|
+
groups.set(label, [...(groups.get(label) ?? []), bareModelName(r.publicModelId)]);
|
|
109
|
+
}
|
|
110
|
+
// A direct model is also the target, so naming it twice would only be noise.
|
|
111
|
+
const detail = target.kind === 'model'
|
|
112
|
+
? [...groups.keys()].join('; ')
|
|
113
|
+
: [...groups].map(([label, names]) => `${[...new Set(names)].join(', ')} (${label})`).join('; ');
|
|
114
|
+
const subject = target.kind === 'combo' ? `Combo "${target.publicModelId}"` : `Model "${bareModelName(target.publicModelId)}"`;
|
|
115
|
+
const message = `${subject} cannot serve this request: ${detail || 'no usable member'}`;
|
|
116
|
+
return rejected.some((r) => CAPABILITY_REASONS.has(r.reason))
|
|
117
|
+
? { message, type: 'capability_not_supported', status: 400 }
|
|
118
|
+
: { message, type: 'upstream_unavailable', status: 502 };
|
|
60
119
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Combo routing: fallback (ordered) or weighted round-robin.
|
|
2
2
|
import { eq } from 'drizzle-orm';
|
|
3
3
|
import { getDb, schema } from '../db/index.js';
|
|
4
|
-
import {
|
|
4
|
+
import { firstMissingCapability } from './capabilities.js';
|
|
5
5
|
export function loadCombo(comboId) {
|
|
6
6
|
const db = getDb();
|
|
7
7
|
const c = db.select().from(schema.combos).where(eq(schema.combos.id, comboId)).get();
|
|
@@ -23,13 +23,6 @@ export function loadCombo(comboId) {
|
|
|
23
23
|
},
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
-
export function expandCodexAccountCandidates(candidate, accounts, now = new Date()) {
|
|
27
|
-
if (!candidate.publicModelId.startsWith('codex/'))
|
|
28
|
-
return [candidate];
|
|
29
|
-
const usable = accounts.filter((a) => a.enabled && (a.healthState === 'healthy' || a.healthState === 'unknown') && Date.parse(a.tokenExpiresAt) > now.getTime())
|
|
30
|
-
.sort((a, b) => a.priority - b.priority || a.id.localeCompare(b.id));
|
|
31
|
-
return usable.map((a) => ({ ...candidate, codexAccountId: a.id, codexChatgptAccountId: a.chatgptAccountId, selectionReason: 'codex_account' }));
|
|
32
|
-
}
|
|
33
26
|
export function selectCandidates(combo, allModels, req, onReject) {
|
|
34
27
|
// Resolve each combo member to a candidate and apply filters
|
|
35
28
|
const map = new Map(allModels.map((m) => [m.modelId, m]));
|
|
@@ -44,6 +37,10 @@ export function selectCandidates(combo, allModels, req, onReject) {
|
|
|
44
37
|
onReject?.({ modelId: m.modelId, publicModelId: m.modelId }, 'model_not_found');
|
|
45
38
|
continue;
|
|
46
39
|
}
|
|
40
|
+
if (c.providerEnabled === false) {
|
|
41
|
+
onReject?.(c, 'provider_disabled');
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
47
44
|
if (!c.enabled) {
|
|
48
45
|
onReject?.(c, 'model_disabled');
|
|
49
46
|
continue;
|
|
@@ -56,30 +53,15 @@ export function selectCandidates(combo, allModels, req, onReject) {
|
|
|
56
53
|
onReject?.(c, 'circuit_open');
|
|
57
54
|
continue;
|
|
58
55
|
}
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
const missing = firstMissingCapability(c.capabilities, req);
|
|
57
|
+
if (missing) {
|
|
58
|
+
onReject?.(c, missing);
|
|
61
59
|
continue;
|
|
62
60
|
}
|
|
63
61
|
candidates.push(c);
|
|
64
62
|
}
|
|
65
63
|
return candidates;
|
|
66
64
|
}
|
|
67
|
-
/** First capability that explicitly failed (undefined = unknown caps never reject). */
|
|
68
|
-
function capabilityRejection(caps, req) {
|
|
69
|
-
if (req.streaming && caps.streaming === false)
|
|
70
|
-
return 'streaming';
|
|
71
|
-
if (req.tools && caps.tools === false)
|
|
72
|
-
return 'tools';
|
|
73
|
-
if (req.structuredOutput && caps.structured_output === false)
|
|
74
|
-
return 'structured_output';
|
|
75
|
-
if (req.imageInput && caps.image_input === false)
|
|
76
|
-
return 'image_input';
|
|
77
|
-
if (req.audioInput && caps.audio_input === false)
|
|
78
|
-
return 'audio_input';
|
|
79
|
-
if (req.responses && caps.responses === false)
|
|
80
|
-
return 'responses';
|
|
81
|
-
return 'capability_mismatch';
|
|
82
|
-
}
|
|
83
65
|
export function orderCandidates(combo, candidates) {
|
|
84
66
|
if (combo.mode === 'fallback') {
|
|
85
67
|
// Preserve declared position order
|
|
@@ -91,25 +73,24 @@ export function orderCandidates(combo, candidates) {
|
|
|
91
73
|
}
|
|
92
74
|
// Weighted round-robin: stable order with weighted lead bias.
|
|
93
75
|
// We rotate via a process-local cursor keyed by combo id.
|
|
94
|
-
const cursor = nextCursor(combo.comboId,
|
|
76
|
+
const cursor = nextCursor(combo.comboId, candidates);
|
|
95
77
|
return cursor;
|
|
96
78
|
}
|
|
97
79
|
const comboCursors = new Map();
|
|
98
|
-
function nextCursor(comboId,
|
|
80
|
+
function nextCursor(comboId, candidates) {
|
|
99
81
|
if (candidates.length === 0)
|
|
100
82
|
return [];
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const cur = (comboCursors.get(comboId) ?? 0) %
|
|
83
|
+
// Compute total weight of available candidates
|
|
84
|
+
const totalWeight = candidates.reduce((s, c) => {
|
|
85
|
+
const m = (candidates.find((x) => x.modelId === c.modelId));
|
|
86
|
+
void m;
|
|
87
|
+
return s + 1; // weight normalization happens upstream
|
|
88
|
+
}, 0);
|
|
89
|
+
void totalWeight;
|
|
90
|
+
// Simple modulo rotation for determinism in tests
|
|
91
|
+
const cur = (comboCursors.get(comboId) ?? 0) % candidates.length;
|
|
110
92
|
comboCursors.set(comboId, cur + 1);
|
|
111
|
-
|
|
112
|
-
return [selected, ...candidates.filter((candidate) => candidate !== selected)];
|
|
93
|
+
return [...candidates.slice(cur), ...candidates.slice(0, cur)];
|
|
113
94
|
}
|
|
114
95
|
export function shouldFallback(combo, reason) {
|
|
115
96
|
switch (reason.type) {
|
|
@@ -2,37 +2,42 @@
|
|
|
2
2
|
import { eq, and } from 'drizzle-orm';
|
|
3
3
|
import { getDb, schema } from '../db/index.js';
|
|
4
4
|
import { GatewayError } from '../errors.js';
|
|
5
|
+
/**
|
|
6
|
+
* Resolution is deliberately blind to `enabled`. A disabled model is NOT an
|
|
7
|
+
* unknown model: reporting it as `Unknown model: X` (404) sent operators hunting
|
|
8
|
+
* for a typo when the real answer was "you turned this off". The `enabled` flag
|
|
9
|
+
* travels with the target so the caller can say which it is.
|
|
10
|
+
*/
|
|
5
11
|
export function resolveRequestedModel(requested) {
|
|
6
12
|
const db = getDb();
|
|
7
13
|
// Physical model
|
|
8
14
|
const model = db.select().from(schema.models).where(eq(schema.models.publicModelId, requested)).get();
|
|
9
|
-
if (model
|
|
10
|
-
return { kind: 'model', modelId: model.id, publicModelId: model.publicModelId };
|
|
11
|
-
}
|
|
15
|
+
if (model)
|
|
16
|
+
return { kind: 'model', modelId: model.id, publicModelId: model.publicModelId, enabled: model.enabled };
|
|
12
17
|
// Combo by exact public ID — with-prefix ("combo/<slug>") or the
|
|
13
18
|
// prefix-less default (<slug>).
|
|
14
19
|
const combo = db.select().from(schema.combos).where(eq(schema.combos.publicModelId, requested)).get();
|
|
15
|
-
if (combo
|
|
16
|
-
return { kind: 'combo', comboId: combo.id, publicModelId: combo.publicModelId };
|
|
20
|
+
if (combo)
|
|
21
|
+
return { kind: 'combo', comboId: combo.id, publicModelId: combo.publicModelId, enabled: combo.enabled };
|
|
17
22
|
// Alias (one hop only)
|
|
18
23
|
const alias = db.select().from(schema.modelAliases).where(and(eq(schema.modelAliases.alias, requested), eq(schema.modelAliases.enabled, true))).get();
|
|
19
24
|
if (alias) {
|
|
20
25
|
if (alias.targetKind === 'model') {
|
|
21
26
|
const m = db.select().from(schema.models).where(eq(schema.models.id, alias.targetId)).get();
|
|
22
|
-
if (m
|
|
23
|
-
return { kind: 'alias', aliasId: alias.id, alias: alias.alias, resolved: { kind: 'model', modelId: m.id, publicModelId: m.publicModelId } };
|
|
27
|
+
if (m)
|
|
28
|
+
return { kind: 'alias', aliasId: alias.id, alias: alias.alias, resolved: { kind: 'model', modelId: m.id, publicModelId: m.publicModelId, enabled: m.enabled } };
|
|
24
29
|
}
|
|
25
30
|
else {
|
|
26
31
|
const c = db.select().from(schema.combos).where(eq(schema.combos.id, alias.targetId)).get();
|
|
27
|
-
if (c
|
|
28
|
-
return { kind: 'alias', aliasId: alias.id, alias: alias.alias, resolved: { kind: 'combo', comboId: c.id, publicModelId: c.publicModelId } };
|
|
32
|
+
if (c)
|
|
33
|
+
return { kind: 'alias', aliasId: alias.id, alias: alias.alias, resolved: { kind: 'combo', comboId: c.id, publicModelId: c.publicModelId, enabled: c.enabled } };
|
|
29
34
|
}
|
|
30
35
|
}
|
|
31
36
|
// Maybe the user typed the combo slug without the prefix
|
|
32
37
|
if (!requested.includes('/')) {
|
|
33
38
|
const c = db.select().from(schema.combos).where(eq(schema.combos.slug, requested)).get();
|
|
34
|
-
if (c
|
|
35
|
-
return { kind: 'combo', comboId: c.id, publicModelId: c.publicModelId };
|
|
39
|
+
if (c)
|
|
40
|
+
return { kind: 'combo', comboId: c.id, publicModelId: c.publicModelId, enabled: c.enabled };
|
|
36
41
|
}
|
|
37
42
|
throw new GatewayError('model_not_found', `Unknown model: ${requested}`, { status: 404 });
|
|
38
43
|
}
|
|
@@ -2,26 +2,10 @@
|
|
|
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';
|
|
7
5
|
import { debugUpstream, errorLine, formatError, truncate } from '../logging/debug.js';
|
|
8
|
-
export function providerToUpstreamConfig(p
|
|
6
|
+
export function providerToUpstreamConfig(p) {
|
|
9
7
|
let apiKey;
|
|
10
8
|
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
|
-
}
|
|
25
9
|
try {
|
|
26
10
|
apiKey = decryptSecret({ ciphertext: p.encryptedApiKey, nonce: p.apiKeyNonce, version: p.apiKeyVersion });
|
|
27
11
|
}
|
|
@@ -51,6 +35,22 @@ export function providerToUpstreamConfig(p, codexAccountId) {
|
|
|
51
35
|
totalTimeoutMs: p.totalTimeoutMs,
|
|
52
36
|
};
|
|
53
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* One conversion of an upstream HTTP status into a gateway error, shared by the
|
|
40
|
+
* streaming and non-streaming paths so both report identical type/code/status.
|
|
41
|
+
* Before this, the same upstream 429 arrived as "Upstream rate limited" over a
|
|
42
|
+
* stream and "Upstream rate limited (HTTP 429)" without one, with a different
|
|
43
|
+
* code (or none) each time.
|
|
44
|
+
*/
|
|
45
|
+
export function upstreamHttpError(status, bodyExcerpt, cause) {
|
|
46
|
+
if (status >= 500)
|
|
47
|
+
return new GatewayError('upstream_error', `Upstream HTTP ${status}`, { status: 502, cause, code: `upstream_http_${status}` });
|
|
48
|
+
if (status === 429)
|
|
49
|
+
return new GatewayError('upstream_rate_limit', `Upstream rate limited (HTTP ${status})`, { status: 429, cause, code: 'upstream_http_429' });
|
|
50
|
+
if (status === 401 || status === 403)
|
|
51
|
+
return new GatewayError('upstream_auth_error', `Upstream authentication failed (HTTP ${status})`, { status: 502, cause, code: `upstream_http_${status}` });
|
|
52
|
+
return new GatewayError('upstream_error', `Upstream HTTP ${status}: ${bodyExcerpt}`, { status: 502, cause, code: `upstream_http_${status}` });
|
|
53
|
+
}
|
|
54
54
|
export async function callUpstreamNonStreaming(cfg, url, payload, requestId = '-') {
|
|
55
55
|
const ctl = new AbortController();
|
|
56
56
|
const timer = setTimeout(() => ctl.abort(), cfg.totalTimeoutMs);
|
|
@@ -58,7 +58,7 @@ export async function callUpstreamNonStreaming(cfg, url, payload, requestId = '-
|
|
|
58
58
|
try {
|
|
59
59
|
const res = await fetch(url, {
|
|
60
60
|
method: 'POST',
|
|
61
|
-
headers: { ...buildHeaders(
|
|
61
|
+
headers: { ...buildHeaders(cfg), 'content-type': 'application/json', accept: 'application/json' },
|
|
62
62
|
body: JSON.stringify(payload),
|
|
63
63
|
signal: ctl.signal,
|
|
64
64
|
});
|
|
@@ -106,7 +106,7 @@ function logUpstreamResponse(requestId, status, statusText, headers, durationMs,
|
|
|
106
106
|
`upstreamRequestId=${extractUpstreamRequestId(headers) ?? 'null'}`,
|
|
107
107
|
]);
|
|
108
108
|
if (status < 200 || status >= 300) {
|
|
109
|
-
errorLine(requestId, 'UPSTREAM ERROR BODY', [
|
|
109
|
+
errorLine(requestId, 'UPSTREAM ERROR BODY', [truncate(body, 4000)]);
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
/**
|
|
@@ -128,7 +128,7 @@ export async function callUpstreamStreaming(cfg, url, payload, onChunk, requestI
|
|
|
128
128
|
try {
|
|
129
129
|
const res = await fetch(url, {
|
|
130
130
|
method: 'POST',
|
|
131
|
-
headers: { ...buildHeaders(
|
|
131
|
+
headers: { ...buildHeaders(cfg), 'content-type': 'application/json', accept: 'text/event-stream' },
|
|
132
132
|
body: JSON.stringify(payload),
|
|
133
133
|
signal: ctl.signal,
|
|
134
134
|
});
|
|
@@ -145,43 +145,43 @@ export async function callUpstreamStreaming(cfg, url, payload, onChunk, requestI
|
|
|
145
145
|
const decoder = new TextDecoder();
|
|
146
146
|
let buffer = '';
|
|
147
147
|
let isFirst = true;
|
|
148
|
-
let
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
else if (line.startsWith('event:'))
|
|
157
|
-
event = line.slice(6).trimStart();
|
|
158
|
-
}
|
|
159
|
-
if (data === '[DONE]')
|
|
160
|
-
return;
|
|
161
|
-
if (data) {
|
|
162
|
-
if (ttft === null) {
|
|
163
|
-
ttft = Date.now() - start;
|
|
164
|
-
if (firstTokenTimer)
|
|
165
|
-
clearTimeout(firstTokenTimer);
|
|
166
|
-
}
|
|
167
|
-
resetIdle();
|
|
168
|
-
onChunk({ data, event }, isFirst);
|
|
169
|
-
isFirst = false;
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
while (!streamDone) {
|
|
173
|
-
const part = await reader.read();
|
|
174
|
-
streamDone = part.done;
|
|
175
|
-
buffer += decoder.decode(part.value ?? new Uint8Array(), { stream: streamDone });
|
|
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)
|
|
176
156
|
let idx;
|
|
177
|
-
while ((idx = buffer.
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
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;
|
|
182
|
+
}
|
|
181
183
|
}
|
|
182
184
|
}
|
|
183
|
-
if (buffer.trim())
|
|
184
|
-
processEvent(buffer.replace(/\r?\n$/, ''));
|
|
185
185
|
return {
|
|
186
186
|
headers: Object.fromEntries(res.headers.entries()),
|
|
187
187
|
upstreamRequestId: extractUpstreamRequestId(res.headers),
|
|
@@ -190,13 +190,7 @@ export async function callUpstreamStreaming(cfg, url, payload, onChunk, requestI
|
|
|
190
190
|
}
|
|
191
191
|
catch (e) {
|
|
192
192
|
if (e instanceof UpstreamHttpError) {
|
|
193
|
-
|
|
194
|
-
throw new GatewayError('upstream_error', `Upstream HTTP ${e.status}`, { status: 502, cause: e, code: 'upstream_http_' + e.status });
|
|
195
|
-
if (e.status === 429)
|
|
196
|
-
throw new GatewayError('upstream_rate_limit', 'Upstream rate limited', { status: 529, cause: e });
|
|
197
|
-
if (e.status === 401 || e.status === 403)
|
|
198
|
-
throw new GatewayError('upstream_auth_error', 'Upstream authentication failed', { status: 502, cause: e });
|
|
199
|
-
throw new GatewayError('upstream_error', `Upstream HTTP ${e.status}: ${e.bodyExcerpt}`, { status: 502, cause: e });
|
|
193
|
+
throw upstreamHttpError(e.status, e.bodyExcerpt, e);
|
|
200
194
|
}
|
|
201
195
|
const err = e;
|
|
202
196
|
errorLine(requestId, 'UPSTREAM FETCH ERROR', [
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--background: 0 0% 100%;--foreground: 240 10% 3.9%;--card: 0 0% 100%;--card-foreground: 240 10% 3.9%;--popover: 0 0% 100%;--popover-foreground: 240 10% 3.9%;--primary: 341 86% 41%;--primary-foreground: 0 0% 100%;--secondary: 240 4.8% 95.9%;--secondary-foreground: 240 5.9% 10%;--muted: 240 4.8% 95.9%;--muted-foreground: 240 3.8% 46.1%;--accent: 240 4.8% 95.9%;--accent-foreground: 240 5.9% 10%;--destructive: 0 84.2% 60.2%;--destructive-foreground: 0 0% 98%;--border: 240 5.9% 90%;--input: 240 5.9% 90%;--ring: 341 86% 41%;--sidebar: 240 5.9% 98%;--sidebar-foreground: 240 10% 3.9%;--radius: .5rem}.dark{--background: 240 10% 3.9%;--foreground: 0 0% 98%;--card: 240 10% 6%;--card-foreground: 0 0% 98%;--popover: 240 10% 5%;--popover-foreground: 0 0% 98%;--primary: 341 86% 55%;--primary-foreground: 0 0% 100%;--secondary: 240 3.7% 15.9%;--secondary-foreground: 0 0% 98%;--muted: 240 3.7% 15.9%;--muted-foreground: 240 5% 64.9%;--accent: 240 3.7% 15.9%;--accent-foreground: 0 0% 98%;--destructive: 0 72% 51%;--destructive-foreground: 0 0% 98%;--border: 240 3.7% 15.9%;--input: 240 3.7% 15.9%;--ring: 341 86% 55%;--sidebar: 240 10% 5%;--sidebar-foreground: 0 0% 98%}*{border-color:hsl(var(--border))}body{background-color:hsl(var(--background));color:hsl(var(--foreground));font-feature-settings:"rlig" 1,"calt" 1}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.left-1\/2{left:50%}.left-2{left:.5rem}.left-2\.5{left:.625rem}.left-\[50\%\]{left:50%}.right-2{right:.5rem}.right-4{right:1rem}.top-1\/2{top:50%}.top-16{top:4rem}.top-2\.5{top:.625rem}.top-4{top:1rem}.top-\[50\%\]{top:50%}.z-50{z-index:50}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.my-1{margin-top:.25rem;margin-bottom:.25rem}.mb-1{margin-bottom:.25rem}.mb-3{margin-bottom:.75rem}.mb-6{margin-bottom:1.5rem}.ml-0\.5{margin-left:.125rem}.ml-1{margin-left:.25rem}.ml-1\.5{margin-left:.375rem}.ml-2{margin-left:.5rem}.mr-0\.5{margin-right:.125rem}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-20{height:5rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-40{height:10rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-auto{height:auto}.h-px{height:1px}.h-screen{height:100vh}.max-h-40{max-height:10rem}.max-h-48{max-height:12rem}.max-h-60{max-height:15rem}.max-h-80{max-height:20rem}.max-h-96{max-height:24rem}.max-h-\[calc\(100vh-8rem\)\]{max-height:calc(100vh - 8rem)}.min-h-\[60px\]{min-height:60px}.min-h-screen{min-height:100vh}.w-1\.5{width:.375rem}.w-2{width:.5rem}.w-20{width:5rem}.w-28{width:7rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-32{width:8rem}.w-36{width:9rem}.w-4{width:1rem}.w-40{width:10rem}.w-56{width:14rem}.w-60{width:15rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-80{width:20rem}.w-9{width:2.25rem}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-\[8rem\]{min-width:8rem}.min-w-\[var\(--radix-select-trigger-width\)\]{min-width:var(--radix-select-trigger-width)}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.caption-bottom{caption-side:bottom}.translate-x-\[-50\%\]{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-\[-50\%\]{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-rotate-90{--tw-rotate: -90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.items-stretch{align-items:stretch}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.25rem * var(--tw-space-x-reverse));margin-left:calc(.25rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-amber-200{--tw-border-opacity: 1;border-color:rgb(253 230 138 / var(--tw-border-opacity, 1))}.border-amber-500\/40{border-color:#f59e0b66}.border-amber-600\/50{border-color:#d9770680}.border-border{border-color:hsl(var(--border))}.border-destructive{border-color:hsl(var(--destructive))}.border-destructive\/40{border-color:hsl(var(--destructive) / .4)}.border-input{border-color:hsl(var(--input))}.border-primary{border-color:hsl(var(--primary))}.border-transparent{border-color:transparent}.bg-amber-400{--tw-bg-opacity: 1;background-color:rgb(251 191 36 / var(--tw-bg-opacity, 1))}.bg-amber-50{--tw-bg-opacity: 1;background-color:rgb(255 251 235 / var(--tw-bg-opacity, 1))}.bg-amber-500{--tw-bg-opacity: 1;background-color:rgb(245 158 11 / var(--tw-bg-opacity, 1))}.bg-amber-500\/90{background-color:#f59e0be6}.bg-background{background-color:hsl(var(--background))}.bg-black\/15{background-color:#00000026}.bg-black\/80{background-color:#000c}.bg-card{background-color:hsl(var(--card))}.bg-destructive{background-color:hsl(var(--destructive))}.bg-destructive\/10{background-color:hsl(var(--destructive) / .1)}.bg-emerald-100{--tw-bg-opacity: 1;background-color:rgb(209 250 229 / var(--tw-bg-opacity, 1))}.bg-emerald-400{--tw-bg-opacity: 1;background-color:rgb(52 211 153 / var(--tw-bg-opacity, 1))}.bg-emerald-600{--tw-bg-opacity: 1;background-color:rgb(5 150 105 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-muted{background-color:hsl(var(--muted))}.bg-popover{background-color:hsl(var(--popover))}.bg-primary{background-color:hsl(var(--primary))}.bg-primary\/10{background-color:hsl(var(--primary) / .1)}.bg-primary\/70{background-color:hsl(var(--primary) / .7)}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.bg-red-400{--tw-bg-opacity: 1;background-color:rgb(248 113 113 / var(--tw-bg-opacity, 1))}.bg-secondary{background-color:hsl(var(--secondary))}.bg-sidebar{background-color:hsl(var(--sidebar))}.bg-transparent{background-color:transparent}.fill-amber-400{fill:#fbbf24}.fill-amber-500\/10{fill:#f59e0b1a}.fill-emerald-400{fill:#34d399}.fill-emerald-500{fill:#10b981}.fill-foreground{fill:hsl(var(--foreground))}.fill-muted-foreground{fill:hsl(var(--muted-foreground))}.fill-primary\/10{fill:hsl(var(--primary) / .1)}.fill-red-400{fill:#f87171}.fill-red-500{fill:#ef4444}.stroke-amber-500{stroke:#f59e0b}.stroke-emerald-500{stroke:#10b981}.stroke-primary{stroke:hsl(var(--primary))}.object-contain{-o-object-fit:contain;object-fit:contain}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pb-6{padding-bottom:1.5rem}.pl-8{padding-left:2rem}.pr-2{padding-right:.5rem}.pt-0{padding-top:0}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.align-text-bottom{vertical-align:text-bottom}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.tabular-nums{--tw-numeric-spacing: tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-none{line-height:1}.tracking-tight{letter-spacing:-.025em}.text-amber-500{--tw-text-opacity: 1;color:rgb(245 158 11 / var(--tw-text-opacity, 1))}.text-amber-600{--tw-text-opacity: 1;color:rgb(217 119 6 / var(--tw-text-opacity, 1))}.text-amber-800{--tw-text-opacity: 1;color:rgb(146 64 14 / var(--tw-text-opacity, 1))}.text-amber-900{--tw-text-opacity: 1;color:rgb(120 53 15 / var(--tw-text-opacity, 1))}.text-amber-950{--tw-text-opacity: 1;color:rgb(69 26 3 / var(--tw-text-opacity, 1))}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.text-card-foreground{color:hsl(var(--card-foreground))}.text-current{color:currentColor}.text-destructive{color:hsl(var(--destructive))}.text-destructive-foreground{color:hsl(var(--destructive-foreground))}.text-emerald-500{--tw-text-opacity: 1;color:rgb(16 185 129 / var(--tw-text-opacity, 1))}.text-emerald-700{--tw-text-opacity: 1;color:rgb(4 120 87 / var(--tw-text-opacity, 1))}.text-foreground{color:hsl(var(--foreground))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity, 1))}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-muted-foreground\/60{color:hsl(var(--muted-foreground) / .6)}.text-popover-foreground{color:hsl(var(--popover-foreground))}.text-primary{color:hsl(var(--primary))}.text-primary-foreground{color:hsl(var(--primary-foreground))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.text-secondary-foreground{color:hsl(var(--secondary-foreground))}.text-sidebar-foreground{color:hsl(var(--sidebar-foreground))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.underline-offset-4{text-underline-offset:4px}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-90{opacity:.9}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring-0{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-offset-background{--tw-ring-offset-color: hsl(var(--background))}.blur{--tw-blur: blur(8px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}@keyframes route-active-pulse{0%{opacity:.35}50%{opacity:1}to{opacity:.35}}.route-active{animation:route-active-pulse 1.4s ease-in-out infinite}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-thumb{background:hsl(var(--border));border-radius:4px}::-webkit-scrollbar-track{background:transparent}.file\:border-0::file-selector-button{border-width:0px}.file\:bg-transparent::file-selector-button{background-color:transparent}.file\:text-sm::file-selector-button{font-size:.875rem;line-height:1.25rem}.file\:font-medium::file-selector-button{font-weight:500}.placeholder\:text-muted-foreground::-moz-placeholder{color:hsl(var(--muted-foreground))}.placeholder\:text-muted-foreground::placeholder{color:hsl(var(--muted-foreground))}.last\:border-0:last-child{border-width:0px}.hover\:bg-accent:hover{background-color:hsl(var(--accent))}.hover\:bg-destructive\/90:hover{background-color:hsl(var(--destructive) / .9)}.hover\:bg-muted\/50:hover{background-color:hsl(var(--muted) / .5)}.hover\:bg-primary\/90:hover{background-color:hsl(var(--primary) / .9)}.hover\:bg-secondary\/80:hover{background-color:hsl(var(--secondary) / .8)}.hover\:text-accent-foreground:hover{color:hsl(var(--accent-foreground))}.hover\:text-foreground:hover{color:hsl(var(--foreground))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.hover\:opacity-95:hover{opacity:.95}.focus\:bg-accent:focus{background-color:hsl(var(--accent))}.focus\:text-accent-foreground:focus{color:hsl(var(--accent-foreground))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-ring:focus{--tw-ring-color: hsl(var(--ring))}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-1:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color: hsl(var(--ring))}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width: 2px}.focus-visible\:ring-offset-background:focus-visible{--tw-ring-offset-color: hsl(var(--background))}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.peer:disabled~.peer-disabled\:cursor-not-allowed{cursor:not-allowed}.peer:disabled~.peer-disabled\:opacity-70{opacity:.7}.data-\[disabled\]\:pointer-events-none[data-disabled]{pointer-events:none}.data-\[state\=checked\]\:translate-x-4[data-state=checked]{--tw-translate-x: 1rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[state\=unchecked\]\:translate-x-0[data-state=unchecked]{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[state\=active\]\:bg-background[data-state=active]{background-color:hsl(var(--background))}.data-\[state\=checked\]\:bg-primary[data-state=checked]{background-color:hsl(var(--primary))}.data-\[state\=selected\]\:bg-muted[data-state=selected]{background-color:hsl(var(--muted))}.data-\[state\=unchecked\]\:bg-input[data-state=unchecked]{background-color:hsl(var(--input))}.data-\[state\=active\]\:text-foreground[data-state=active]{color:hsl(var(--foreground))}.data-\[state\=checked\]\:text-primary-foreground[data-state=checked]{color:hsl(var(--primary-foreground))}.data-\[disabled\]\:opacity-50[data-disabled]{opacity:.5}.data-\[state\=active\]\:shadow[data-state=active]{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.dark\:border-amber-800:is(.dark *){--tw-border-opacity: 1;border-color:rgb(146 64 14 / var(--tw-border-opacity, 1))}.dark\:bg-amber-950\/20:is(.dark *){background-color:#451a0333}.dark\:bg-emerald-900\/40:is(.dark *){background-color:#064e3b66}.dark\:bg-green-900:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(20 83 45 / var(--tw-bg-opacity, 1))}.dark\:bg-red-900\/40:is(.dark *){background-color:#7f1d1d66}.dark\:text-amber-200:is(.dark *){--tw-text-opacity: 1;color:rgb(253 230 138 / var(--tw-text-opacity, 1))}.dark\:text-amber-300:is(.dark *){--tw-text-opacity: 1;color:rgb(252 211 77 / var(--tw-text-opacity, 1))}.dark\:text-emerald-300:is(.dark *){--tw-text-opacity: 1;color:rgb(110 231 183 / var(--tw-text-opacity, 1))}.dark\:text-green-200:is(.dark *){--tw-text-opacity: 1;color:rgb(187 247 208 / var(--tw-text-opacity, 1))}.dark\:text-red-300:is(.dark *){--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}@media(min-width:640px){.sm\:max-w-\[425px\]{max-width:425px}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:justify-end{justify-content:flex-end}.sm\:space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.sm\:rounded-lg{border-radius:var(--radius)}.sm\:text-left{text-align:left}}@media(min-width:768px){.md\:flex{display:flex}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:flex-col{flex-direction:column}}@media(min-width:1024px){.lg\:col-span-2{grid-column:span 2 / span 2}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}.\[\&_tr\:last-child\]\:border-0 tr:last-child{border-width:0px}.\[\&_tr\]\:border-b tr{border-bottom-width:1px}
|