@bitkyc08/opencodex 2.6.32 → 2.7.1-preview.20260710
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.ko.md +9 -5
- package/README.md +7 -4
- package/README.zh-CN.md +8 -4
- package/gui/dist/assets/index-BUAMcKFd.css +1 -0
- package/gui/dist/assets/index-KorpEKW8.js +34 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +62 -1
- package/src/adapters/cursor/cursor-errors.ts +28 -1
- package/src/adapters/cursor/discovery.ts +60 -10
- package/src/adapters/cursor/effort-map.ts +38 -7
- package/src/adapters/cursor/live-models.ts +3 -0
- package/src/adapters/cursor/live-transport.ts +136 -7
- package/src/adapters/cursor/protobuf-request.ts +24 -1
- package/src/adapters/cursor/request-builder.ts +6 -5
- package/src/adapters/cursor/transport-retry.ts +22 -3
- package/src/adapters/cursor.ts +2 -1
- package/src/adapters/openai-chat.ts +75 -26
- package/src/bridge.ts +42 -3
- package/src/cli/debug.ts +203 -0
- package/src/cli/doctor.ts +11 -0
- package/src/cli/help.ts +11 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/v2.ts +131 -0
- package/src/codex/auth-api.ts +7 -3
- package/src/codex/catalog.ts +334 -31
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/features.ts +178 -0
- package/src/codex/project-config-warnings.ts +388 -0
- package/src/codex/sync.ts +8 -0
- package/src/codex/warmup.ts +62 -7
- package/src/config.ts +7 -5
- package/src/lib/debug-log-buffer.ts +42 -0
- package/src/lib/debug-settings.ts +84 -0
- package/src/lib/debug.ts +18 -9
- package/src/lib/errors.ts +104 -1
- package/src/oauth/cursor.ts +35 -12
- package/src/oauth/store.ts +4 -3
- package/src/providers/derive.ts +8 -0
- package/src/providers/registry.ts +56 -21
- package/src/reasoning-effort.ts +37 -9
- package/src/responses/parser.ts +7 -2
- package/src/router.ts +5 -0
- package/src/server/adapter-resolve.ts +1 -1
- package/src/server/index.ts +27 -3
- package/src/server/management-api.ts +189 -7
- package/src/server/relay.ts +2 -2
- package/src/server/request-decompress.ts +8 -2
- package/src/server/request-log.ts +78 -0
- package/src/server/responses.ts +241 -9
- package/src/types.ts +34 -1
- package/src/usage/debug.ts +32 -5
- package/src/usage/summary.ts +6 -6
- package/src/vision/describe.ts +4 -0
- package/src/web-search/executor.ts +4 -0
- package/src/web-search/format-result.ts +11 -3
- package/src/web-search/index.ts +31 -2
- package/src/web-search/loop.ts +112 -61
- package/src/web-search/parse.ts +4 -1
- package/gui/dist/assets/index-ByGC8-Bm.css +0 -1
- package/gui/dist/assets/index-D_JZzI0r.js +0 -15
package/src/web-search/loop.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderConfig, Ocx
|
|
|
3
3
|
import { namespacedToolName } from "../types";
|
|
4
4
|
import { bridgeToResponsesSSE } from "../bridge";
|
|
5
5
|
import { runWebSearch, type SidecarOutcome, type SidecarOutcomeRecorder, type SidecarSettings } from "./executor";
|
|
6
|
-
import { cancelBodyOnAbort } from "../lib/abort";
|
|
6
|
+
import { cancelBodyOnAbort, signalWithTimeout } from "../lib/abort";
|
|
7
7
|
import { fetchWithResetRetry } from "../lib/upstream-retry";
|
|
8
8
|
import { formatWebSearchResults } from "./format-result";
|
|
9
9
|
import { WEB_SEARCH_TOOL_NAME } from "./synthetic-tool";
|
|
@@ -170,6 +170,13 @@ export interface WebSearchLoopDeps {
|
|
|
170
170
|
recordSidecarOutcome?: SidecarOutcomeRecorder;
|
|
171
171
|
/** Per-iteration deadline for routed model calls (mirrors the normal path's connectTimeoutMs). */
|
|
172
172
|
connectTimeoutMs?: number;
|
|
173
|
+
/**
|
|
174
|
+
* Effective bridge stall deadline for this turn (seconds). Computed by planWebSearch
|
|
175
|
+
* (webSearchStallTimeoutSec) to cover the loop's bounded silent units — a non-streaming model
|
|
176
|
+
* iteration (connectTimeoutMs) or one sidecar search (settings.timeoutMs) — so a legitimately
|
|
177
|
+
* slow-but-bounded unit never trips the bridge's 90s default upstream_stall_timeout.
|
|
178
|
+
*/
|
|
179
|
+
stallTimeoutSec?: number;
|
|
173
180
|
/**
|
|
174
181
|
* 429 key-failover hook: rotate the provider's active pool key and return a rebuilt adapter,
|
|
175
182
|
* or null when the pool is exhausted (same semantics as the normal routed path).
|
|
@@ -190,6 +197,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
190
197
|
if (!adapter.parseResponse) return jsonError(500, "web-search sidecar requires a non-streaming adapter");
|
|
191
198
|
|
|
192
199
|
const messages: OcxMessage[] = [...parsed.context.messages];
|
|
200
|
+
const loopT0 = Date.now();
|
|
193
201
|
const allTools = parsed.context.tools ?? [];
|
|
194
202
|
// For the forced-answer pass we drop the synthetic web_search tool so the model MUST answer from the
|
|
195
203
|
// results already in `messages` (can't search again) — this guarantees a non-empty final answer.
|
|
@@ -214,11 +222,15 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
214
222
|
// Hard iteration bound (termination safety net); forceAnswer normally ends the loop sooner.
|
|
215
223
|
const HARD_CAP = maxSearches + 2;
|
|
216
224
|
|
|
217
|
-
// Run one model iteration: build the request, fetch it, parse to adapter events.
|
|
218
|
-
// scanned split. Throws `LoopError` on a hard
|
|
219
|
-
// turn it into a non-200 jsonError
|
|
220
|
-
//
|
|
221
|
-
|
|
225
|
+
// Run one model iteration: build the request, fetch it, parse to adapter events. RETURNS the
|
|
226
|
+
// scanned split (generator return value, not a yield). Throws `LoopError` on a hard
|
|
227
|
+
// provider/parse failure so the EAGER first call can turn it into a non-200 jsonError
|
|
228
|
+
// (preserving the status contract), while later iterations — already inside the 200 SSE —
|
|
229
|
+
// surface it as an in-stream error event. A generator so the 429 key-failover loop can YIELD a
|
|
230
|
+
// heartbeat between bounded retry fetches: the iteration-wide AbortSignal.timeout bounds the
|
|
231
|
+
// plain-fetch path, but adapters with their own fetchResponse timeout handling could otherwise
|
|
232
|
+
// chain silent retries past the bridge stall deadline.
|
|
233
|
+
const runIterationEvents = async function* (forceAnswer: boolean): AsyncGenerator<AdapterEvent, { calls: WebSearchCall[]; passthrough: AdapterEvent[]; hasRealToolCall: boolean }> {
|
|
222
234
|
// On the forced-answer pass the synthetic web_search tool is gone, so the model MUST answer
|
|
223
235
|
// from the results already in `messages`. A weak model can still produce a thin answer that
|
|
224
236
|
// ignores what the search found, which reads to the user as "the search did nothing". Nudge it
|
|
@@ -233,61 +245,77 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
233
245
|
};
|
|
234
246
|
// Per-iteration deadline: routed calls elsewhere carry connectTimeoutMs; without it a hung
|
|
235
247
|
// upstream would stall the whole loop until the client gives up.
|
|
236
|
-
const
|
|
237
|
-
?
|
|
238
|
-
:
|
|
239
|
-
const
|
|
240
|
-
|
|
248
|
+
const iterationTimeout = deps.connectTimeoutMs
|
|
249
|
+
? signalWithTimeout(deps.connectTimeoutMs, signal)
|
|
250
|
+
: null;
|
|
251
|
+
const iterationSignal = iterationTimeout?.signal ?? signal;
|
|
252
|
+
try {
|
|
253
|
+
const fetchOnce = async (): Promise<Response> => {
|
|
254
|
+
const request = await adapter.buildRequest(iterParsed, { headers: selectedForwardHeaders });
|
|
255
|
+
try {
|
|
256
|
+
return adapter.fetchResponse
|
|
257
|
+
? await adapter.fetchResponse(request, { abortSignal: iterationSignal, ...(deps.connectTimeoutMs ? { timeoutMs: deps.connectTimeoutMs } : {}) })
|
|
258
|
+
: await fetchWithResetRetry(
|
|
259
|
+
() => fetch(request.url, {
|
|
260
|
+
method: request.method,
|
|
261
|
+
headers: request.headers,
|
|
262
|
+
body: request.body,
|
|
263
|
+
signal: iterationSignal,
|
|
264
|
+
}),
|
|
265
|
+
{ abortSignal: iterationSignal, label: "web-search-loop" },
|
|
266
|
+
);
|
|
267
|
+
} catch (e) {
|
|
268
|
+
if (!signal.aborted && iterationSignal.aborted) {
|
|
269
|
+
throw new LoopError(504, `Provider timeout after ${deps.connectTimeoutMs}ms during web-search`);
|
|
270
|
+
}
|
|
271
|
+
throw new LoopError(502, `Provider unreachable: ${e instanceof Error ? e.message : String(e)}`);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
let resp = await fetchOnce();
|
|
275
|
+
// 429 key-failover parity with the normal routed path: rotate pool keys until one responds
|
|
276
|
+
// or the pool is exhausted (deps.on429 returns null — cooldown map guarantees termination).
|
|
277
|
+
while (resp.status === 429 && deps.on429) {
|
|
278
|
+
const rotated = deps.on429(resp.headers.get("retry-after"));
|
|
279
|
+
if (!rotated?.parseResponse) break;
|
|
280
|
+
try { void resp.body?.cancel(); } catch { /* already consumed */ }
|
|
281
|
+
adapter = rotated;
|
|
282
|
+
// Stall-watchdog seam between bounded retry fetches (audit 011 B3).
|
|
283
|
+
yield { type: "heartbeat" };
|
|
284
|
+
resp = await fetchOnce();
|
|
285
|
+
}
|
|
286
|
+
if (!resp.ok) {
|
|
287
|
+
const t = await resp.text().catch(() => "");
|
|
288
|
+
throw new LoopError(resp.status, `Provider error ${resp.status}: ${t.slice(0, 400)}`);
|
|
289
|
+
}
|
|
290
|
+
// The fetch above carries `signal`; when the turn is superseded/cancelled, Bun aborts the
|
|
291
|
+
// response body stream. If parseResponse hasn't attached a reader yet, the body's pending read is
|
|
292
|
+
// orphaned off the awaited path and surfaces as `unhandledRejection: TypeError: null is not an
|
|
293
|
+
// object` (native-only stack). Proactively cancel the body on abort so WE settle it, and guard
|
|
294
|
+
// the drain so a mid-decode abort/stream error ends cleanly instead of throwing.
|
|
295
|
+
const detachBodyGuard = cancelBodyOnAbort(resp.body, signal);
|
|
296
|
+
let events: AdapterEvent[];
|
|
241
297
|
try {
|
|
242
|
-
|
|
243
|
-
? await adapter.fetchResponse(request, { abortSignal: iterationSignal, ...(deps.connectTimeoutMs ? { timeoutMs: deps.connectTimeoutMs } : {}) })
|
|
244
|
-
: await fetchWithResetRetry(
|
|
245
|
-
() => fetch(request.url, {
|
|
246
|
-
method: request.method,
|
|
247
|
-
headers: request.headers,
|
|
248
|
-
body: request.body,
|
|
249
|
-
signal: iterationSignal,
|
|
250
|
-
}),
|
|
251
|
-
{ abortSignal: iterationSignal, label: "web-search-loop" },
|
|
252
|
-
);
|
|
298
|
+
events = await adapter.parseResponse!(resp);
|
|
253
299
|
} catch (e) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
300
|
+
await resp.body?.cancel().catch(() => {});
|
|
301
|
+
if (signal.aborted) throw new LoopError(499, "client closed request during web-search");
|
|
302
|
+
throw new LoopError(502, `Provider stream error: ${e instanceof Error ? e.message : String(e)}`);
|
|
303
|
+
} finally {
|
|
304
|
+
detachBodyGuard();
|
|
258
305
|
}
|
|
259
|
-
|
|
260
|
-
let resp = await fetchOnce();
|
|
261
|
-
// 429 key-failover parity with the normal routed path: rotate pool keys until one responds
|
|
262
|
-
// or the pool is exhausted (deps.on429 returns null — cooldown map guarantees termination).
|
|
263
|
-
while (resp.status === 429 && deps.on429) {
|
|
264
|
-
const rotated = deps.on429(resp.headers.get("retry-after"));
|
|
265
|
-
if (!rotated?.parseResponse) break;
|
|
266
|
-
try { void resp.body?.cancel(); } catch { /* already consumed */ }
|
|
267
|
-
adapter = rotated;
|
|
268
|
-
resp = await fetchOnce();
|
|
269
|
-
}
|
|
270
|
-
if (!resp.ok) {
|
|
271
|
-
const t = await resp.text().catch(() => "");
|
|
272
|
-
throw new LoopError(resp.status, `Provider error ${resp.status}: ${t.slice(0, 400)}`);
|
|
273
|
-
}
|
|
274
|
-
// The fetch above carries `signal`; when the turn is superseded/cancelled, Bun aborts the
|
|
275
|
-
// response body stream. If parseResponse hasn't attached a reader yet, the body's pending read is
|
|
276
|
-
// orphaned off the awaited path and surfaces as `unhandledRejection: TypeError: null is not an
|
|
277
|
-
// object` (native-only stack). Proactively cancel the body on abort so WE settle it, and guard
|
|
278
|
-
// the drain so a mid-decode abort/stream error ends cleanly instead of throwing.
|
|
279
|
-
const detachBodyGuard = cancelBodyOnAbort(resp.body, signal);
|
|
280
|
-
let events: AdapterEvent[];
|
|
281
|
-
try {
|
|
282
|
-
events = await adapter.parseResponse!(resp);
|
|
283
|
-
} catch (e) {
|
|
284
|
-
await resp.body?.cancel().catch(() => {});
|
|
285
|
-
if (signal.aborted) throw new LoopError(499, "client closed request during web-search");
|
|
286
|
-
throw new LoopError(502, `Provider stream error: ${e instanceof Error ? e.message : String(e)}`);
|
|
306
|
+
return scanEventsForWebSearch(events);
|
|
287
307
|
} finally {
|
|
288
|
-
|
|
308
|
+
iterationTimeout?.cleanup();
|
|
289
309
|
}
|
|
290
|
-
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// Drain an iteration OUTSIDE the bridge (eager first call): the stall deadline is not armed
|
|
313
|
+
// before bridgeToResponsesSSE exists, so seam heartbeats have nowhere to go — discard them.
|
|
314
|
+
const runIterationDrained = async (forceAnswer: boolean): Promise<{ calls: WebSearchCall[]; passthrough: AdapterEvent[]; hasRealToolCall: boolean }> => {
|
|
315
|
+
const it = runIterationEvents(forceAnswer);
|
|
316
|
+
let r = await it.next();
|
|
317
|
+
while (!r.done) r = await it.next();
|
|
318
|
+
return r.value;
|
|
291
319
|
};
|
|
292
320
|
|
|
293
321
|
// Execute one model-requested web_search call. The call may batch several queries (native
|
|
@@ -306,6 +334,10 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
306
334
|
results.push({ query: "", outcome: { text: "", sources: [], error: "the model called web_search with an empty query" } });
|
|
307
335
|
}
|
|
308
336
|
for (const query of call.queries) {
|
|
337
|
+
// Stall-watchdog seam: batched queries run sequentially inside ONE begin/end cell, and
|
|
338
|
+
// placeholder outcomes (repeat/limit) emit no cell at all — without this, consecutive
|
|
339
|
+
// bounded units chain into one silent span past the stall deadline (audit 011 B1).
|
|
340
|
+
yield { type: "heartbeat" };
|
|
309
341
|
let outcome: SidecarOutcome;
|
|
310
342
|
if (failedQueries.has(normalizeQuery(query))) {
|
|
311
343
|
// Already failed this turn — don't spend another real search on it.
|
|
@@ -375,7 +407,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
375
407
|
// streaming starts (the status contract Codex relies on). Later iterations run live inside the SSE.
|
|
376
408
|
let first: { calls: WebSearchCall[]; passthrough: AdapterEvent[]; hasRealToolCall: boolean };
|
|
377
409
|
try {
|
|
378
|
-
first = await
|
|
410
|
+
first = await runIterationDrained(false);
|
|
379
411
|
} catch (e) {
|
|
380
412
|
if (abortSignal) abortSignal.removeEventListener("abort", linkAbort);
|
|
381
413
|
if (e instanceof LoopError) return jsonError(e.status, e.message);
|
|
@@ -401,7 +433,12 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
401
433
|
// First loop turn reuses the eager result; subsequent turns run a fresh iteration here.
|
|
402
434
|
if (i > 0) {
|
|
403
435
|
try {
|
|
404
|
-
|
|
436
|
+
// Stall-watchdog seam before each in-stream iteration (placeholder search turns emit no
|
|
437
|
+
// cell, so consecutive non-streaming iterations would otherwise be one silent span).
|
|
438
|
+
yield { type: "heartbeat" };
|
|
439
|
+
// Delegate so seam heartbeats reach the bridge, the scanned split returns here, and an
|
|
440
|
+
// outer consumer close propagates into the iteration generator's cleanup.
|
|
441
|
+
split = yield* runIterationEvents(forceAnswer);
|
|
405
442
|
} catch (e) {
|
|
406
443
|
yield { type: "error", message: e instanceof LoopError ? e.message : (e instanceof Error ? e.message : String(e)) };
|
|
407
444
|
return;
|
|
@@ -411,7 +448,15 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
411
448
|
// tool call (e.g. shell/apply_patch) means this turn is terminal for Codex — finalize so those
|
|
412
449
|
// calls reach Codex. forceAnswer also finalizes.
|
|
413
450
|
const shouldLoop = split.calls.length > 0 && !split.hasRealToolCall && !forceAnswer;
|
|
414
|
-
|
|
451
|
+
if (!shouldLoop) {
|
|
452
|
+
if (executedSearchCount > 0) {
|
|
453
|
+
const failedCount = failedQueries.size;
|
|
454
|
+
console.warn(
|
|
455
|
+
`[web-search-loop] done — ${executedSearchCount} search${executedSearchCount > 1 ? "es" : ""}`
|
|
456
|
+
+ (failedCount > 0 ? ` (${failedCount} failed)` : "")
|
|
457
|
+
+ `, ${i + 1} iteration${i > 0 ? "s" : ""}, ${Date.now() - loopT0}ms`,
|
|
458
|
+
);
|
|
459
|
+
}
|
|
415
460
|
yield* replay(split.passthrough);
|
|
416
461
|
return;
|
|
417
462
|
}
|
|
@@ -424,11 +469,17 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
424
469
|
}
|
|
425
470
|
|
|
426
471
|
const sse = bridgeToResponsesSSE(
|
|
427
|
-
produce(), parsed.modelId, toolNsMap, freeform, toolSearch,
|
|
428
|
-
|
|
472
|
+
produce(), parsed.modelId, toolNsMap, freeform, toolSearch, () => {
|
|
473
|
+
const elapsed = Date.now() - loopT0;
|
|
474
|
+
if (executedSearchCount > 0 || searchesExecuted > 0) {
|
|
475
|
+
console.warn(`[web-search-loop] cancelled — ${executedSearchCount} real searches, ${searchesExecuted - executedSearchCount} placeholders, ${elapsed}ms`);
|
|
476
|
+
}
|
|
477
|
+
internalAbort.abort("client closed responses stream");
|
|
478
|
+
}, undefined,
|
|
429
479
|
{
|
|
430
480
|
...(deps.forceEmptyResponseId ? { responseId: "" } : {}),
|
|
431
481
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
482
|
+
...(deps.stallTimeoutSec !== undefined ? { stallTimeoutSec: deps.stallTimeoutSec } : {}),
|
|
432
483
|
},
|
|
433
484
|
);
|
|
434
485
|
return new Response(sse, { headers: SSE_HEADERS });
|
package/src/web-search/parse.ts
CHANGED
|
@@ -154,7 +154,10 @@ export async function parseSidecarSSE(response: Response): Promise<WebSearchResu
|
|
|
154
154
|
const handle = (payload: string): void => {
|
|
155
155
|
if (!payload || payload === "[DONE]") return;
|
|
156
156
|
let data: Record<string, unknown>;
|
|
157
|
-
try { data = JSON.parse(payload) as Record<string, unknown>; } catch {
|
|
157
|
+
try { data = JSON.parse(payload) as Record<string, unknown>; } catch {
|
|
158
|
+
console.warn(`[web-search-parse] malformed SSE JSON (${payload.length} chars): ${payload.slice(0, 120)}`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
158
161
|
const type = data.type as string | undefined;
|
|
159
162
|
if (type === "response.output_text.delta" && typeof data.delta === "string") {
|
|
160
163
|
acc.deltaText += data.delta;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;--bg:var(--lightningcss-light,#f6f7f9)var(--lightningcss-dark,#0c0d11);--rail:var(--lightningcss-light,#fff)var(--lightningcss-dark,#101218);--surface:var(--lightningcss-light,#fff)var(--lightningcss-dark,#15171d);--raised:var(--lightningcss-light,#f1f2f5)var(--lightningcss-dark,#1c1f27);--raised-hover:var(--lightningcss-light,#e8eaee)var(--lightningcss-dark,#242833);--border:var(--lightningcss-light,#e2e4e9)var(--lightningcss-dark,#2a2e39);--border-soft:var(--lightningcss-light,#ededf1)var(--lightningcss-dark,#20242d);--hover:var(--lightningcss-light,#11131c09)var(--lightningcss-dark,#ffffff06);--text:var(--lightningcss-light,#16181d)var(--lightningcss-dark,#edeef2);--muted:var(--lightningcss-light,#5b6270)var(--lightningcss-dark,#a3a9b5);--faint:var(--lightningcss-light,#868d9b)var(--lightningcss-dark,#6b7280);--accent:var(--lightningcss-light,#4f46e5)var(--lightningcss-dark,#6366f1);--accent-hover:var(--lightningcss-light,#4338ca)var(--lightningcss-dark,#818cf8);--accent-ink:#fff;--accent-soft:var(--lightningcss-light,#4f46e51a)var(--lightningcss-dark,#6366f129);--accent-ring:var(--lightningcss-light,#4f46e566)var(--lightningcss-dark,#6366f180);--green:var(--lightningcss-light,#047857)var(--lightningcss-dark,#34d399);--green-soft:var(--lightningcss-light,#0596691a)var(--lightningcss-dark,#34d39921);--red:var(--lightningcss-light,#b91c1c)var(--lightningcss-dark,#f87171);--red-soft:var(--lightningcss-light,#b91c1c17)var(--lightningcss-dark,#f8717121);--amber:var(--lightningcss-light,#b45309)var(--lightningcss-dark,#fbbf24);--amber-soft:var(--lightningcss-light,#b453091a)var(--lightningcss-dark,#fbbf2421);--radius:8px;--radius-sm:5px;--radius-xs:4px;--font:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, "Helvetica Neue", sans-serif;--mono:ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo, Consolas, monospace;--shadow:0 1px 2px var(--lightningcss-light,#1018280f)var(--lightningcss-dark,#00000080), 0 10px 28px var(--lightningcss-light,#10182812)var(--lightningcss-dark,#0000004d);--shadow-sm:0 1px 2px var(--lightningcss-light,#1018280f)var(--lightningcss-dark,#0006)}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}:root[data-theme=light]{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}:root[data-theme=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}*{box-sizing:border-box}html,body,#root{height:100%}body{background:var(--bg);color:var(--text);font-family:var(--font);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;margin:0;font-size:14px;line-height:1.5}a{color:var(--accent-hover);text-decoration:none}a:hover{text-decoration:underline}code,.mono{font-family:var(--mono);font-size:.92em}h1,h2,h3,h4{letter-spacing:-.01em;margin:0;font-weight:650}::selection{background:var(--accent-soft)}input[type=checkbox],input[type=radio]{accent-color:var(--accent)}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-thumb{background:var(--border);border:2px solid var(--bg);border-radius:99px}::-webkit-scrollbar-thumb:hover{background:var(--faint)}:focus-visible{outline:2px solid var(--accent-ring);outline-offset:2px;border-radius:4px}.app{grid-template-columns:232px 1fr;min-height:100dvh;display:grid}.sidebar{border-right:1px solid var(--border);background:var(--rail);flex-direction:column;align-self:start;gap:4px;height:100dvh;padding:18px 14px;display:flex;position:sticky;top:0}.brand{align-items:center;gap:10px;padding:6px 8px 14px;display:flex}.brand-logo{background:var(--text);flex-shrink:0;width:26px;height:26px;-webkit-mask:url(/logo.png) 50%/contain no-repeat;mask:url(/logo.png) 50%/contain no-repeat}.brand .name{letter-spacing:-.02em;font-size:15px;font-weight:700;line-height:26px}.brand .ver{font-family:var(--mono);color:var(--muted);background:var(--raised);border:1px solid var(--border);border-radius:99px;align-self:center;padding:2px 6px;font-size:10px;line-height:1}.nav-item{border-radius:var(--radius-sm);text-align:left;cursor:pointer;width:100%;color:var(--muted);font:inherit;background:0 0;border:none;align-items:center;gap:10px;padding:8px 10px;font-size:13.5px;font-weight:500;transition:background .12s,color .12s;display:flex}.nav-item:hover{background:var(--raised);color:var(--text)}.nav-item.active{background:var(--accent-soft);color:var(--text)}.nav-item svg{width:17px;height:17px;color:var(--faint);flex-shrink:0}.nav-item.active svg{color:var(--accent)}.sidebar-foot{flex-direction:column;gap:2px;margin-top:auto;padding-top:12px;display:flex}.sidebar-link{color:var(--muted);border-radius:var(--radius-sm);align-items:center;gap:9px;padding:8px 10px;font-size:13px;display:flex}.sidebar-link:hover{background:var(--raised);color:var(--text);text-decoration:none}.sidebar-link svg{width:16px;height:16px}.theme-toggle{text-align:left;cursor:pointer;width:100%;color:var(--muted);font:inherit;border-radius:var(--radius-sm);background:0 0;border:none;align-items:center;gap:9px;padding:8px 10px;font-size:13px;transition:background .12s,color .12s;display:flex}.theme-toggle:hover{background:var(--raised);color:var(--text)}.theme-toggle svg{flex-shrink:0;width:16px;height:16px}.theme-toggle .mode{text-transform:capitalize}.stop-toggle{color:var(--red)}.stop-toggle:hover{background:var(--red-soft);color:var(--red)}.stop-toggle:disabled{opacity:.5;cursor:default}.main{min-width:0}.main-inner{max-width:980px;margin:0 auto;padding:32px 36px 64px}.page-head{justify-content:space-between;align-items:center;gap:16px;margin-bottom:6px;display:flex}.page-head h2{font-size:19px}.page-sub{color:var(--muted);max-width:70ch;margin:4px 0 22px;font-size:13.5px}.page-sub b{color:var(--text);font-weight:600}.api-page h2 svg{vertical-align:-.16em;width:1em;height:1em}.api-page .page-sub code{color:var(--text);background:var(--raised);border:1px solid var(--border-soft);border-radius:var(--radius-xs);padding:1px 4px;font-size:.92em}.btn{border-radius:var(--radius-sm);font:inherit;cursor:pointer;white-space:nowrap;border:1px solid #0000;justify-content:center;align-items:center;gap:7px;padding:7px 14px;font-size:13px;font-weight:550;transition:background .12s,border-color .12s,opacity .12s;display:inline-flex}.btn svg{width:15px;height:15px}.btn:disabled{opacity:.55;cursor:default}.btn-primary{background:var(--accent);color:var(--accent-ink)}.btn-primary:hover:not(:disabled){background:var(--accent-hover)}.btn-ghost{background:var(--raised);color:var(--text);border-color:var(--border)}.btn-ghost:hover:not(:disabled){background:var(--raised-hover)}.btn-danger{color:var(--red);background:0 0;border-color:#f871714d}.btn-danger:hover:not(:disabled){background:var(--red-soft)}.btn-sm{border-radius:var(--radius-xs);padding:4px 9px;font-size:12px}.btn-icon{color:var(--muted);border-radius:var(--radius-xs);padding:5px;transition:background .12s,color .12s}.btn-icon:hover{background:var(--raised-hover);color:var(--text)}.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);min-width:0}.panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:18px}.panel-accent{background:linear-gradient(180deg, var(--accent-soft), transparent 120%), var(--surface);border-color:#7c5cff47}.api-panel{flex-direction:column;gap:10px;display:flex;overflow:hidden}.api-panel .panel-title,.api-panel .muted{margin:0}.api-form-row{align-items:center;gap:8px;min-width:0;display:flex}.api-form-row .input{flex:1;min-width:0}.api-code{min-width:0;color:var(--text);background:var(--raised);border:1px solid var(--border-soft);border-radius:var(--radius-sm);font-family:var(--mono);white-space:pre;margin:0;padding:9px 11px;font-size:12.5px;line-height:1.55;display:block;overflow-x:auto}.api-code-inline{white-space:nowrap}.api-actions{justify-content:flex-end;align-items:center;gap:4px;display:flex}.maintenance-head{flex-wrap:wrap;align-items:flex-start}.maintenance-actions{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;display:flex}.maintenance-notice{align-items:flex-start;margin:14px 0 0}.update-row{justify-content:space-between;align-items:center;gap:12px;margin-bottom:14px;display:flex}.update-row .field-label{margin:0}.update-empty{margin-bottom:14px;padding:18px}.update-box{flex-direction:column;gap:12px;display:flex}.update-command{flex-wrap:wrap;align-items:center;gap:6px;font-size:12px;display:flex}.update-restart{border-top:1px solid var(--border-soft);padding-top:12px}.stat-row{grid-template-columns:repeat(5,1fr);gap:12px;margin-bottom:28px;display:grid}.stat{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px;transition:border-color .12s}.stat:hover{border-color:var(--accent-ring)}.stat .label{color:var(--muted);text-transform:uppercase;letter-spacing:.03em;align-items:center;gap:6px;margin-bottom:6px;font-size:12px;font-weight:600;display:flex}.stat .label svg{width:14px;height:14px}.stat .value{letter-spacing:-.02em;font-size:20px;font-weight:700;line-height:1.15}.stat .value.mono{font-family:var(--mono);font-size:17px}.model-group-head{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;align-items:baseline;gap:8px;margin:0 0 8px;font-size:12px;font-weight:600;display:flex}.model-group-head .count{font-family:var(--mono);text-transform:none;letter-spacing:0;color:var(--faint);font-weight:500}.model-grid{grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px;display:grid}.model-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px 12px;transition:border-color .12s,background .12s}.model-card:hover{border-color:var(--accent-ring);background:var(--hover)}.model-card .id{font-family:var(--mono);letter-spacing:-.01em;color:var(--text);font-size:13px;font-weight:600}.badge{font-size:11px;font-weight:600;font-family:var(--mono);letter-spacing:.01em;border:1px solid #0000;border-radius:99px;align-items:center;gap:5px;padding:2px 8px;display:inline-flex}.badge-accent{background:var(--accent-soft);color:var(--accent-hover)}.badge-green{background:var(--green-soft);color:var(--green)}.badge-amber{background:var(--amber-soft);color:var(--amber)}.badge-muted{background:var(--raised);color:var(--muted);border:1px solid var(--border)}.badge-clickable{cursor:pointer;appearance:none;transition:filter .12s}.badge-clickable:hover{filter:brightness(1.1)}.badge-disabled{opacity:.5;cursor:default}.card-badges{flex-wrap:wrap;align-items:center;gap:8px;min-width:0;display:inline-flex}.card-badges .badge{flex-shrink:0}.confirm-icon{background:var(--amber-soft);width:44px;height:44px;color:var(--amber);border-radius:50%;justify-content:center;align-items:center;margin:0 auto 12px;display:flex}.credit-list{flex-direction:column;gap:6px;display:flex}.credit-item{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--raised);padding:8px 10px;transition:border-color .12s}.credit-next{border-color:var(--amber);background:var(--amber-soft)}.credit-item-head{color:var(--text);align-items:center;gap:6px;margin-bottom:3px;font-size:12px;font-weight:600;display:flex}.credit-item-head svg{color:var(--amber);flex-shrink:0}.credit-item-dates{font-size:11px;font-family:var(--mono);color:var(--muted);justify-content:space-between;display:flex}.credit-urgent{color:var(--red);font-weight:600}.dot{border-radius:50%;flex-shrink:0;width:7px;height:7px}.dot-green{background:var(--green);box-shadow:0 0 0 3px var(--green-soft)}.dot-red{background:var(--red);box-shadow:0 0 0 3px var(--red-soft)}.tbl{border-collapse:collapse;width:100%;font-size:13px}.tbl thead th{text-align:left;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;border-bottom:1px solid var(--border);padding:9px 12px;font-size:11.5px;font-weight:600}.tbl tbody td{border-bottom:1px solid var(--border-soft);padding:10px 12px}.tbl tbody tr:last-child td{border-bottom:none}.tbl tbody tr:hover td{background:var(--hover)}.tbl .num{text-align:right;font-family:var(--mono)}.tbl-wrap{border:1px solid var(--border);border-radius:var(--radius);overflow-x:auto}.input,textarea.input{border-radius:var(--radius-sm);background:var(--raised);border:1px solid var(--border);width:100%;color:var(--text);font:inherit;padding:8px 11px;font-size:13px;transition:border-color .12s}.input::placeholder{color:var(--faint)}.input:focus{border-color:var(--accent);outline:none}textarea.input{resize:vertical;font-family:var(--mono);line-height:1.55}.field-label{color:var(--muted);margin-bottom:5px;font-size:12px;font-weight:500;display:block}select.input{appearance:none}.select-sm{border-radius:var(--radius-sm);background:var(--raised);border:1px solid var(--border);color:var(--text);font:inherit;cursor:pointer;padding:5px 8px;font-size:13px;transition:border-color .12s}.select-sm:focus{border-color:var(--accent);outline:none}.select-sm:disabled{opacity:.5;cursor:default}.switch{cursor:pointer;background:var(--lightningcss-light,#c5c9d2)var(--lightningcss-dark,#3a3f4b);border:none;border-radius:99px;flex-shrink:0;width:34px;height:19px;padding:0;transition:background .15s;position:relative}.switch.on{background:var(--accent)}.switch:disabled{opacity:.6;cursor:default}.switch .knob{background:#fff;border-radius:50%;width:15px;height:15px;transition:left .15s;position:absolute;top:2px;left:2px;box-shadow:0 1px 2px #1018284d}.switch.on .knob{left:17px}.muted{color:var(--muted)}.faint{color:var(--faint)}.row{align-items:center;gap:10px;display:flex}.spread{justify-content:space-between;align-items:center;gap:12px;display:flex}.setting-hint{margin-top:3px;font-size:13px}.stack{flex-direction:column;display:flex}.chip{font-family:var(--mono);background:var(--raised);border:1px solid var(--border);border-radius:var(--radius-xs);color:var(--text);padding:1px 7px;font-size:12px}.empty{text-align:center;border:1px dashed var(--border);border-radius:var(--radius);color:var(--muted);padding:56px 20px}.empty svg{width:30px;height:30px;color:var(--faint);margin-bottom:12px}.empty .title{color:var(--text);margin-bottom:6px;font-weight:600}.notice{border-radius:var(--radius-sm);align-items:center;gap:8px;margin-bottom:14px;padding:9px 12px;font-size:13px;display:flex}.notice svg{flex-shrink:0;width:15px;height:15px}.notice-ok{background:var(--green-soft);color:var(--green)}.notice-err{background:var(--red-soft);color:var(--red)}.h-section{color:var(--text);align-items:center;gap:8px;margin:30px 0 12px;font-size:13px;font-weight:600;display:flex}.h-section .count{color:var(--muted);font-weight:500;font-family:var(--mono);font-size:12px}.spin{border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;width:14px;height:14px;animation:.7s linear infinite spin;display:inline-block}@keyframes spin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}.modal-overlay{-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);z-index:50;background:var(--lightningcss-light,#11131c73)var(--lightningcss-dark,#0009);justify-content:center;align-items:flex-start;padding:8vh 16px;display:flex;position:fixed;inset:0}.modal-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);width:100%;max-width:520px;box-shadow:var(--shadow);max-height:84vh;padding:20px;overflow-y:auto}.modal-head{justify-content:space-between;align-items:center;margin-bottom:16px;display:flex}.modal-head h3{font-size:16px}.card-head{flex-wrap:wrap;align-items:center;gap:8px;min-width:0;padding:14px 16px 6px;display:flex}.card-head strong{overflow-wrap:anywhere;min-width:0}.card-sub{color:var(--muted);overflow-wrap:anywhere;min-width:0;padding:0 16px 10px;font-size:12px}.card-active{border-color:var(--accent-ring)}.card-right{color:var(--faint);align-items:center;gap:4px;margin-left:auto;font-size:11px;display:flex}.btn-icon-danger.card-right{appearance:none;color:var(--red);cursor:pointer;background:0 0;border:1px solid #0000;justify-content:center}.btn-icon-danger.card-right:hover{background:var(--red-soft);border-color:var(--red-soft);color:var(--red)}.card-row{justify-content:space-between;align-items:center;padding:14px 16px;display:flex}.badge-primary{background:var(--accent-soft);color:var(--accent-hover)}.dot-blue{background:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}.dot-muted{background:var(--muted)}.dot-amber{background:var(--amber);box-shadow:0 0 0 3px var(--amber-soft)}.section-sep{align-items:center;gap:10px;margin:20px 0 12px;display:flex}.section-label{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;white-space:nowrap;font-size:12px;font-weight:600}.sep-line{background:var(--border);flex:1;height:1px}.quota-compact{gap:4px;padding:2px 16px 12px;display:grid}.quota-row{grid-template-columns:minmax(34px,max-content) 34px minmax(34px,44px) minmax(38px,42px) minmax(58px,1fr) minmax(30px,max-content);align-items:center;gap:8px;min-width:0;display:grid}.quota-label{color:var(--muted);font-size:11px;font-weight:600}.quota-val{font-size:11px;font-family:var(--mono);color:var(--text);text-align:right}.quota-reset-label{text-overflow:ellipsis;white-space:nowrap;color:var(--faint);font-size:11px;overflow:hidden}.quota-reset-day,.quota-reset-time{color:var(--muted);white-space:nowrap;font-size:11px}.quota-reset-time{font-family:var(--mono)}.bar{background:var(--raised);border-radius:3px;min-width:0;height:5px;overflow:hidden}.bar-fill{border-radius:3px;height:100%;transition:width .3s}.bar-green{background:var(--green)}.bar-amber{background:linear-gradient(90deg, var(--green), var(--amber))}.toggle{background:var(--raised);border:1px solid var(--border);cursor:pointer;border-radius:10px;flex-shrink:0;width:36px;height:20px;transition:background .15s;position:relative}.toggle.on{background:var(--accent);border-color:var(--accent)}.toggle-knob{background:#fff;border-radius:50%;width:16px;height:16px;transition:left .15s;position:absolute;top:1px;left:1px}.toggle.on .toggle-knob{left:17px}.notice-warn{border-radius:var(--radius-sm);background:var(--amber-soft);color:var(--amber);align-items:center;gap:6px;margin-bottom:12px;padding:8px 10px;font-size:12px;display:flex}.notice-warn svg{flex-shrink:0;width:14px;height:14px}.modal-desc{color:var(--muted);margin-bottom:14px;font-size:13px}.modal-actions{gap:8px;margin-top:16px;display:flex}.modal-actions .btn{flex:1}.log-reqid{-webkit-line-clamp:2;word-break:break-all;-webkit-box-orient:vertical;max-width:14ch;display:-webkit-box;overflow:hidden}.log-col-model{overflow-wrap:break-word;word-break:normal;max-width:18ch}.log-col-tokens{min-width:10ch}.log-status-cell{flex-direction:column;align-items:flex-start;gap:2px;display:inline-flex}.log-detail-btn{cursor:pointer;color:var(--accent-hover);font:inherit;white-space:nowrap;background:0 0;border:none;padding:0;font-size:11px;text-decoration:underline}.log-detail-grid{grid-template-columns:max-content minmax(0,1fr);gap:6px 14px;font-size:13px;display:grid}.log-detail-break{word-break:break-all}.log-detail-json{background:var(--raised);border:1px solid var(--border-soft);border-radius:var(--radius-sm);font-family:var(--mono);white-space:pre-wrap;word-break:break-all;max-height:40vh;margin:0;padding:10px 12px;font-size:11.5px;line-height:1.5;overflow:auto}.setup-guide{border:1px solid var(--border);border-radius:var(--radius-sm);margin-bottom:4px;padding:8px 12px;font-size:13px}.setup-guide summary{cursor:pointer;color:var(--accent-hover);font-weight:500}.setup-guide summary:hover{text-decoration:underline}.setup-guide a{color:var(--accent-hover)}.list-row{text-align:left;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--raised);cursor:pointer;width:100%;color:var(--text);font:inherit;justify-content:space-between;align-items:center;gap:10px;padding:11px 13px;transition:background .12s,border-color .12s;display:flex}.list-row:hover{background:var(--raised-hover);border-color:var(--accent-ring)}.list-row .title{font-size:14px;font-weight:600}.list-row .sub{color:var(--muted);margin-top:2px;font-size:12px}.prov-card{overflow:hidden}.prov-card-main{flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:12px;padding:15px 16px;display:flex}.prov-card-disabled{opacity:.62}.prov-card-info{flex:360px;align-items:flex-start;gap:11px;min-width:0;display:flex}.prov-card-copy{flex:1;min-width:0}.prov-title{flex-wrap:wrap;align-items:center;gap:8px;min-width:0;margin-bottom:5px;display:flex}.provider-icon{background:var(--raised);border:1px solid var(--border-soft);width:31px;height:31px;color:var(--text);border-radius:7px;flex:none;justify-content:center;align-items:center;display:inline-flex}.provider-icon img{object-fit:contain;width:19px;height:19px;display:block}.prov-meta{flex-wrap:wrap;align-items:center;gap:5px;min-width:0;display:flex}.prov-meta>span{text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:100%;overflow:hidden}.provider-quota{padding-left:58px}.provider-actions{flex-shrink:0;align-items:center;gap:8px;margin-left:auto;display:flex}.link-btn{color:var(--accent-hover);font:inherit;cursor:pointer;background:0 0;border:none;padding:6px 2px;font-size:13px;text-decoration:underline}.prov-accounts-toggle{border:none;border-top:1px solid var(--border-soft);width:100%;color:var(--muted);font:inherit;cursor:pointer;background:0 0;justify-content:center;align-items:center;gap:6px;min-height:24px;padding:4px 0;font-size:12px;display:flex}.prov-accounts-toggle:hover{color:var(--text);background:var(--raised)}.prov-accounts-toggle .chev{transition:transform .15s;display:inline-flex}.prov-accounts-toggle .chev svg{width:12px;height:12px;transform:rotate(90deg)}.prov-accounts-toggle.open .chev svg{transform:rotate(-90deg)}.prov-accounts-list{border-top:1px solid var(--border-soft);flex-direction:column;gap:2px;padding:6px 16px 10px;display:flex}.prov-account-row{text-align:left;border-radius:var(--radius-xs);width:100%;color:var(--text);font:inherit;cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;min-height:32px;padding:6px 8px;font-size:13px;display:flex}.prov-account-row:hover{background:var(--raised)}.prov-account-row.active{cursor:default}.prov-account-row .prov-account-email{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}.prov-account-row .badge{flex:none}.prov-account-remove{color:var(--muted);cursor:pointer;border-radius:var(--radius-xs);background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:4px;display:inline-flex}.prov-account-remove:hover{color:var(--red);background:var(--red-soft)}.prov-account-add{color:var(--muted);font-size:12.5px}.prov-account-add:hover{color:var(--accent-hover)}.prov-account-keyform{cursor:default;gap:6px}.prov-account-keyform:hover{background:0 0}.prov-account-keyform .input-sm{flex:auto;min-width:0;height:28px;padding:4px 8px;font-size:12.5px}.oauth-grid{grid-template-columns:minmax(140px,max-content) minmax(0,1fr) max-content;align-items:center;gap:10px 16px;display:grid}.oauth-row{grid-column:1/-1;grid-template-columns:subgrid;align-items:center;min-height:30px;display:grid}.oauth-name{align-items:center;gap:8px;min-width:0;font-size:13px;font-weight:600;display:inline-flex}.oauth-name-text{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.provider-icon-sm{border-radius:6px;width:24px;height:24px}.provider-icon-sm img{width:15px;height:15px}.oauth-status{align-items:center;gap:7px;min-width:0;font-size:13px;display:inline-flex}.oauth-email{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.oauth-actions{justify-content:flex-end;align-items:center;gap:8px;min-width:0;display:inline-flex}.oauth-login-hint{flex-wrap:wrap;grid-column:1/-1;align-items:center;gap:8px;font-size:12px;display:inline-flex}@media (width<=760px){.app{grid-template-columns:1fr}.sidebar{z-index:20;border-right:none;border-bottom:1px solid var(--border);background:var(--rail);flex-flow:wrap;align-items:center;gap:0;min-width:0;height:auto;padding:0 10px;position:sticky;top:0}.brand{flex:auto;order:1;width:auto;padding:10px 4px}.sidebar-foot{flex-direction:row;flex:none;order:2;gap:4px;margin:0;padding:0}.sidebar-foot .sidebar-link{display:none}.theme-toggle{justify-content:center;min-width:44px;min-height:44px;padding:8px}.theme-toggle .mode{display:none}.sidebar nav{border-top:1px solid var(--border-soft);flex:100%;order:3;grid-template-columns:repeat(4,minmax(0,1fr));gap:2px;min-width:0;margin:0;padding:4px 0 8px;display:grid}.nav-item{text-align:center;text-overflow:ellipsis;white-space:nowrap;flex-direction:column;justify-content:center;gap:3px;width:auto;min-height:44px;padding:6px 4px;font-size:12px;overflow:hidden}.nav-item svg{width:16px;height:16px}.main-inner{padding:22px 18px 48px}.setting-row{flex-wrap:wrap}.setting-row .setting-copy{flex:100%!important}.setting-row .setting-controls{width:100%}.setting-row .setting-controls .select-sm{flex:1 1 0;min-width:0}.setting-row>.select-sm{width:100%}.api-form-row{flex-direction:column;align-items:stretch}.api-form-row .btn{width:100%;min-height:40px}.stat-row{grid-template-columns:repeat(2,minmax(0,1fr))}.tbl{min-width:460px}.usage-cards{grid-template-columns:repeat(2,minmax(0,1fr))!important}.provider-quota{padding-left:16px}.prov-meta{grid-template-columns:max-content minmax(0,1fr);width:100%;display:grid}.prov-meta .chip~span:not(:last-child){display:none}.oauth-grid{grid-template-columns:minmax(0,auto) minmax(0,1fr) max-content;column-gap:8px}}.usage-cards{grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-top:8px;display:grid}.usage-cards .stat-value{margin-top:4px;font-size:20px;font-weight:600}.usage-range{border:1px solid var(--border);background:var(--surface);border-radius:999px;gap:2px;padding:2px;display:inline-flex}.usage-range-btn{color:var(--muted);cursor:pointer;font:inherit;background:0 0;border:none;border-radius:999px;padding:4px 12px}.usage-range-btn.active{background:var(--raised);color:var(--text);font-weight:600}.panel-title{color:var(--text);margin:0 0 12px;font-size:14px;font-weight:600}.panel-head{justify-content:space-between;align-items:center;gap:12px;margin-bottom:12px;display:flex}.panel-head .panel-title{margin:0}.panel-head .input{max-width:220px}.heatmap{--hm-cell:11px;--hm-gap:3px;flex-direction:column;gap:6px;padding-bottom:4px;display:flex;overflow-x:auto}.heatmap-months{color:var(--muted);width:max-content;margin-bottom:-2px;font-size:11px;display:grid}.heatmap-day-spacer{grid-column:1}.heatmap-month{white-space:nowrap}.heatmap-body{gap:var(--hm-gap);width:max-content;display:flex}.heatmap-days{grid-template-rows:repeat(7, var(--hm-cell));row-gap:var(--hm-gap);color:var(--muted);background:var(--surface);z-index:1;flex-shrink:0;align-items:center;width:25px;font-size:10px;display:grid;position:sticky;left:0}.heatmap-grid{gap:var(--hm-gap);display:grid}.heatmap-week{grid-template-rows:repeat(7, var(--hm-cell));gap:var(--hm-gap);display:grid}.heatmap-cell{width:var(--hm-cell);height:var(--hm-cell);background:var(--border);border-radius:2px}.heatmap-cell-0{background:var(--border)}.heatmap-cell-1{background:color-mix(in oklch, var(--green) 25%, var(--surface))}.heatmap-cell-2{background:color-mix(in oklch, var(--green) 50%, var(--surface))}.heatmap-cell-3{background:color-mix(in oklch, var(--green) 75%, var(--surface))}.heatmap-cell-4{background:var(--green)}.heatmap-legend{align-self:flex-end;align-items:center;gap:4px;font-size:12px;display:inline-flex;position:sticky;right:0}.heatmap-legend .heatmap-cell{width:10px;height:10px}.usage-bar{background:var(--border);border-radius:999px;width:100%;min-width:60px;height:6px;overflow:hidden}.usage-bar-fill{background:var(--green);border-radius:999px;height:100%}.daybars{grid-template-columns:repeat(7,1fr);align-items:end;gap:10px;height:180px;padding-top:8px;display:grid}.daybar{flex-direction:column;justify-content:flex-end;align-items:center;gap:6px;height:100%;display:flex;position:relative}.daybar-track{background:var(--border);border-radius:6px;flex:1;align-items:flex-end;width:100%;max-width:48px;display:flex;overflow:hidden}.daybar-stack{border-radius:6px 6px 0 0;flex-direction:column-reverse;width:100%;min-height:2px;transition:height .2s;display:flex;overflow:hidden}.daybar-seg{width:100%;min-height:1px}.daybar-count{color:var(--text);font-size:12px;font-weight:600}.daybar-label{white-space:nowrap;font-size:11px}.daybar:hover .daybar-track{outline:1px solid var(--border)}.daybar-tip{z-index:5;background:var(--surface);border:1px solid var(--border);pointer-events:none;border-radius:8px;min-width:160px;padding:8px 10px;position:absolute;bottom:calc(100% + 6px);left:50%;transform:translate(-50%);box-shadow:0 6px 20px #00000059}.daybar-tip-date{color:var(--text);white-space:nowrap;margin-bottom:6px;font-size:12px;font-weight:600}.daybar-tip-row{align-items:center;gap:8px;font-size:12px;line-height:1.7;display:flex}.daybar-tip-swatch{border-radius:2px;flex-shrink:0;width:10px;height:10px}.daybar-tip-name{color:var(--text);white-space:nowrap;text-overflow:ellipsis;flex:1;max-width:160px;overflow:hidden}.daybar-tip-val{color:var(--muted);font-variant-numeric:tabular-nums}.usage-scroll{max-height:360px;overflow-y:auto}.usage-scroll thead th{z-index:1;background:var(--surface);position:sticky;top:0}
|