@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.
- package/README.ko.md +37 -7
- package/README.md +52 -11
- package/README.zh-CN.md +36 -7
- package/bin/ocx.mjs +5 -3
- package/gui/dist/assets/index-BzhyTAco.js +40 -0
- package/gui/dist/assets/index-Dq3eZ1cU.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/opencode.svg +1 -1
- package/package.json +5 -2
- package/src/adapters/anthropic-image-normalize.ts +70 -29
- package/src/adapters/cursor/transport-retry.ts +20 -1
- package/src/adapters/run-turn-queue.ts +40 -0
- package/src/codex/auth-api.ts +10 -1
- package/src/codex/auth-context.ts +33 -7
- package/src/codex/catalog.ts +357 -23
- package/src/codex/routing.ts +10 -4
- package/src/combos/failover.ts +102 -0
- package/src/combos/index.ts +37 -0
- package/src/combos/request.ts +31 -0
- package/src/combos/resolve.ts +171 -0
- package/src/combos/types.ts +203 -0
- package/src/config.ts +280 -11
- package/src/lib/errors.ts +86 -24
- package/src/lib/upstream-retry.ts +8 -4
- package/src/oauth/index.ts +7 -1
- package/src/oauth/key-providers.ts +2 -32
- package/src/oauth/login-cli.ts +4 -3
- package/src/oauth/token-guardian.ts +38 -3
- package/src/providers/derive.ts +27 -2
- package/src/providers/kiro-models.ts +8 -3
- package/src/providers/label.ts +3 -1
- package/src/providers/openai-sidecar.ts +94 -0
- package/src/providers/openai-tier-startup.ts +27 -0
- package/src/providers/openai-tiers.ts +283 -0
- package/src/providers/openai-virtual-models.ts +82 -0
- package/src/providers/quota.ts +344 -24
- package/src/providers/registry.ts +112 -20
- package/src/reasoning-effort.ts +12 -11
- package/src/router.ts +80 -36
- package/src/server/auth-cors.ts +85 -9
- package/src/server/images.ts +31 -75
- package/src/server/index.ts +45 -86
- package/src/server/management-api.ts +273 -21
- package/src/server/request-log.ts +221 -20
- package/src/server/responses.ts +594 -75
- package/src/server/search.ts +22 -37
- package/src/types.ts +49 -1
- package/src/update/index.ts +50 -6
- package/src/update/job.ts +21 -4
- package/src/usage/log.ts +124 -1
- package/src/usage/summary.ts +147 -56
- package/src/vision/index.ts +20 -19
- package/src/web-search/index.ts +15 -17
- package/gui/dist/assets/index-Bk_GgFrh.css +0 -1
- package/gui/dist/assets/index-DQjt6Hly.js +0 -40
package/src/usage/summary.ts
CHANGED
|
@@ -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(
|
|
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
|
|
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(
|
|
178
|
-
const mKey = `${providerKey}/${
|
|
229
|
+
const providerKey = baseProviderLabel(attribution.provider);
|
|
230
|
+
const mKey = `${providerKey}/${attribution.model}`;
|
|
179
231
|
let m = models.get(mKey);
|
|
180
|
-
if (!m) {
|
|
181
|
-
|
|
182
|
-
|
|
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,
|
|
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
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
model.
|
|
242
|
-
|
|
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
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
provider
|
|
257
|
-
provider
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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);
|
package/src/vision/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 (
|
|
180
|
-
const forwardProvider = findForwardProvider(config);
|
|
181
|
-
if (!forwardProvider) return undefined;
|
|
182
|
+
if (!openAiSidecar) return undefined;
|
|
182
183
|
return {
|
|
183
184
|
backend,
|
|
184
|
-
|
|
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.
|
|
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.
|
|
262
|
-
|
|
262
|
+
plan.forwardSidecar.provider,
|
|
263
|
+
plan.forwardSidecar.headers,
|
|
263
264
|
plan.settings,
|
|
264
265
|
abortSignal,
|
|
265
266
|
recordSidecarOutcome,
|
package/src/web-search/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
182
|
-
const forwardProvider = findForwardProvider(config);
|
|
183
|
-
if (!forwardProvider) return undefined;
|
|
181
|
+
if (!openAiSidecar) return undefined;
|
|
184
182
|
return {
|
|
185
183
|
backend: "openai",
|
|
186
|
-
|
|
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)}
|