@bitkyc08/opencodex 2.7.23 → 2.7.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.ko.md +37 -7
  2. package/README.md +52 -11
  3. package/README.zh-CN.md +36 -7
  4. package/bin/ocx.mjs +5 -3
  5. package/gui/dist/assets/index-BzhyTAco.js +40 -0
  6. package/gui/dist/assets/index-Dq3eZ1cU.css +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/opencode.svg +1 -1
  9. package/package.json +5 -2
  10. package/src/adapters/anthropic-image-normalize.ts +70 -29
  11. package/src/adapters/cursor/transport-retry.ts +20 -1
  12. package/src/adapters/run-turn-queue.ts +40 -0
  13. package/src/codex/auth-api.ts +10 -1
  14. package/src/codex/auth-context.ts +33 -7
  15. package/src/codex/catalog.ts +357 -23
  16. package/src/codex/routing.ts +10 -4
  17. package/src/combos/failover.ts +102 -0
  18. package/src/combos/index.ts +37 -0
  19. package/src/combos/request.ts +31 -0
  20. package/src/combos/resolve.ts +171 -0
  21. package/src/combos/types.ts +203 -0
  22. package/src/config.ts +280 -11
  23. package/src/lib/errors.ts +86 -24
  24. package/src/lib/upstream-retry.ts +8 -4
  25. package/src/oauth/index.ts +7 -1
  26. package/src/oauth/key-providers.ts +2 -32
  27. package/src/oauth/login-cli.ts +4 -3
  28. package/src/oauth/token-guardian.ts +38 -3
  29. package/src/providers/derive.ts +27 -2
  30. package/src/providers/kiro-models.ts +8 -3
  31. package/src/providers/label.ts +3 -1
  32. package/src/providers/openai-sidecar.ts +94 -0
  33. package/src/providers/openai-tier-startup.ts +27 -0
  34. package/src/providers/openai-tiers.ts +283 -0
  35. package/src/providers/openai-virtual-models.ts +82 -0
  36. package/src/providers/quota.ts +344 -24
  37. package/src/providers/registry.ts +112 -20
  38. package/src/reasoning-effort.ts +12 -11
  39. package/src/router.ts +80 -36
  40. package/src/server/auth-cors.ts +85 -9
  41. package/src/server/images.ts +31 -75
  42. package/src/server/index.ts +45 -86
  43. package/src/server/management-api.ts +273 -21
  44. package/src/server/request-log.ts +221 -20
  45. package/src/server/responses.ts +594 -75
  46. package/src/server/search.ts +22 -37
  47. package/src/types.ts +49 -1
  48. package/src/update/index.ts +50 -6
  49. package/src/update/job.ts +21 -4
  50. package/src/usage/log.ts +124 -1
  51. package/src/usage/summary.ts +147 -56
  52. package/src/vision/index.ts +20 -19
  53. package/src/web-search/index.ts +15 -17
  54. package/gui/dist/assets/index-Bk_GgFrh.css +0 -1
  55. package/gui/dist/assets/index-DQjt6Hly.js +0 -40
@@ -7,6 +7,7 @@ export type UsageSurface = "all" | "codex" | "claude";
7
7
 
8
8
  export interface UsageSummaryTotals {
9
9
  requests: number;
10
+ attemptCount: number;
10
11
  measuredRequests: number;
11
12
  reportedRequests: number;
12
13
  unreportedRequests: number;
@@ -35,6 +36,7 @@ export interface UsageDayModel {
35
36
  model: string;
36
37
  provider: string;
37
38
  requests: number;
39
+ attemptCount: number;
38
40
  totalTokens: number;
39
41
  }
40
42
 
@@ -43,6 +45,7 @@ export interface UsageModel {
43
45
  model: string;
44
46
  resolvedModel?: string;
45
47
  requests: number;
48
+ attemptCount: number;
46
49
  measuredRequests: number;
47
50
  reportedRequests: number;
48
51
  estimatedRequests: number;
@@ -55,6 +58,7 @@ export interface UsageModel {
55
58
  export interface UsageProvider {
56
59
  provider: string;
57
60
  requests: number;
61
+ attemptCount: number;
58
62
  measuredRequests: number;
59
63
  reportedRequests: number;
60
64
  estimatedRequests: number;
@@ -109,6 +113,7 @@ function dayCountForAllRange(entries: PersistedUsageEntry[], now: number): numbe
109
113
  function blankTotals(): UsageSummaryTotals {
110
114
  return {
111
115
  requests: 0,
116
+ attemptCount: 0,
112
117
  measuredRequests: 0,
113
118
  reportedRequests: 0,
114
119
  unreportedRequests: 0,
@@ -129,6 +134,49 @@ function isMeasuredStatus(status: UsageStatus): boolean {
129
134
  return status === "reported" || status === "estimated";
130
135
  }
131
136
 
137
+ interface UsageAttribution {
138
+ requestId: string;
139
+ provider: string;
140
+ model: string;
141
+ resolvedModel?: string;
142
+ usageStatus: UsageStatus;
143
+ usage?: PersistedUsageEntry["usage"];
144
+ totalTokens?: number;
145
+ }
146
+
147
+ function usageAttributions(entry: PersistedUsageEntry): UsageAttribution[] {
148
+ if (!entry.attempts?.length) {
149
+ return [{
150
+ requestId: entry.requestId,
151
+ provider: entry.provider,
152
+ model: entry.model,
153
+ ...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
154
+ usageStatus: entry.usageStatus,
155
+ ...(entry.usage ? { usage: entry.usage } : {}),
156
+ ...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
157
+ }];
158
+ }
159
+ return entry.attempts.map(attempt => ({
160
+ requestId: entry.requestId,
161
+ provider: attempt.provider,
162
+ model: attempt.model,
163
+ usageStatus: attempt.usageStatus,
164
+ ...(attempt.usage ? { usage: attempt.usage } : {}),
165
+ ...(attempt.totalTokens !== undefined ? { totalTokens: attempt.totalTokens } : {}),
166
+ }));
167
+ }
168
+
169
+ function foldAttributionStatuses(statuses: readonly UsageStatus[]): UsageStatus {
170
+ if (statuses.length > 0 && statuses.every(status => status === "unsupported")) {
171
+ return "unsupported";
172
+ }
173
+ if (statuses.some(status => status === "unreported" || status === "unsupported")) {
174
+ return "unreported";
175
+ }
176
+ if (statuses.some(status => status === "estimated")) return "estimated";
177
+ return statuses.length > 0 ? "reported" : "unreported";
178
+ }
179
+
132
180
  function bumpStatus(totals: UsageSummaryTotals, status: UsageStatus): void {
133
181
  totals.requests += 1;
134
182
  if (isMeasuredStatus(status)) totals.measuredRequests += 1;
@@ -138,7 +186,10 @@ function bumpStatus(totals: UsageSummaryTotals, status: UsageStatus): void {
138
186
  else if (status === "estimated") totals.estimatedRequests += 1;
139
187
  }
140
188
 
141
- function addTokens(totals: UsageSummaryTotals, entry: PersistedUsageEntry): void {
189
+ function addTokens(
190
+ totals: UsageSummaryTotals,
191
+ entry: Pick<PersistedUsageEntry, "usage" | "totalTokens">,
192
+ ): void {
142
193
  if (!entry.usage) return;
143
194
  totals.inputTokens += entry.usage.inputTokens;
144
195
  totals.outputTokens += entry.usage.outputTokens;
@@ -171,15 +222,24 @@ function buildDayGrid(range: UsageRange, since: number | null, now: number, entr
171
222
  // Per-day model breakdown accumulator, keyed by day then provider/model, so the 7d bar chart can
172
223
  // render a per-model stacked bar with a hover tooltip without a second pass over the entries.
173
224
  const dayModels = new Map<string, Map<string, UsageDayModel>>();
174
- const bumpDayModel = (dayKey: string, entry: PersistedUsageEntry): void => {
225
+ const dayModelRequests = new Map<string, Set<string>>();
226
+ const bumpDayModel = (dayKey: string, attribution: UsageAttribution): void => {
175
227
  let models = dayModels.get(dayKey);
176
228
  if (!models) { models = new Map(); dayModels.set(dayKey, models); }
177
- const providerKey = baseProviderLabel(entry.provider);
178
- const mKey = `${providerKey}/${entry.model}`;
229
+ const providerKey = baseProviderLabel(attribution.provider);
230
+ const mKey = `${providerKey}/${attribution.model}`;
179
231
  let m = models.get(mKey);
180
- if (!m) { m = { model: entry.model, provider: providerKey, requests: 0, totalTokens: 0 }; models.set(mKey, m); }
181
- m.requests += 1;
182
- m.totalTokens += usageDisplayTotalTokens(entry.usage, entry.totalTokens) ?? 0;
232
+ if (!m) {
233
+ m = { model: attribution.model, provider: providerKey, requests: 0, attemptCount: 0, totalTokens: 0 };
234
+ models.set(mKey, m);
235
+ }
236
+ const requestKey = `${dayKey}\0${mKey}`;
237
+ let requests = dayModelRequests.get(requestKey);
238
+ if (!requests) { requests = new Set(); dayModelRequests.set(requestKey, requests); }
239
+ requests.add(attribution.requestId);
240
+ m.requests = requests.size;
241
+ m.attemptCount += 1;
242
+ m.totalTokens += usageDisplayTotalTokens(attribution.usage, attribution.totalTokens) ?? 0;
183
243
  };
184
244
  for (let i = days - 1; i >= 0; i--) {
185
245
  const key = localDateKey(now - i * DAY_MS);
@@ -196,7 +256,7 @@ function buildDayGrid(range: UsageRange, since: number | null, now: number, entr
196
256
  if (isMeasuredStatus(entry.usageStatus)) day.measuredRequests += 1;
197
257
  if (entry.usageStatus === "reported") day.reportedRequests += 1;
198
258
  day.totalTokens += usageDisplayTotalTokens(entry.usage, entry.totalTokens) ?? 0;
199
- bumpDayModel(key, entry);
259
+ for (const attribution of usageAttributions(entry)) bumpDayModel(key, attribution);
200
260
  }
201
261
  void since;
202
262
  const out = [...grid.values()].sort((a, b) => a.date.localeCompare(b.date));
@@ -209,37 +269,51 @@ function buildDayGrid(range: UsageRange, since: number | null, now: number, entr
209
269
 
210
270
  function buildModels(entries: PersistedUsageEntry[], totalTokens: number): UsageModel[] {
211
271
  const byKey = new Map<string, UsageModel>();
272
+ const statusesByKey = new Map<string, Map<string, UsageStatus[]>>();
212
273
  for (const entry of entries) {
213
- const providerKey = baseProviderLabel(entry.provider);
214
- // resolvedModel is a routing detail (passthrough vs alias), not a row identity. Entries with
215
- // the same provider+model but differing resolvedModel (common when unreported rows carry no
216
- // resolvedModel) must collapse into one row.
217
- const key = `${providerKey}${entry.model}`;
218
- let model = byKey.get(key);
219
- if (!model) {
220
- model = {
221
- provider: providerKey,
222
- model: entry.model,
223
- ...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
224
- requests: 0,
225
- measuredRequests: 0,
226
- reportedRequests: 0,
227
- estimatedRequests: 0,
228
- totalTokens: 0,
229
- inputTokens: 0,
230
- outputTokens: 0,
231
- shareRatio: 0,
232
- };
233
- byKey.set(key, model);
274
+ for (const attribution of usageAttributions(entry)) {
275
+ const providerKey = baseProviderLabel(attribution.provider);
276
+ // resolvedModel is a routing detail, not a row identity.
277
+ const key = `${providerKey}${attribution.model}`;
278
+ let model = byKey.get(key);
279
+ if (!model) {
280
+ model = {
281
+ provider: providerKey,
282
+ model: attribution.model,
283
+ ...(attribution.resolvedModel ? { resolvedModel: attribution.resolvedModel } : {}),
284
+ requests: 0,
285
+ attemptCount: 0,
286
+ measuredRequests: 0,
287
+ reportedRequests: 0,
288
+ estimatedRequests: 0,
289
+ totalTokens: 0,
290
+ inputTokens: 0,
291
+ outputTokens: 0,
292
+ shareRatio: 0,
293
+ };
294
+ byKey.set(key, model);
295
+ }
296
+ model.attemptCount += 1;
297
+ let requests = statusesByKey.get(key);
298
+ if (!requests) { requests = new Map(); statusesByKey.set(key, requests); }
299
+ const statuses = requests.get(attribution.requestId) ?? [];
300
+ statuses.push(attribution.usageStatus);
301
+ requests.set(attribution.requestId, statuses);
302
+ if (attribution.usage) {
303
+ model.inputTokens += attribution.usage.inputTokens;
304
+ model.outputTokens += attribution.usage.outputTokens;
305
+ model.totalTokens += usageDisplayTotalTokens(attribution.usage, attribution.totalTokens) ?? 0;
306
+ }
234
307
  }
235
- model.requests += 1;
236
- if (isMeasuredStatus(entry.usageStatus)) model.measuredRequests += 1;
237
- if (entry.usageStatus === "reported") model.reportedRequests += 1;
238
- else if (entry.usageStatus === "estimated") model.estimatedRequests += 1;
239
- if (entry.usage) {
240
- model.inputTokens += entry.usage.inputTokens;
241
- model.outputTokens += entry.usage.outputTokens;
242
- model.totalTokens += usageDisplayTotalTokens(entry.usage, entry.totalTokens) ?? 0;
308
+ }
309
+ for (const [key, model] of byKey) {
310
+ const groups = statusesByKey.get(key) ?? new Map();
311
+ model.requests = groups.size;
312
+ for (const statuses of groups.values()) {
313
+ const status = foldAttributionStatuses(statuses);
314
+ if (isMeasuredStatus(status)) model.measuredRequests += 1;
315
+ if (status === "reported") model.reportedRequests += 1;
316
+ else if (status === "estimated") model.estimatedRequests += 1;
243
317
  }
244
318
  }
245
319
  const models = [...byKey.values()];
@@ -249,27 +323,43 @@ function buildModels(entries: PersistedUsageEntry[], totalTokens: number): Usage
249
323
 
250
324
  function buildProviders(entries: PersistedUsageEntry[], totalTokens: number): UsageProvider[] {
251
325
  const byKey = new Map<string, UsageProvider>();
326
+ const statusesByKey = new Map<string, Map<string, UsageStatus[]>>();
252
327
  for (const entry of entries) {
253
- const providerKey = baseProviderLabel(entry.provider);
254
- let provider = byKey.get(providerKey);
255
- if (!provider) {
256
- provider = {
257
- provider: providerKey,
258
- requests: 0,
259
- measuredRequests: 0,
260
- reportedRequests: 0,
261
- estimatedRequests: 0,
262
- totalTokens: 0,
263
- shareRatio: 0,
264
- };
265
- byKey.set(providerKey, provider);
328
+ for (const attribution of usageAttributions(entry)) {
329
+ const providerKey = baseProviderLabel(attribution.provider);
330
+ let provider = byKey.get(providerKey);
331
+ if (!provider) {
332
+ provider = {
333
+ provider: providerKey,
334
+ requests: 0,
335
+ attemptCount: 0,
336
+ measuredRequests: 0,
337
+ reportedRequests: 0,
338
+ estimatedRequests: 0,
339
+ totalTokens: 0,
340
+ shareRatio: 0,
341
+ };
342
+ byKey.set(providerKey, provider);
343
+ }
344
+ provider.attemptCount += 1;
345
+ let requests = statusesByKey.get(providerKey);
346
+ if (!requests) { requests = new Map(); statusesByKey.set(providerKey, requests); }
347
+ const statuses = requests.get(attribution.requestId) ?? [];
348
+ statuses.push(attribution.usageStatus);
349
+ requests.set(attribution.requestId, statuses);
350
+ if (attribution.usage) {
351
+ provider.totalTokens += usageDisplayTotalTokens(attribution.usage, attribution.totalTokens) ?? 0;
352
+ }
266
353
  }
267
- provider.requests += 1;
268
- if (isMeasuredStatus(entry.usageStatus)) provider.measuredRequests += 1;
269
- if (entry.usageStatus === "reported") provider.reportedRequests += 1;
270
- else if (entry.usageStatus === "estimated") provider.estimatedRequests += 1;
271
- if (entry.usage) {
272
- provider.totalTokens += usageDisplayTotalTokens(entry.usage, entry.totalTokens) ?? 0;
354
+ }
355
+ for (const [key, provider] of byKey) {
356
+ const groups = statusesByKey.get(key) ?? new Map();
357
+ provider.requests = groups.size;
358
+ for (const statuses of groups.values()) {
359
+ const status = foldAttributionStatuses(statuses);
360
+ if (isMeasuredStatus(status)) provider.measuredRequests += 1;
361
+ if (status === "reported") provider.reportedRequests += 1;
362
+ else if (status === "estimated") provider.estimatedRequests += 1;
273
363
  }
274
364
  }
275
365
  const providers = [...byKey.values()];
@@ -293,6 +383,7 @@ export function summarizeUsage(
293
383
  const totals = blankTotals();
294
384
  for (const entry of filteredEntries) {
295
385
  bumpStatus(totals, entry.usageStatus);
386
+ totals.attemptCount += entry.attempts?.length ?? 1;
296
387
  addTokens(totals, entry);
297
388
  }
298
389
  finalizeCoverage(totals);
@@ -5,6 +5,7 @@ import { describeImage, type DescribeOutcome, type VisionSettings } from "./desc
5
5
  import { describeImageAnthropic } from "./anthropic-describe";
6
6
  import type { CodexAuthContext } from "../codex/auth-context";
7
7
  import { getAccountSet } from "../oauth/store";
8
+ import type { ResolvedOpenAiForwardSidecar } from "../providers/openai-sidecar";
8
9
  import type { SidecarOutcomeRecorder } from "../web-search/executor";
9
10
 
10
11
  export { describeImage } from "./describe";
@@ -93,15 +94,6 @@ function clamp(s: string, max: number): string {
93
94
  return s.length <= max ? s : `${s.slice(0, max)}\n…[description truncated]`;
94
95
  }
95
96
 
96
- /** First configured forward (ChatGPT passthrough) provider — the OpenAI path with native image input. */
97
- function findForwardProvider(config: OcxConfig): OcxProviderConfig | undefined {
98
- for (const prov of Object.values(config.providers)) {
99
- if (prov.disabled === true) continue;
100
- if (prov.authMode === "forward") return prov;
101
- }
102
- return undefined;
103
- }
104
-
105
97
  export interface AnthropicVisionProvider {
106
98
  providerName: string;
107
99
  provider: OcxProviderConfig;
@@ -136,9 +128,21 @@ function messagesHaveImage(parsed: OcxParsedRequest): boolean {
136
128
  carriesImages(m.role) && Array.isArray(m.content) && (m.content as OcxContentPart[]).some(p => p.type === "image"));
137
129
  }
138
130
 
131
+ export function shouldResolveOpenAiVisionSidecar(
132
+ config: OcxConfig,
133
+ provider: OcxProviderConfig,
134
+ modelId: string,
135
+ parsed: OcxParsedRequest,
136
+ ): boolean {
137
+ if (!modelInList(provider.noVisionModels, modelId) || !messagesHaveImage(parsed)) return false;
138
+ const cfg = config.visionSidecar ?? {};
139
+ if (cfg.enabled === false) return false;
140
+ return resolveVisionBackend(cfg.backend, findAnthropicVisionProvider(config)) === "openai";
141
+ }
142
+
139
143
  export interface VisionPlan {
140
144
  backend: "openai" | "anthropic";
141
- forwardProvider?: OcxProviderConfig;
145
+ forwardSidecar?: ResolvedOpenAiForwardSidecar;
142
146
  anthropicSidecar?: AnthropicVisionProvider;
143
147
  settings: VisionSettings;
144
148
  maxDescriptionsPerTurn: number;
@@ -155,8 +159,7 @@ export function planVisionSidecar(
155
159
  provider: OcxProviderConfig,
156
160
  modelId: string,
157
161
  parsed: OcxParsedRequest,
158
- incomingHeaders: Headers,
159
- authContext: CodexAuthContext = { kind: "main", accountId: null },
162
+ openAiSidecar?: ResolvedOpenAiForwardSidecar,
160
163
  ): VisionPlan | undefined {
161
164
  if (!modelInList(provider.noVisionModels, modelId)) return undefined;
162
165
  if (!messagesHaveImage(parsed)) return undefined;
@@ -176,12 +179,10 @@ export function planVisionSidecar(
176
179
  };
177
180
  }
178
181
 
179
- if (authContext.kind === "main" && !incomingHeaders.get("authorization")) return undefined;
180
- const forwardProvider = findForwardProvider(config);
181
- if (!forwardProvider) return undefined;
182
+ if (!openAiSidecar) return undefined;
182
183
  return {
183
184
  backend,
184
- forwardProvider,
185
+ forwardSidecar: openAiSidecar,
185
186
  settings: { model: cfg.model ?? DEFAULT_VISION_MODEL, timeoutMs: cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS },
186
187
  maxDescriptionsPerTurn,
187
188
  };
@@ -253,13 +254,13 @@ async function executeDescription(
253
254
  abortSignal,
254
255
  );
255
256
  }
256
- if (!plan.forwardProvider) return { text: "", error: "OpenAI vision sidecar is unavailable" };
257
+ if (!plan.forwardSidecar) return { text: "", error: "OpenAI vision sidecar is unavailable" };
257
258
  return describeImage(
258
259
  job.imageUrl,
259
260
  job.detail,
260
261
  job.contextText,
261
- plan.forwardProvider,
262
- selectedForwardHeaders,
262
+ plan.forwardSidecar.provider,
263
+ plan.forwardSidecar.headers,
263
264
  plan.settings,
264
265
  abortSignal,
265
266
  recordSidecarOutcome,
@@ -1,7 +1,7 @@
1
1
  import type { OcxConfig, OcxParsedRequest, OcxProviderConfig } from "../types";
2
2
  import { modelInList } from "../types";
3
3
  import type { SidecarSettings } from "./executor";
4
- import type { CodexAuthContext } from "../codex/auth-context";
4
+ import type { ResolvedOpenAiForwardSidecar } from "../providers/openai-sidecar";
5
5
  import { getAccountSet } from "../oauth/store";
6
6
 
7
7
  export { runWithWebSearch } from "./loop";
@@ -65,15 +65,6 @@ export function webSearchStallTimeoutSec(
65
65
  return Math.min(Number.MAX_VALUE, Math.ceil(largestUnitSec) + STALL_MARGIN_SEC);
66
66
  }
67
67
 
68
- /** First configured forward (ChatGPT passthrough) provider — the only path with server-side web_search. */
69
- export function findForwardProvider(config: OcxConfig): OcxProviderConfig | undefined {
70
- for (const prov of Object.values(config.providers)) {
71
- if (prov.disabled === true) continue;
72
- if (prov.authMode === "forward") return prov;
73
- }
74
- return undefined;
75
- }
76
-
77
68
  /** A configured anthropic-adapter OAuth provider whose ACTIVE stored account is usable (not needs-reauth). */
78
69
  export interface AnthropicSidecarProvider {
79
70
  providerName: string;
@@ -113,7 +104,7 @@ export interface SidecarPlan {
113
104
  /** Which executor runs the search. Anthropic does not require a forward provider. */
114
105
  backend: "openai" | "anthropic";
115
106
  /** Present for the openai backend (ChatGPT forward path); undefined for anthropic. */
116
- forwardProvider?: OcxProviderConfig;
107
+ forwardSidecar?: ResolvedOpenAiForwardSidecar;
117
108
  /** Present for the anthropic backend (stored-OAuth /v1/messages path); undefined for openai. */
118
109
  anthropicSidecar?: AnthropicSidecarProvider;
119
110
  hostedTool: Record<string, unknown>;
@@ -125,6 +116,16 @@ export interface SidecarPlan {
125
116
  stallTimeoutSec: number;
126
117
  }
127
118
 
119
+ export function shouldResolveOpenAiWebSearchSidecar(
120
+ config: OcxConfig,
121
+ parsed: OcxParsedRequest,
122
+ isPassthrough: boolean,
123
+ ): boolean {
124
+ if (!parsed._webSearch || isPassthrough) return false;
125
+ const cfg = config.webSearchSidecar ?? {};
126
+ return cfg.enabled !== false && resolveSidecarBackend(cfg.backend) === "openai";
127
+ }
128
+
128
129
  /**
129
130
  * Decide whether the web-search sidecar should handle this request, returning the plan if so. Active
130
131
  * when: web_search was requested (`parsed._webSearch`), the route is NOT the passthrough adapter
@@ -135,10 +136,9 @@ export function planWebSearch(
135
136
  config: OcxConfig,
136
137
  parsed: OcxParsedRequest,
137
138
  isPassthrough: boolean,
138
- incomingHeaders: Headers,
139
139
  provider: OcxProviderConfig,
140
140
  modelId: string,
141
- authContext: CodexAuthContext = { kind: "main", accountId: null },
141
+ openAiSidecar?: ResolvedOpenAiForwardSidecar,
142
142
  ): SidecarPlan | undefined {
143
143
  if (!parsed._webSearch || isPassthrough) return undefined;
144
144
  const cfg = config.webSearchSidecar ?? {};
@@ -178,12 +178,10 @@ export function planWebSearch(
178
178
  }
179
179
 
180
180
  // OpenAI backend: needs a ChatGPT login (main) and a forward provider to reach server-side web_search.
181
- if (authContext.kind === "main" && !incomingHeaders.get("authorization")) return undefined;
182
- const forwardProvider = findForwardProvider(config);
183
- if (!forwardProvider) return undefined;
181
+ if (!openAiSidecar) return undefined;
184
182
  return {
185
183
  backend: "openai",
186
- forwardProvider,
184
+ forwardSidecar: openAiSidecar,
187
185
  hostedTool: parsed._webSearch,
188
186
  settings: { model: cfg.model ?? DEFAULT_SIDECAR_MODEL, reasoning, timeoutMs, describeImages },
189
187
  maxSearches,
@@ -1 +0,0 @@
1
- :root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;--bg:var(--lightningcss-light,#fff)var(--lightningcss-dark,#212121);--rail:var(--lightningcss-light,#f9f9f9)var(--lightningcss-dark,#171717);--surface:var(--lightningcss-light,#fff)var(--lightningcss-dark,#262626);--raised:var(--lightningcss-light,#f4f4f4)var(--lightningcss-dark,#303030);--raised-hover:var(--lightningcss-light,#ececec)var(--lightningcss-dark,#3a3a3a);--border:var(--lightningcss-light,#e6e6e6)var(--lightningcss-dark,#3d3d3d);--border-soft:var(--lightningcss-light,#f0f0f0)var(--lightningcss-dark,#333);--hover:var(--lightningcss-light,#0d0d0d08)var(--lightningcss-dark,#ffffff08);--text:var(--lightningcss-light,#0d0d0d)var(--lightningcss-dark,#ececec);--muted:var(--lightningcss-light,#6e6e6e)var(--lightningcss-dark,#a6a6a6);--faint:var(--lightningcss-light,#707070)var(--lightningcss-dark,#9a9a9a);--accent:var(--lightningcss-light,#0d0d0d)var(--lightningcss-dark,#ececec);--accent-hover:var(--lightningcss-light,#3d3d3d)var(--lightningcss-dark,#fff);--accent-ink:var(--lightningcss-light,#fff)var(--lightningcss-dark,#0d0d0d);--accent-soft:var(--lightningcss-light,#0d0d0d0f)var(--lightningcss-dark,#ffffff17);--accent-ring:var(--lightningcss-light,#0d0d0d4d)var(--lightningcss-dark,#ffffff61);--green:var(--lightningcss-light,#0a7d5c)var(--lightningcss-dark,#4ecb9d);--green-soft:var(--lightningcss-light,#10a37f1a)var(--lightningcss-dark,#4ecb9d21);--red:var(--lightningcss-light,#b91c1c)var(--lightningcss-dark,#f87171);--red-soft:var(--lightningcss-light,#b91c1c17)var(--lightningcss-dark,#f8717121);--amber:var(--lightningcss-light,#9a4a08)var(--lightningcss-dark,#fbbf24);--amber-soft:var(--lightningcss-light,#b453091a)var(--lightningcss-dark,#fbbf2421);--space-0-5:2px;--space-1:4px;--space-1-5:6px;--space-2:8px;--space-3:12px;--space-4:16px;--space-5:20px;--space-6:24px;--space-8:32px;--space-10:40px;--space-12:48px;--space-16:64px;--radius-2xs:4px;--radius:12px;--radius-sm:8px;--radius-xs:6px;--radius-lg:16px;--radius-round:50%;--radius-pill:999px;--font-ui:"OpenAI Sans", "Pretendard Variable", Pretendard, "Noto Sans KR", "Apple SD Gothic Neo", "Malgun Gothic", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, sans-serif;--font-code:ui-monospace, "SFMono-Regular", "Cascadia Code", "JetBrains Mono", "Noto Sans Mono CJK KR", Menlo, Consolas, monospace;--font:var(--font-ui);--mono:var(--font-code);--text-micro:10px;--text-caption:11px;--text-label:12px;--text-control:13px;--text-body:14px;--text-subtitle:16px;--text-title:20px;--text-display:24px;--weight-regular:400;--weight-medium:500;--weight-semibold:600;--weight-bold:700;--leading-tight:1.2;--leading-ui:1.35;--leading-body:1.5;--leading-relaxed:1.6;--tracking-normal:0;--tracking-wide:.04em;--control-sm:28px;--control-md:34px;--control-lg:40px;--control-touch:44px;--icon-sm:14px;--icon-md:16px;--icon-lg:20px;--motion-fast:.12s;--motion-normal:.18s;--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);--toggle-w:36px;--toggle-h:20px;--toggle-dot:14px;--toggle-off-bg:var(--lightningcss-light,#d4d4d4)var(--lightningcss-dark,#4a4a4a);--toggle-on-bg:var(--lightningcss-light,#0d0d0d)var(--lightningcss-dark,#4ecb9d);--toggle-dot-color:var(--lightningcss-light,#fff)var(--lightningcss-dark,#0d0d0d)}@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}:root{--glass-rail:var(--lightningcss-light,#f9f9f9a8)var(--lightningcss-dark,#1717179e);--glass-panel:var(--lightningcss-light,#ffffffc7)var(--lightningcss-dark,#262626d1);--glass-blur:saturate(1.6) blur(22px)}*{box-sizing:border-box}html,body,#root{height:100%}html{background:var(--bg);overflow-x:hidden}body{color:var(--text);font-family:var(--font-ui);font-size:var(--text-body);line-height:var(--leading-body);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;background:0 0;margin:0;overflow-x:hidden}body:before{content:"";z-index:-1;pointer-events:none;filter:blur(70px);background:radial-gradient(42% 38% at 12% 6%,var(--lightningcss-light,#a4c4ff6b)var(--lightningcss-dark,#6082c829),#0000 70%),radial-gradient(46% 42% at 88% 18%,var(--lightningcss-light,#a8e2c561)var(--lightningcss-dark,#58a08221),#0000 70%),radial-gradient(40% 36% at 70% 92%,var(--lightningcss-light,#ffe0c24d)var(--lightningcss-dark,#b48c6414),#0000 72%);position:fixed;inset:-20%}a{color:var(--text);text-decoration:underline;-webkit-text-decoration-color:var(--faint);text-decoration-color:var(--faint);text-underline-offset:2px}a:hover{-webkit-text-decoration-color:var(--text);text-decoration-color:var(--text)}code{font-family:var(--font-code);font-size:var(--text-label)}.mono{font-family:var(--font-code);font-variant-numeric:tabular-nums}h1,h2,h3,h4{font-weight:var(--weight-semibold);letter-spacing:0;line-height:var(--leading-tight);margin:0}.text-micro{line-height:var(--leading-ui);font-size:var(--text-micro)!important}.text-caption{line-height:var(--leading-ui);font-size:var(--text-caption)!important}.text-label{line-height:var(--leading-ui);font-size:var(--text-label)!important}.text-control{line-height:var(--leading-ui);font-size:var(--text-control)!important}.text-body{line-height:var(--leading-body);font-size:var(--text-body)!important}.text-subtitle{line-height:var(--leading-tight);font-size:var(--text-subtitle)!important}.text-title{line-height:var(--leading-tight);font-size:var(--text-title)!important}.text-display{line-height:var(--leading-tight);font-size:var(--text-display)!important}.font-regular{font-weight:var(--weight-regular)!important}.font-medium{font-weight:var(--weight-medium)!important}.font-semibold{font-weight:var(--weight-semibold)!important}.font-bold{font-weight:var(--weight-bold)!important}.leading-tight{line-height:var(--leading-tight)!important}.leading-ui{line-height:var(--leading-ui)!important}.leading-body{line-height:var(--leading-body)!important}.leading-relaxed{line-height:var(--leading-relaxed)!important}::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-radius:var(--radius-pill);border:2px solid var(--bg)}::-webkit-scrollbar-thumb:hover{background:var(--faint)}:focus-visible{outline:2px solid var(--accent-ring);outline-offset:2px;border-radius:var(--radius-2xs)}.app{grid-template-columns:232px 1fr;min-height:100dvh;display:grid}.sidebar{border-right:1px solid var(--border);background:var(--glass-rail);height:100dvh;-webkit-backdrop-filter:var(--glass-blur);flex-direction:column;align-self:start;gap:4px;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{font-weight:var(--weight-semibold);font-size:var(--text-subtitle);letter-spacing:0;line-height:26px}.brand .ver{font-family:var(--font-code);font-size:var(--text-micro);color:var(--muted);line-height:var(--leading-tight);background:var(--raised);border:1px solid var(--border);border-radius:var(--radius-pill);align-self:center;padding:2px 6px}.sidebar nav{flex-direction:column;gap:4px;display:flex}.nav-item{border-radius:var(--radius-sm);text-align:left;cursor:pointer;width:100%;color:var(--muted);font:inherit;font-size:var(--text-control);font-weight:var(--weight-medium);transition:background var(--motion-fast), color var(--motion-fast);background:0 0;border:none;align-items:center;gap:10px;padding:8px 10px;display:flex}.nav-item:hover,.nav-item.active{background:var(--accent-soft);color:var(--text)}.nav-item.active{font-weight:var(--weight-semibold)}.nav-item svg{width:17px;height:17px;color:var(--faint);flex-shrink:0}.nav-item.active svg{color:var(--text)}.sidebar-foot{flex-direction:column;gap:2px;margin-top:auto;padding-top:12px;display:flex}.drawer-head{align-items:flex-start;display:flex}.drawer-head .brand{flex:auto;min-width:0}.mobile-topbar{display:none}.menu-toggle{border-radius:var(--radius-sm);min-width:44px;min-height:44px;color:var(--muted);cursor:pointer;transition:background var(--motion-fast), color var(--motion-fast);background:0 0;border:none;justify-content:center;align-items:center;padding:8px;display:none}.menu-toggle:hover{background:var(--accent-soft);color:var(--text)}.menu-toggle svg{width:20px;height:20px}.drawer-scrim{z-index:30;background:var(--lightningcss-light,#14141452)var(--lightningcss-dark,#00000085);display:none;position:fixed;inset:0}.sidebar-link{color:var(--muted);font-size:var(--text-control);border-radius:var(--radius-sm);align-items:center;gap:9px;padding:8px 10px;text-decoration:none;display:flex}.sidebar-link:hover{background:var(--accent-soft);color:var(--text);text-decoration:none}.sidebar-link svg{width:16px;height:16px}.lang-toggle{width:100%;color:var(--muted);font-size:var(--text-control);border-radius:var(--radius-sm);transition:background var(--motion-fast), color var(--motion-fast);align-items:center;gap:9px;padding:8px 10px;display:flex;position:relative}.lang-toggle:hover{background:var(--accent-soft);color:var(--text)}.lang-toggle svg{flex-shrink:0;width:16px;height:16px}.lang-toggle .custom-select{width:100%;position:static!important}.lang-toggle .select-trigger{width:100%;color:inherit;font-size:var(--text-control);min-height:auto;box-shadow:none;-webkit-backdrop-filter:none;background:0 0;border:none;justify-content:space-between;padding:0}.lang-toggle .select-trigger:hover:not(:disabled){color:inherit;box-shadow:none;background:0 0;border:none}.lang-toggle .select-dropdown{background:var(--glass-rail);-webkit-backdrop-filter:var(--glass-blur);border:1px solid var(--border);box-shadow:var(--shadow-sm)}.lang-toggle .select-dropdown-beside{min-width:10rem;top:0;left:calc(100% + 6px);right:auto}.theme-toggle{text-align:left;cursor:pointer;width:100%;color:var(--muted);font:inherit;font-size:var(--text-control);border-radius:var(--radius-sm);transition:background var(--motion-fast), color var(--motion-fast);background:0 0;border:none;align-items:center;gap:9px;padding:8px 10px;display:flex}.theme-toggle:hover{background:var(--accent-soft);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:var(--text-title)}.page-sub{color:var(--muted);font-size:var(--text-body);max-width:70ch;margin:4px 0 22px}.page-sub b{color:var(--text);font-weight:var(--weight-semibold)}.api-page h2 svg{vertical-align:-.16em;width:1em;height:1em}.api-page .page-sub code{font-size:var(--text-label);color:var(--text);background:var(--raised);border:1px solid var(--border-soft);border-radius:var(--radius-xs);padding:1px 4px}.btn{border-radius:var(--radius-pill);font:inherit;font-size:var(--text-control);font-weight:var(--weight-medium);line-height:var(--leading-ui);cursor:pointer;transition:background var(--motion-fast), border-color var(--motion-fast), opacity var(--motion-fast);white-space:nowrap;border:1px solid #0000;justify-content:center;align-items:center;gap:7px;padding:8px 16px;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(--bg);color:var(--text);border-color:var(--border)}.btn-ghost:hover:not(:disabled){background:var(--raised)}.btn-danger{color:var(--red);background:0 0;border-color:#f871714d}.btn-danger:hover:not(:disabled){background:var(--red-soft)}.btn-sm{font-size:var(--text-label);border-radius:var(--radius-pill);padding:4px 12px}.btn-icon{appearance:none;font:inherit;cursor:pointer;width:28px;height:28px;color:var(--muted);border-radius:var(--radius-sm);transition:background var(--motion-fast), color var(--motion-fast);background:0 0;border:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.btn-icon:hover{background:var(--raised-hover);color:var(--text)}.btn-icon:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}.btn-icon svg{width:16px;height:16px;display:block}.btn.btn-ghost.btn-icon{width:28px;height:28px;padding:0}.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{border-color:color-mix(in srgb, var(--accent) 28%, var(--border));background:color-mix(in srgb, var(--accent) 5%, var(--surface))}.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(--font-code);font-size:var(--text-label);line-height:var(--leading-relaxed);white-space:pre;margin:0;padding:9px 11px;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{font-size:var(--text-label);flex-wrap:wrap;align-items:flex-start;gap:6px;display:flex}.update-command .chip{white-space:pre-wrap;word-break:break-all;overflow-wrap:anywhere;flex:auto;min-width:0;max-width:100%;line-height:1.5}.update-recheck{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.update-recheck .btn{flex:none}.update-recheck-reason{min-width:0;font-size:var(--text-label);color:var(--muted);flex:200px}.update-restart{border-top:1px solid var(--border-soft);padding-top:12px}.injection-head{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.injection-label{font-weight:var(--weight-semibold)}@media (width<=800px){.injection-head{gap:8px}.injection-label{flex:1 0 calc(100% - 70px);order:1}.injection-head>.badge{order:1;margin-left:auto}.injection-head>.custom-select{order:2}}.stat-row{grid-template-columns:repeat(6,1fr);gap:12px;margin-bottom:28px;display:grid}.stat-row>.stat{flex-direction:column;justify-content:center;min-height:80px;display:flex}@media (width<=1100px){.stat-row{grid-template-columns:repeat(3,1fr)}}@media (width<=640px){.stat-row{grid-template-columns:repeat(2,1fr)}}.stat{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);transition:border-color var(--motion-fast);padding:14px 16px}.stat:hover{border-color:var(--faint)}.stat .label{font-size:var(--text-label);color:var(--muted);font-weight:var(--weight-medium);align-items:center;gap:6px;margin-bottom:6px;display:flex}.stat .label svg{width:14px;height:14px}.stat .value{font-size:var(--text-title);font-weight:var(--weight-semibold);letter-spacing:0;line-height:var(--leading-tight)}.stat .value.mono{font-family:var(--font-code);font-size:var(--text-subtitle)}.model-group-head{font-size:var(--text-control);font-weight:var(--weight-semibold);color:var(--text);align-items:baseline;gap:8px;margin:0 0 8px;display:flex}.model-group-head .count{font-family:var(--font-code);font-weight:var(--weight-medium);color:var(--faint);font-size:var(--text-label)}.group-head{cursor:pointer;background:var(--surface);transition:background var(--motion-fast);padding:10px 12px}.group-head:hover{background:var(--hover)}.group-head.open{border-bottom:1px solid var(--border-soft)}.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);transition:border-color var(--motion-fast), background var(--motion-fast);padding:10px 12px}.model-card:hover{border-color:var(--faint);background:var(--hover)}.model-card .id{font-family:var(--font-code);font-weight:var(--weight-semibold);font-size:var(--text-control);letter-spacing:0;color:var(--text)}.badge{font-size:var(--text-caption);font-weight:var(--weight-semibold);line-height:var(--leading-ui);border-radius:var(--radius-pill);font-family:var(--font-code);letter-spacing:0;border:1px solid #0000;align-items:center;gap:5px;padding:2px 8px;display:inline-flex}.badge-accent{background:var(--accent-soft);color:var(--text)}.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;transition:filter var(--motion-fast);appearance:none}.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{border-radius:var(--radius-round);background:var(--amber-soft);width:44px;height:44px;color:var(--amber);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);transition:border-color var(--motion-fast);padding:8px 10px}.credit-next{border-color:var(--amber);background:var(--amber-soft)}.credit-item-head{color:var(--text);font-size:var(--text-label);font-weight:var(--weight-semibold);align-items:center;gap:6px;margin-bottom:3px;display:flex}.credit-item-head svg{color:var(--amber);flex-shrink:0}.credit-item-dates{font-size:var(--text-caption);font-family:var(--font-code);color:var(--muted);justify-content:space-between;display:flex}.credit-urgent{color:var(--red);font-weight:var(--weight-semibold)}.dot{border-radius:var(--radius-round);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:var(--text-control)}.tbl thead th{text-align:left;color:var(--muted);font-weight:var(--weight-medium);font-size:var(--text-label);border-bottom:1px solid var(--border);padding:9px 12px}.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(--font-code);font-variant-numeric:tabular-nums}.tbl-wrap{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);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;font-size:var(--text-control);line-height:var(--leading-ui);transition:border-color var(--motion-fast);padding:8px 11px}.input::placeholder{color:var(--faint)}.input:focus{border-color:var(--faint);box-shadow:0 0 0 3px var(--accent-soft);outline:none}textarea.input{resize:vertical;font-family:var(--font-code);line-height:var(--leading-relaxed)}.field-label{font-size:var(--text-label);color:var(--muted);font-weight:var(--weight-medium);margin-bottom:5px;display:block}select.input{appearance:none}.select-sm{border-radius:var(--radius-pill);background:color-mix(in oklab, canvas 75%, transparent);-webkit-backdrop-filter:blur(12px)saturate(1.3);border:1px solid var(--border);color:var(--text);font:inherit;font-size:var(--text-control);line-height:var(--leading-ui);cursor:pointer;transition:border-color var(--motion-fast), box-shadow var(--motion-fast);padding:6px 12px;box-shadow:0 2px 8px #0000000f}.select-sm:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);outline:none}.select-sm:hover:not(:disabled){border-color:var(--faint);box-shadow:0 2px 12px #0000001a}.select-sm:disabled{opacity:.5;cursor:default;-webkit-backdrop-filter:none}.select-trigger{border-radius:var(--radius-pill);background:color-mix(in oklab, canvas 75%, transparent);-webkit-backdrop-filter:blur(12px)saturate(1.3);border:1px solid var(--border);color:var(--text);font:inherit;font-size:var(--text-control);line-height:var(--leading-ui);cursor:pointer;transition:border-color var(--motion-fast), box-shadow var(--motion-fast);white-space:nowrap;align-items:center;gap:6px;padding:6px 12px;display:inline-flex;box-shadow:0 2px 8px #0000000f}.select-trigger:hover:not(:disabled){border-color:var(--faint);box-shadow:0 2px 12px #0000001a}.select-trigger:disabled{opacity:.5;cursor:default}.select-dropdown{z-index:40;background:color-mix(in oklab, canvas 84%, transparent);-webkit-backdrop-filter:blur(20px)saturate(1.4);border:1px solid var(--border);border-radius:var(--radius);min-width:100%;max-height:280px;padding:4px;position:absolute;top:calc(100% + 4px);left:0;overflow-y:auto;box-shadow:0 8px 32px #00000029}.select-dropdown-right{right:0;left:auto!important}.select-dropdown-beside{min-width:10rem;top:0;left:calc(100% + 6px);right:auto}.select-option{text-align:left;border-radius:var(--radius-sm);width:100%;color:var(--text);font:inherit;font-size:var(--text-control);line-height:var(--leading-ui);cursor:pointer;transition:background var(--motion-fast);white-space:nowrap;background:0 0;border:none;padding:7px 12px;display:block}.select-option:hover{background:var(--hover)}.select-option.active{background:var(--accent-soft);color:var(--accent);font-weight:var(--weight-semibold)}.switch{border-radius:var(--radius-pill);border:1px solid var(--border);cursor:pointer;background:var(--raised);vertical-align:middle;width:34px;height:20px;transition:background var(--motion-normal), border-color var(--motion-normal);appearance:none;flex-shrink:0;align-items:center;padding:2px;line-height:0;display:inline-flex}.switch.on{background:var(--toggle-on-bg);border-color:var(--toggle-on-bg)}.switch:disabled{opacity:.6;cursor:default}.switch .knob{border-radius:var(--radius-round);width:14px;height:14px;transition:transform var(--motion-normal), background var(--motion-normal);background:var(--lightningcss-light,#fff)var(--lightningcss-dark,#ececec);transform:translate(0);box-shadow:0 0 0 1px #10182814,0 1px 1px #1018282e}.switch.on .knob{background:var(--toggle-dot-color);transform:translate(14px);box-shadow:0 0 0 1px #0000001f}.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{font-size:var(--text-control);line-height:var(--leading-body);margin-top:3px}.stack{flex-direction:column;display:flex}.chip{font-family:var(--font-code);font-size:var(--text-label);line-height:var(--leading-ui);background:var(--raised);border:1px solid var(--border);border-radius:var(--radius-xs);color:var(--text);padding:1px 7px}.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);font-weight:var(--weight-semibold);margin-bottom:6px}.notice{font-size:var(--text-control);line-height:var(--leading-body);border-radius:var(--radius-sm);align-items:center;gap:8px;margin-bottom:14px;padding:9px 12px;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{font-size:var(--text-control);font-weight:var(--weight-semibold);line-height:var(--leading-ui);color:var(--text);align-items:center;gap:8px;margin:30px 0 12px;display:flex}.h-section .count{color:var(--muted);font-weight:var(--weight-medium);font-family:var(--font-code);font-size:var(--text-label)}.spin{border:2px solid var(--border);border-top-color:var(--accent);border-radius:var(--radius-round);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}}@supports not ((-webkit-backdrop-filter:blur(1px)) or (backdrop-filter:blur(1px))){.sidebar,.lang-toggle .select-dropdown{background:var(--rail)}.modal-card{background:var(--surface)}}@media (prefers-reduced-transparency:reduce){body:before{display:none}.sidebar,.lang-toggle .select-dropdown{background:var(--rail);-webkit-backdrop-filter:none}.modal-card{background:var(--surface);-webkit-backdrop-filter:none}.modal-overlay{-webkit-backdrop-filter:none}}.modal-overlay{-webkit-backdrop-filter:blur(40px)saturate(1.2);z-index:50;background:var(--lightningcss-light,#11131cc7)var(--lightningcss-dark,#000000d1);justify-content:center;align-items:flex-start;padding:8vh 16px;display:flex;position:fixed;inset:0}.modal-card{background:color-mix(in oklab, canvas 92%, transparent);-webkit-backdrop-filter:blur(20px)saturate(1.4);border:1px solid var(--border);border-radius:var(--radius-lg);width:100%;max-width:520px;max-height:84vh;padding:20px;overflow-y:auto;box-shadow:0 8px 32px #0000002e}.modal-head{justify-content:space-between;align-items:center;margin-bottom:16px;display:flex}.modal-head h3{font-size:var(--text-subtitle)}.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{font-size:var(--text-label);line-height:var(--leading-body);color:var(--muted);overflow-wrap:anywhere;min-width:0;padding:0 16px 10px}.card-active{border-color:var(--accent-ring)}.card-right{font-size:var(--text-caption);color:var(--faint);align-items:center;gap:4px;margin-left:auto;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{font-size:var(--text-label);color:var(--muted);font-weight:var(--weight-medium);white-space:nowrap}.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{font-size:var(--text-caption);color:var(--muted);font-weight:var(--weight-semibold)}.quota-val{font-size:var(--text-caption);font-family:var(--font-code);color:var(--text);text-align:right}.quota-reset-label{text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-caption);color:var(--faint);overflow:hidden}.quota-reset-day,.quota-reset-time{font-size:var(--text-caption);color:var(--muted);white-space:nowrap}.quota-reset-time{font-family:var(--font-code)}.bar{background:var(--raised);border-radius:var(--radius-2xs);min-width:0;height:5px;overflow:hidden}.bar-fill{border-radius:var(--radius-2xs);height:100%;transition:width var(--motion-normal)}.bar-green{background:var(--green)}.bar-amber{background:linear-gradient(90deg, var(--green), var(--amber))}.toggle{width:var(--toggle-w);height:var(--toggle-h);border-radius:var(--radius-pill);background:var(--toggle-off-bg);border:1px solid var(--border);cursor:pointer;transition:background var(--motion-normal), border-color var(--motion-normal);flex-shrink:0;position:relative}.toggle.on{background:var(--toggle-on-bg);border-color:var(--toggle-on-bg)}.toggle-knob{border-radius:var(--radius-round);width:16px;height:16px;transition:left var(--motion-normal), background var(--motion-normal);background:var(--lightningcss-light,#fff)var(--lightningcss-dark,#ececec);position:absolute;top:1px;left:1px}.toggle.on .toggle-knob{background:var(--toggle-dot-color);left:17px}.notice-warn{font-size:var(--text-label);line-height:var(--leading-body);border-radius:var(--radius-sm);background:var(--amber-soft);color:var(--amber);align-items:center;gap:6px;margin-bottom:12px;padding:8px 10px;display:flex}.notice-warn svg{flex-shrink:0;width:14px;height:14px}.modal-desc{font-size:var(--text-control);line-height:var(--leading-body);color:var(--muted);margin-bottom:14px}.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;font-size:var(--text-caption);white-space:nowrap;background:0 0;border:none;padding:0;text-decoration:underline}.log-detail-grid{font-size:var(--text-control);grid-template-columns:max-content minmax(0,1fr);gap:6px 14px;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(--font-code);font-size:var(--text-caption);line-height:var(--leading-body);white-space:pre-wrap;word-break:break-all;max-height:40vh;margin:0;padding:10px 12px;overflow:auto}.setup-guide{font-size:var(--text-control);line-height:var(--leading-body);border:1px solid var(--border);border-radius:var(--radius-sm);margin-bottom:4px;padding:8px 12px}.setup-guide summary{cursor:pointer;color:var(--accent-hover);font-weight:var(--weight-medium)}.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;transition:background var(--motion-fast), border-color var(--motion-fast);justify-content:space-between;align-items:center;gap:10px;padding:11px 13px;display:flex}.list-row:hover{background:var(--raised-hover);border-color:var(--accent-ring)}.list-row .title{font-weight:var(--weight-semibold);font-size:var(--text-body)}.list-row .sub{font-size:var(--text-label);color:var(--muted);margin-top:2px}.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{border-radius:var(--radius-xs);background:var(--raised);border:1px solid var(--border-soft);width:31px;height:31px;color:var(--text);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;font-size:var(--text-control);cursor:pointer;background:0 0;border:none;padding:6px 2px;text-decoration:underline}.link-btn svg{flex-shrink:0;width:14px;height:14px}.prov-accounts-toggle{border:none;border-top:1px solid var(--border-soft);width:100%;color:var(--muted);font:inherit;font-size:var(--text-label);cursor:pointer;background:0 0;justify-content:center;align-items:center;gap:6px;min-height:24px;padding:4px 0;display:flex}.prov-accounts-toggle:hover{color:var(--text);background:var(--raised)}.prov-accounts-toggle .chev{transition:transform var(--motion-normal) ease;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;font-size:var(--text-control);line-height:var(--leading-ui);cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;min-height:32px;padding:6px 8px;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:var(--text-label)}.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{min-width:0;font-size:var(--text-label);height:var(--control-sm);flex:auto;padding:4px 8px}.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{font-size:var(--text-control);font-weight:var(--weight-semibold);align-items:center;gap:8px;min-width:0;display:inline-flex}.oauth-name-text{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.provider-icon-sm{border-radius:var(--radius-xs);width:24px;height:24px}.provider-icon-sm img{width:15px;height:15px}.oauth-status{font-size:var(--text-control);align-items:center;gap:7px;min-width:0;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{font-size:var(--text-label);line-height:var(--leading-body);flex-direction:column;grid-column:1/-1;align-items:stretch;gap:8px;display:flex}.oauth-login-hint-links{flex-wrap:wrap;align-items:center;gap:8px;display:inline-flex}.oauth-login-paste{align-items:center;gap:8px;width:100%;display:flex}.oauth-login-paste .input{min-width:0;font-size:var(--text-label);flex:1;padding:6px 10px}@media (width<=760px){.app{grid-template-rows:auto 1fr;grid-template-columns:1fr}.mobile-topbar{z-index:20;border-bottom:1px solid var(--border);background:var(--glass-rail);min-width:0;-webkit-backdrop-filter:var(--glass-blur);align-items:center;gap:2px;padding:4px 10px;display:flex;position:sticky;top:0}.mobile-topbar .brand{flex:auto;min-width:0;padding:4px}.mobile-topbar .stop-toggle{justify-content:center;width:auto;min-width:44px;min-height:44px;padding:8px}.menu-toggle{display:inline-flex}.sidebar{z-index:40;width:min(280px,84vw);height:100dvh;padding-bottom:calc(18px + env(safe-area-inset-bottom));visibility:hidden;transition:transform var(--motion-normal) ease, visibility var(--motion-normal);background:var(--lightningcss-light,#f9f9f9f7)var(--lightningcss-dark,#171717f5);outline:none;position:fixed;top:0;bottom:0;left:0;overflow-y:auto;transform:translate(-100%)}.sidebar.open{visibility:visible;transform:translate(0);box-shadow:0 12px 40px var(--lightningcss-light,#14141438)var(--lightningcss-dark,#00000080)}.drawer-scrim{display:block}.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>.stat{min-width:100px}.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}.sidebar .lang-toggle .select-dropdown-beside{inset:auto auto calc(100% + 6px) 0}}.usage-cards{grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-top:8px;display:grid}.usage-cards .stat-value{font-size:var(--text-title);font-weight:var(--weight-semibold);margin-top:4px}.usage-head{flex-wrap:wrap}.usage-filters{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;display:flex}.usage-segmented{border:1px solid var(--border);border-radius:var(--radius-pill);background:var(--surface);gap:2px;padding:2px;display:inline-flex}.usage-segmented-btn{color:var(--muted);border-radius:var(--radius-pill);cursor:pointer;font:inherit;white-space:nowrap;background:0 0;border:none;justify-content:center;align-items:center;gap:6px;padding:4px 12px;display:inline-flex}.usage-segmented-btn.active{background:var(--raised);color:var(--text);font-weight:var(--weight-semibold)}.usage-source-mark{width:var(--icon-sm);height:var(--icon-sm);object-fit:contain;flex:none}:root[data-theme=dark] .usage-source-mark{filter:invert()}@media (prefers-color-scheme:dark){:root:not([data-theme=light]) .usage-source-mark{filter:invert()}}@media (width<=760px){.usage-segmented-btn{min-height:var(--control-touch)}}@media (width<=640px){.usage-source-btn .usage-source-label-collapsible{display:none}}@media (width<=360px){.usage-filters{flex-direction:column;align-items:stretch;width:100%}.usage-segmented{width:100%}.usage-segmented-btn{flex:1 1 0}}.panel-title{font-size:var(--text-body);font-weight:var(--weight-semibold);color:var(--text);margin:0 0 12px}.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{font-size:var(--text-caption);color:var(--muted);width:max-content;margin-bottom:-2px;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);font-size:var(--text-micro);color:var(--muted);background:var(--surface);z-index:1;flex-shrink:0;align-items:center;width:25px;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);border-radius:var(--radius-2xs);background:var(--border)}.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{font-size:var(--text-label);align-self:flex-end;align-items:center;gap:4px;display:inline-flex;position:sticky;right:0}.heatmap-legend .heatmap-cell{width:10px;height:10px}.heatmap-tip{z-index:10;pointer-events:none;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);white-space:nowrap;font-size:var(--text-label);padding:6px 10px;position:fixed;transform:translate(-50%,-100%)translateY(-8px);box-shadow:0 6px 20px #00000059}.heatmap-tip-date{font-weight:var(--weight-semibold);color:var(--text);margin-bottom:2px}.heatmap-tip-val{color:var(--text);font-variant-numeric:tabular-nums}.heatmap-tip-req{font-size:var(--text-caption)}.usage-bar{background:var(--border);border-radius:var(--radius-pill);width:100%;min-width:60px;height:6px;overflow:hidden}.usage-bar-fill{background:var(--green);border-radius:var(--radius-pill);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:var(--radius-xs);flex:1;align-items:flex-end;width:100%;max-width:48px;display:flex;overflow:hidden}.daybar-stack{border-radius:var(--radius-xs) var(--radius-xs) 0 0;width:100%;min-height:2px;transition:height var(--motion-normal) ease;flex-direction:column-reverse;display:flex;overflow:hidden}.daybar-seg{width:100%;min-height:1px}.daybar-count{font-size:var(--text-label);font-weight:var(--weight-semibold);color:var(--text)}.daybar-label{font-size:var(--text-caption);white-space:nowrap}.daybar:hover .daybar-track{outline:1px solid var(--border)}.daybar-tip{z-index:5;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);pointer-events:none;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{font-size:var(--text-label);font-weight:var(--weight-semibold);color:var(--text);white-space:nowrap;margin-bottom:6px}.daybar-tip-row{font-size:var(--text-label);line-height:var(--leading-relaxed);align-items:center;gap:8px;display:flex}.daybar-tip-swatch{border-radius:var(--radius-2xs);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}.toggle{width:var(--toggle-w);height:var(--toggle-h);flex-shrink:0;display:inline-block;position:relative}.toggle input{opacity:0;width:0;height:0;position:absolute}.toggle .slider{background:var(--toggle-off-bg);border-radius:var(--radius-pill);cursor:pointer;transition:background var(--motion-normal) ease;position:absolute;inset:0}.toggle .slider:after{content:"";width:var(--toggle-dot);height:var(--toggle-dot);background:var(--toggle-dot-color);border-radius:var(--radius-round);transition:transform var(--motion-normal) ease;position:absolute;top:3px;left:3px}.toggle input:checked+.slider{background:var(--toggle-on-bg)}.toggle input:checked+.slider:after{transform:translateX(calc(var(--toggle-w) - var(--toggle-dot) - 6px))}.toggle input:focus-visible+.slider{outline:2px solid var(--accent-ring);outline-offset:2px}.setting-row{justify-content:space-between;align-items:center;gap:16px;padding:12px 16px;display:flex}.setting-row+.setting-row{border-top:1px solid var(--border-soft)}.setting-label{flex-direction:column;gap:2px;min-width:0;display:flex}.setting-label .title{font-size:var(--text-body);font-weight:var(--weight-semibold);color:var(--text)}.setting-label .desc{font-size:var(--text-label);color:var(--muted);line-height:var(--leading-body)}