@lyogvce/dsh-codex-subscription 0.2.13 → 0.3.1
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.md +3 -2
- package/lib/client.js +326 -19
- package/lib/index.d.ts +45 -9
- package/lib/index.js +299 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
- 通过 `retryPolicy` 暴露 DSH 原生 `RetryPolicySchema`(支持 `normal` / `always` 模式、`maxRetries`、`retryableCodes` 与 `backoff`,并随 `registration.replace` 热更新生效)。未配置时保持宿主默认策略(`mode: "normal"`、`maxRetries: 5`、默认 5 类错误码 `EMPTY_RESPONSE / RATE_LIMIT / SERVER / TIMEOUT / TRANSPORT` 及 `500ms..10000ms` 退避)。**注意:`retryableCodes` 为完整覆盖集合而非追加列表**,显式配置时需同时列出默认错误码与新增错误码。
|
|
22
22
|
4. **原生多轮思考链回放、工具调用与出站指纹净化**
|
|
23
23
|
- 底层复用 DSH 内置的 `PiAiAdapter` 与 `@earendil-works/pi-ai` 的 `openaiCodexProvider()`,固定使用 `sse` 传输(含 `zstd` 请求体压缩、规范化 36 位 RFC 4122 UUID `session-id` 缓存头与 `prompt_cache_key`、`reasoning.encrypted_content` 签名回放与图片自动缩放卸载),并通过自定义 `fetch` 与 `onPayload` 剥除 `OpenAI-Beta: responses=experimental`、静态 `x-client-request-id`、`msg_pi_*` 等非官方指纹。
|
|
24
|
-
5. **Codex
|
|
25
|
-
- 当会话选中 `openai-codex` 模型时,在输入框右下角紧邻模型选择器左侧展示限额胶囊(根据 `wham/usage` 返回的 `limit_window_seconds` 自适应展示 Plus 的 `5h X% · 7d Y%` 双窗口或 Pro 的 `7d X%`
|
|
24
|
+
5. **Codex 订阅限额、限额重置卡(Rate Limit Reset Credits)与 ⚡ Fast 极速模式一键切换(`conversation.input.right`)**
|
|
25
|
+
- 当会话选中 `openai-codex` 模型时,在输入框右下角紧邻模型选择器左侧展示限额胶囊(根据 `wham/usage` 返回的 `limit_window_seconds` 自适应展示 Plus 的 `5h X% · 7d Y%` 双窗口或 Pro 的 `7d X%` 单窗口;由模型请求与流完成结算触发、按 60 秒限流在后台自动更新缓存,并由前端周期静默同步,无请求时零远端轮询,点击胶囊直接展开详情弹窗不触发额外请求),弹窗内可查看窗口进度条、重置倒计时与手动刷新按钮;切换至其他 Provider 模型时自动隐藏。
|
|
26
|
+
- **🎫 限额重置卡展示与二次确认核销**:自动解析 `wham/usage` 中的 `rate_limit_reset_credits`,并在持有可用重置卡(`available_count > 0`)时按需拉取 `wham/rate-limit-reset-credits` 详情,在弹窗内展示每张重置卡各自的过期时间,支持带内联二次确认的核销操作(`POST /wham/rate-limit-reset-credits/consume`)并在核销后自动刷新最新额度。
|
|
26
27
|
- **⚡ 会话树级 Fast 极速模式(`service_tier: "priority"`,最高 2x 推理速度,主会话与 Subagent / 智能体团队同开同关)**:每个主会话可在限额弹窗底部独立开关 Fast 模式(不同主会话互不影响,新会话默认继承插件设置中的 `fastMode`,未配置时默认 `false` 关闭),该主会话派生的所有 `subagent`、`subagent_fork`、`workflow` 子任务以及智能体团队成员(`spawn_teammate`)均自动绑定至同一主会话开关,实时联动、同开同关;状态自动持久化至 `~/.codex/cache/dsh-codex-subscription/runtime-state.json`。开启后胶囊显示闪电标识 `⚡`,并在请求体中自动注入 `"service_tier": "priority"`。
|
|
27
28
|
|
|
28
29
|
---
|
package/lib/client.js
CHANGED
|
@@ -47,6 +47,20 @@ window.__ModuleLoader__.load({
|
|
|
47
47
|
function clampPercent(value) {
|
|
48
48
|
return Math.max(0, Math.min(100, Math.round(value)));
|
|
49
49
|
}
|
|
50
|
+
function normalizeConsumeOutcome(raw) {
|
|
51
|
+
if (typeof raw !== "string") return null;
|
|
52
|
+
switch (raw.trim()) {
|
|
53
|
+
case "reset":
|
|
54
|
+
case "nothing_to_reset":
|
|
55
|
+
case "no_credit":
|
|
56
|
+
case "already_redeemed": return raw.trim();
|
|
57
|
+
default: return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function generateRedeemRequestId(nowMs) {
|
|
61
|
+
const rand = Math.random().toString(16).slice(2, 14).padEnd(12, "0");
|
|
62
|
+
return `redeem-${nowMs.toString(16)}-${rand}`;
|
|
63
|
+
}
|
|
50
64
|
//#endregion
|
|
51
65
|
//#region src/client/quota.ts
|
|
52
66
|
const RING_CIRCUMFERENCE = 2 * Math.PI * 5.5;
|
|
@@ -168,15 +182,46 @@ window.__ModuleLoader__.load({
|
|
|
168
182
|
const remHours = totalHours % 24;
|
|
169
183
|
return fmt.days(days, remHours);
|
|
170
184
|
}
|
|
185
|
+
const CREDIT_EXPIRY_FORMATTERS = {
|
|
186
|
+
zh: {
|
|
187
|
+
never: () => "永久有效",
|
|
188
|
+
expired: () => "已过期",
|
|
189
|
+
minutes: (m, dateStr) => `${m} 分钟后过期 (${dateStr})`,
|
|
190
|
+
hours: (h, dateStr) => `${h} 小时后过期 (${dateStr})`,
|
|
191
|
+
days: (d, dateStr) => `${d} 天后过期 (${dateStr})`
|
|
192
|
+
},
|
|
193
|
+
en: {
|
|
194
|
+
never: () => "Never expires",
|
|
195
|
+
expired: () => "Expired",
|
|
196
|
+
minutes: (m, dateStr) => `Expires in ${m}m (${dateStr})`,
|
|
197
|
+
hours: (h, dateStr) => `Expires in ${h}h (${dateStr})`,
|
|
198
|
+
days: (d, dateStr) => `Expires in ${d}d (${dateStr})`
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
function formatResetCreditExpiry(expiresAtSec, nowMs, locale) {
|
|
202
|
+
const fmt = CREDIT_EXPIRY_FORMATTERS[locale];
|
|
203
|
+
if (expiresAtSec === null || expiresAtSec === void 0) return fmt.never();
|
|
204
|
+
if (typeof expiresAtSec !== "number" || !Number.isFinite(expiresAtSec) || expiresAtSec <= 0) return "";
|
|
205
|
+
const diffSec = Math.round(expiresAtSec - nowMs / 1e3);
|
|
206
|
+
if (diffSec <= 0) return fmt.expired();
|
|
207
|
+
const expDate = new Date(expiresAtSec * 1e3);
|
|
208
|
+
const dateStr = `${padTwo(expDate.getMonth() + 1)}-${padTwo(expDate.getDate())}`;
|
|
209
|
+
if (diffSec < 3600) return fmt.minutes(Math.max(1, Math.ceil(diffSec / 60)), dateStr);
|
|
210
|
+
if (diffSec < 86400) return fmt.hours(Math.max(1, Math.ceil(diffSec / 3600)), dateStr);
|
|
211
|
+
return fmt.days(Math.max(1, Math.ceil(diffSec / 86400)), dateStr);
|
|
212
|
+
}
|
|
171
213
|
function createQuotaStore(options = {}) {
|
|
172
|
-
const
|
|
214
|
+
const getFetch = () => options.fetchImpl ?? globalThis.fetch;
|
|
173
215
|
const nowFn = options.now ?? Date.now;
|
|
174
|
-
const
|
|
216
|
+
const explicitTtlMs = options.ttlMs;
|
|
175
217
|
let state = {
|
|
176
218
|
quota: null,
|
|
177
219
|
defaultFastMode: false,
|
|
178
220
|
sessionFastModes: {},
|
|
179
221
|
sessionParents: {},
|
|
222
|
+
redeeming: false,
|
|
223
|
+
redeemingCreditId: null,
|
|
224
|
+
redeemNoticeOutcome: null,
|
|
180
225
|
loading: false,
|
|
181
226
|
error: null
|
|
182
227
|
};
|
|
@@ -186,9 +231,11 @@ window.__ModuleLoader__.load({
|
|
|
186
231
|
const pendingBySeq = new Map();
|
|
187
232
|
let inflight = null;
|
|
188
233
|
let inflightForce = false;
|
|
234
|
+
let redeemingInflight = null;
|
|
189
235
|
let requestSeq = 0;
|
|
190
236
|
let fastModeSeq = 0;
|
|
191
237
|
let fastMutationEpoch = 0;
|
|
238
|
+
let redeemSeq = 0;
|
|
192
239
|
const listeners = new Set();
|
|
193
240
|
function setState(next) {
|
|
194
241
|
state = next;
|
|
@@ -272,22 +319,27 @@ window.__ModuleLoader__.load({
|
|
|
272
319
|
sessionParents: knownSessionParents
|
|
273
320
|
};
|
|
274
321
|
}
|
|
322
|
+
function extractPayloadError(payload, fallback) {
|
|
323
|
+
return typeof payload?.error === "string" && payload.error !== "" ? payload.error : fallback;
|
|
324
|
+
}
|
|
275
325
|
async function refresh(force = false) {
|
|
276
|
-
if (!force && state.quota !== null && nowFn() - state.quota.fetchedAt <
|
|
326
|
+
if (!force && explicitTtlMs !== void 0 && state.quota !== null && nowFn() - state.quota.fetchedAt < explicitTtlMs) return;
|
|
277
327
|
if (inflight !== null && (!force || inflightForce)) return inflight;
|
|
278
328
|
const seq = ++requestSeq;
|
|
279
329
|
const canReplaceFastAtStart = pendingBySeq.size === 0;
|
|
280
330
|
const fastEpochAtStart = fastMutationEpoch;
|
|
281
331
|
inflightForce = force;
|
|
282
|
-
|
|
332
|
+
const showLoading = force || state.quota === null;
|
|
333
|
+
if (showLoading || state.error !== null || state.redeemNoticeOutcome !== null) setState({
|
|
283
334
|
...state,
|
|
284
|
-
loading: true,
|
|
335
|
+
loading: showLoading ? true : state.loading,
|
|
336
|
+
redeemNoticeOutcome: null,
|
|
285
337
|
error: null
|
|
286
338
|
});
|
|
287
339
|
const url = force ? `${USAGE_ROUTE_PATH}?force=1` : USAGE_ROUTE_PATH;
|
|
288
340
|
const task = (async () => {
|
|
289
341
|
try {
|
|
290
|
-
const response = await
|
|
342
|
+
const response = await getFetch()(url, {
|
|
291
343
|
method: "GET",
|
|
292
344
|
headers: { Accept: "application/json" }
|
|
293
345
|
});
|
|
@@ -296,6 +348,7 @@ window.__ModuleLoader__.load({
|
|
|
296
348
|
setState({
|
|
297
349
|
...state,
|
|
298
350
|
loading: false,
|
|
351
|
+
redeemNoticeOutcome: null,
|
|
299
352
|
error: `HTTP ${response.status}`
|
|
300
353
|
});
|
|
301
354
|
return;
|
|
@@ -304,18 +357,22 @@ window.__ModuleLoader__.load({
|
|
|
304
357
|
if (seq !== requestSeq) return;
|
|
305
358
|
const fastState = mergeFastStateFromPayload(payload, canReplaceFastAtStart && pendingBySeq.size === 0 && fastEpochAtStart === fastMutationEpoch);
|
|
306
359
|
if (payload && payload.ok === true && payload.quota) setState({
|
|
360
|
+
...state,
|
|
307
361
|
quota: payload.quota,
|
|
308
362
|
...fastState,
|
|
309
363
|
loading: false,
|
|
364
|
+
redeemNoticeOutcome: null,
|
|
310
365
|
error: null
|
|
311
366
|
});
|
|
312
367
|
else {
|
|
313
368
|
const fallbackQuota = payload && !payload.ok && payload.quota ? payload.quota : state.quota;
|
|
314
|
-
const errMsg = payload && !payload.ok
|
|
369
|
+
const errMsg = extractPayloadError(payload && !payload.ok ? payload : null, "Invalid response");
|
|
315
370
|
setState({
|
|
371
|
+
...state,
|
|
316
372
|
quota: fallbackQuota ?? null,
|
|
317
373
|
...fastState,
|
|
318
374
|
loading: false,
|
|
375
|
+
redeemNoticeOutcome: null,
|
|
319
376
|
error: errMsg
|
|
320
377
|
});
|
|
321
378
|
}
|
|
@@ -324,6 +381,7 @@ window.__ModuleLoader__.load({
|
|
|
324
381
|
setState({
|
|
325
382
|
...state,
|
|
326
383
|
loading: false,
|
|
384
|
+
redeemNoticeOutcome: null,
|
|
327
385
|
error: safeErrorMessage(error)
|
|
328
386
|
});
|
|
329
387
|
} finally {
|
|
@@ -361,6 +419,7 @@ window.__ModuleLoader__.load({
|
|
|
361
419
|
...state,
|
|
362
420
|
sessionFastModes: deriveSessionFastModes(),
|
|
363
421
|
sessionParents: knownSessionParents,
|
|
422
|
+
redeemNoticeOutcome: null,
|
|
364
423
|
error: errorMsg
|
|
365
424
|
});
|
|
366
425
|
};
|
|
@@ -368,10 +427,11 @@ window.__ModuleLoader__.load({
|
|
|
368
427
|
...state,
|
|
369
428
|
sessionFastModes: deriveSessionFastModes(),
|
|
370
429
|
sessionParents: knownSessionParents,
|
|
430
|
+
redeemNoticeOutcome: null,
|
|
371
431
|
error: null
|
|
372
432
|
});
|
|
373
433
|
try {
|
|
374
|
-
const response = await
|
|
434
|
+
const response = await getFetch()(USAGE_ROUTE_PATH, {
|
|
375
435
|
method: "POST",
|
|
376
436
|
headers: {
|
|
377
437
|
"Content-Type": "application/json",
|
|
@@ -385,7 +445,7 @@ window.__ModuleLoader__.load({
|
|
|
385
445
|
if (!response.ok) {
|
|
386
446
|
const errPayload = await response.json().catch(() => null);
|
|
387
447
|
mergeServerSessionParents(errPayload?.sessionParents);
|
|
388
|
-
rollback(
|
|
448
|
+
rollback(extractPayloadError(errPayload, `HTTP ${response.status}`));
|
|
389
449
|
return;
|
|
390
450
|
}
|
|
391
451
|
const payload = await response.json();
|
|
@@ -395,7 +455,7 @@ window.__ModuleLoader__.load({
|
|
|
395
455
|
return;
|
|
396
456
|
}
|
|
397
457
|
if (!payload || payload.ok === false) {
|
|
398
|
-
rollback(
|
|
458
|
+
rollback(extractPayloadError(payload, "Invalid response"));
|
|
399
459
|
return;
|
|
400
460
|
}
|
|
401
461
|
fastMutationEpoch += 1;
|
|
@@ -421,6 +481,7 @@ window.__ModuleLoader__.load({
|
|
|
421
481
|
defaultFastMode,
|
|
422
482
|
sessionFastModes: deriveSessionFastModes(),
|
|
423
483
|
sessionParents: knownSessionParents,
|
|
484
|
+
redeemNoticeOutcome: null,
|
|
424
485
|
error: null
|
|
425
486
|
});
|
|
426
487
|
} catch (error) {
|
|
@@ -429,6 +490,99 @@ window.__ModuleLoader__.load({
|
|
|
429
490
|
pendingBySeq.delete(seq);
|
|
430
491
|
}
|
|
431
492
|
}
|
|
493
|
+
async function consumeResetCredit(creditId) {
|
|
494
|
+
if (redeemingInflight !== null) return redeemingInflight;
|
|
495
|
+
const normalizedId = typeof creditId === "string" && creditId.trim() !== "" ? creditId.trim() : null;
|
|
496
|
+
redeemSeq += 1;
|
|
497
|
+
const redeemRequestId = generateRedeemRequestId(nowFn() + redeemSeq);
|
|
498
|
+
setState({
|
|
499
|
+
...state,
|
|
500
|
+
redeeming: true,
|
|
501
|
+
redeemingCreditId: normalizedId,
|
|
502
|
+
redeemNoticeOutcome: null,
|
|
503
|
+
error: null
|
|
504
|
+
});
|
|
505
|
+
const task = (async () => {
|
|
506
|
+
try {
|
|
507
|
+
const response = await getFetch()(USAGE_ROUTE_PATH, {
|
|
508
|
+
method: "POST",
|
|
509
|
+
headers: {
|
|
510
|
+
"Content-Type": "application/json",
|
|
511
|
+
Accept: "application/json"
|
|
512
|
+
},
|
|
513
|
+
body: JSON.stringify({
|
|
514
|
+
action: "consumeResetCredit",
|
|
515
|
+
...normalizedId !== null ? { creditId: normalizedId } : {},
|
|
516
|
+
redeemRequestId
|
|
517
|
+
})
|
|
518
|
+
});
|
|
519
|
+
if (!response.ok) {
|
|
520
|
+
const errMsg = extractPayloadError(await response.json().catch(() => null), `HTTP ${response.status}`);
|
|
521
|
+
setState({
|
|
522
|
+
...state,
|
|
523
|
+
redeeming: false,
|
|
524
|
+
redeemingCreditId: null,
|
|
525
|
+
redeemNoticeOutcome: null,
|
|
526
|
+
error: errMsg
|
|
527
|
+
});
|
|
528
|
+
return {
|
|
529
|
+
ok: false,
|
|
530
|
+
error: errMsg
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
const payload = await response.json();
|
|
534
|
+
const fastState = mergeFastStateFromPayload(payload, pendingBySeq.size === 0);
|
|
535
|
+
if (!payload || payload.ok !== true) {
|
|
536
|
+
const errMsg = extractPayloadError(payload, "Invalid response");
|
|
537
|
+
setState({
|
|
538
|
+
...state,
|
|
539
|
+
quota: payload?.quota ?? state.quota,
|
|
540
|
+
...fastState,
|
|
541
|
+
redeeming: false,
|
|
542
|
+
redeemingCreditId: null,
|
|
543
|
+
redeemNoticeOutcome: null,
|
|
544
|
+
error: errMsg
|
|
545
|
+
});
|
|
546
|
+
return {
|
|
547
|
+
ok: false,
|
|
548
|
+
error: errMsg
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
const outcome = normalizeConsumeOutcome(payload.outcome) ?? "reset";
|
|
552
|
+
setState({
|
|
553
|
+
...state,
|
|
554
|
+
quota: payload.quota ?? state.quota,
|
|
555
|
+
...fastState,
|
|
556
|
+
redeeming: false,
|
|
557
|
+
redeemingCreditId: null,
|
|
558
|
+
redeemNoticeOutcome: outcome === "reset" ? null : outcome,
|
|
559
|
+
error: null
|
|
560
|
+
});
|
|
561
|
+
return {
|
|
562
|
+
ok: true,
|
|
563
|
+
outcome,
|
|
564
|
+
...payload.quota !== void 0 ? { quota: payload.quota } : {}
|
|
565
|
+
};
|
|
566
|
+
} catch (error) {
|
|
567
|
+
const errMsg = safeErrorMessage(error);
|
|
568
|
+
setState({
|
|
569
|
+
...state,
|
|
570
|
+
redeeming: false,
|
|
571
|
+
redeemingCreditId: null,
|
|
572
|
+
redeemNoticeOutcome: null,
|
|
573
|
+
error: errMsg
|
|
574
|
+
});
|
|
575
|
+
return {
|
|
576
|
+
ok: false,
|
|
577
|
+
error: errMsg
|
|
578
|
+
};
|
|
579
|
+
} finally {
|
|
580
|
+
redeemingInflight = null;
|
|
581
|
+
}
|
|
582
|
+
})();
|
|
583
|
+
redeemingInflight = task;
|
|
584
|
+
return task;
|
|
585
|
+
}
|
|
432
586
|
return {
|
|
433
587
|
getSnapshot: () => state,
|
|
434
588
|
subscribe(listener) {
|
|
@@ -438,7 +592,8 @@ window.__ModuleLoader__.load({
|
|
|
438
592
|
};
|
|
439
593
|
},
|
|
440
594
|
refresh,
|
|
441
|
-
setFastMode
|
|
595
|
+
setFastMode,
|
|
596
|
+
consumeResetCredit
|
|
442
597
|
};
|
|
443
598
|
}
|
|
444
599
|
//#endregion
|
|
@@ -480,6 +635,26 @@ window.__ModuleLoader__.load({
|
|
|
480
635
|
".dsh-codex-fast-switch[data-checked=\"true\"]{background:var(--dsw-alias-brand-primary,#3b82f6)}",
|
|
481
636
|
".dsh-codex-fast-switch-thumb{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,0.2);transition:transform .15s ease}",
|
|
482
637
|
".dsh-codex-fast-switch[data-checked=\"true\"] .dsh-codex-fast-switch-thumb{transform:translateX(14px)}",
|
|
638
|
+
".dsh-codex-reset-section{margin-top:8px;padding-top:8px;border-top:1px solid var(--dsw-alias-border-l1)}",
|
|
639
|
+
".dsh-codex-reset-row{display:flex;align-items:center;justify-content:space-between;gap:10px}",
|
|
640
|
+
".dsh-codex-reset-icon{color:var(--dsw-alias-brand-primary,#3b82f6);flex:none}",
|
|
641
|
+
".dsh-codex-reset-pill{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:999px;border:none;font-family:inherit;font-size:11px;font-weight:600;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-brand-primary);white-space:nowrap;font-variant-numeric:tabular-nums;cursor:pointer;flex:none;transition:background-color .12s,color .12s}",
|
|
642
|
+
".dsh-codex-reset-pill[data-zero=\"true\"]{color:var(--dsw-alias-label-tertiary);font-weight:500;cursor:default}",
|
|
643
|
+
".dsh-codex-reset-list{margin-top:6px;display:flex;flex-direction:column;gap:5px}",
|
|
644
|
+
".dsh-codex-reset-card{padding:6px 8px;border-radius:6px;background:var(--dsw-alias-interactive-bg-hover);border:1px dashed var(--dsw-alias-border-l2);display:flex;flex-direction:column;gap:5px}",
|
|
645
|
+
".dsh-codex-reset-item{display:flex;align-items:center;justify-content:space-between;gap:8px}",
|
|
646
|
+
".dsh-codex-reset-item-info{display:flex;flex-direction:column;gap:1px;min-width:0}",
|
|
647
|
+
".dsh-codex-reset-item-title{font-size:11.5px;font-weight:500;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
648
|
+
".dsh-codex-reset-item-expiry{font-size:10.5px;line-height:14px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
|
|
649
|
+
".dsh-codex-reset-use-btn{border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-brand-primary);font-family:inherit;font-size:11px;font-weight:600;padding:2px 8px;border-radius:5px;cursor:pointer;white-space:nowrap;flex:none;transition:background-color .12s,color .12s,border-color .12s}",
|
|
650
|
+
".dsh-codex-reset-use-btn:hover:not(:disabled){background:var(--dsw-alias-brand-primary,#3b82f6);border-color:var(--dsw-alias-brand-primary,#3b82f6);color:#fff}",
|
|
651
|
+
".dsh-codex-reset-use-btn:disabled{opacity:0.45;cursor:default;color:var(--dsw-alias-label-tertiary)}",
|
|
652
|
+
".dsh-codex-reset-confirm{padding-top:5px;border-top:1px dashed var(--dsw-alias-border-l1);display:flex;align-items:center;justify-content:space-between;gap:6px}",
|
|
653
|
+
".dsh-codex-reset-confirm-text{font-size:11px;line-height:14px;color:var(--dsw-alias-state-warn-primary)}",
|
|
654
|
+
".dsh-codex-reset-confirm-actions{display:inline-flex;align-items:center;gap:4px;flex:none}",
|
|
655
|
+
".dsh-codex-reset-confirm-cancel,.dsh-codex-reset-confirm-submit{border:none;font-family:inherit;font-size:11px;font-weight:600;padding:2px 7px;border-radius:4px;cursor:pointer;white-space:nowrap}",
|
|
656
|
+
".dsh-codex-reset-confirm-cancel{background:transparent;color:var(--dsw-alias-label-secondary)}",
|
|
657
|
+
".dsh-codex-reset-confirm-submit{background:var(--dsw-alias-state-warn-primary,#f59e0b);color:#111}",
|
|
483
658
|
".dsh-codex-quota-error{margin-top:6px;padding:4px 6px;border-radius:4px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-state-error-primary);font-size:11px;word-break:break-word}"
|
|
484
659
|
].join("");
|
|
485
660
|
//#endregion
|
|
@@ -489,6 +664,8 @@ window.__ModuleLoader__.load({
|
|
|
489
664
|
const NS = "codex-subscription";
|
|
490
665
|
const SLOT_NAME = "conversation.input.right";
|
|
491
666
|
const SLOT_ENTRY_ID = "codex-subscription:quota";
|
|
667
|
+
const ACTIVE_QUOTA_SYNC_INTERVAL_MS = 5e3;
|
|
668
|
+
const IDLE_QUOTA_SYNC_INTERVAL_MS = 15e3;
|
|
492
669
|
const zh = {
|
|
493
670
|
"quota.title": "Codex 订阅限额",
|
|
494
671
|
"quota.loading": "正在获取 Codex 限额…",
|
|
@@ -497,7 +674,22 @@ window.__ModuleLoader__.load({
|
|
|
497
674
|
"quota.usedPrefix": "已用 ",
|
|
498
675
|
"quota.remainPrefix": "剩余 ",
|
|
499
676
|
"quota.fastTitle": "Fast 极速模式",
|
|
500
|
-
"quota.fastDesc": "2x 推理速度,消耗更多限额"
|
|
677
|
+
"quota.fastDesc": "2x 推理速度,消耗更多限额",
|
|
678
|
+
"quota.resetTitle": "限额重置卡",
|
|
679
|
+
"quota.resetZero": "0 张可用",
|
|
680
|
+
"quota.resetZeroDesc": "暂无可用重置卡",
|
|
681
|
+
"quota.resetAvailableSuffix": " 张可用",
|
|
682
|
+
"quota.resetNextExpiryPrefix": "下次到期:",
|
|
683
|
+
"quota.resetReadyDesc": "可重置当前周期已用限额",
|
|
684
|
+
"quota.resetItemDefaultTitle": "重置卡",
|
|
685
|
+
"quota.resetUseBtn": "使用",
|
|
686
|
+
"quota.resetUsingBtn": "核销中…",
|
|
687
|
+
"quota.resetConfirmPrompt": "确认使用此卡重置当前已用限额?",
|
|
688
|
+
"quota.resetConfirmYes": "确认使用",
|
|
689
|
+
"quota.resetConfirmNo": "取消",
|
|
690
|
+
"quota.resetNothingToReset": "当前无需重置限额",
|
|
691
|
+
"quota.resetNoCredit": "没有可用的重置卡",
|
|
692
|
+
"quota.resetAlreadyRedeemed": "该重置卡已被使用"
|
|
501
693
|
};
|
|
502
694
|
const en = {
|
|
503
695
|
"quota.title": "Codex Subscription Quota",
|
|
@@ -507,7 +699,22 @@ window.__ModuleLoader__.load({
|
|
|
507
699
|
"quota.usedPrefix": "Used ",
|
|
508
700
|
"quota.remainPrefix": "Remaining ",
|
|
509
701
|
"quota.fastTitle": "Fast Mode",
|
|
510
|
-
"quota.fastDesc": "2x speed, increased usage"
|
|
702
|
+
"quota.fastDesc": "2x speed, increased usage",
|
|
703
|
+
"quota.resetTitle": "Reset Credits",
|
|
704
|
+
"quota.resetZero": "0 available",
|
|
705
|
+
"quota.resetZeroDesc": "No reset credits available",
|
|
706
|
+
"quota.resetAvailableSuffix": " available",
|
|
707
|
+
"quota.resetNextExpiryPrefix": "Next expiry: ",
|
|
708
|
+
"quota.resetReadyDesc": "Reset current window usage",
|
|
709
|
+
"quota.resetItemDefaultTitle": "Reset Credit",
|
|
710
|
+
"quota.resetUseBtn": "Use",
|
|
711
|
+
"quota.resetUsingBtn": "Using…",
|
|
712
|
+
"quota.resetConfirmPrompt": "Use this credit to reset current usage?",
|
|
713
|
+
"quota.resetConfirmYes": "Confirm",
|
|
714
|
+
"quota.resetConfirmNo": "Cancel",
|
|
715
|
+
"quota.resetNothingToReset": "Your usage does not need a reset right now",
|
|
716
|
+
"quota.resetNoCredit": "No reset credits available",
|
|
717
|
+
"quota.resetAlreadyRedeemed": "This reset credit was already redeemed"
|
|
511
718
|
};
|
|
512
719
|
const EMPTY_MODEL_STORE = {
|
|
513
720
|
getSnapshot: () => null,
|
|
@@ -525,6 +732,25 @@ window.__ModuleLoader__.load({
|
|
|
525
732
|
fill: "currentColor"
|
|
526
733
|
}));
|
|
527
734
|
}
|
|
735
|
+
function renderTicketIcon(size) {
|
|
736
|
+
return h("svg", {
|
|
737
|
+
className: "dsh-codex-reset-icon",
|
|
738
|
+
viewBox: "0 0 16 16",
|
|
739
|
+
width: size,
|
|
740
|
+
height: size,
|
|
741
|
+
fill: "none",
|
|
742
|
+
stroke: "currentColor",
|
|
743
|
+
strokeWidth: 1.5,
|
|
744
|
+
strokeLinecap: "round",
|
|
745
|
+
strokeLinejoin: "round",
|
|
746
|
+
"aria-hidden": true
|
|
747
|
+
}, h("path", { d: "M2 5a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1.6a1.4 1.4 0 0 0 0 2.8V11a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V9.4a1.4 1.4 0 0 0 0-2.8V5z" }), h("path", { d: "M6.5 4v8" }));
|
|
748
|
+
}
|
|
749
|
+
const REDEEM_OUTCOME_LOCALE_KEYS = {
|
|
750
|
+
nothing_to_reset: "quota.resetNothingToReset",
|
|
751
|
+
no_credit: "quota.resetNoCredit",
|
|
752
|
+
already_redeemed: "quota.resetAlreadyRedeemed"
|
|
753
|
+
};
|
|
528
754
|
function CodexQuotaPill(props) {
|
|
529
755
|
const { sessionId, quotaStore, getModelStore, useProjection, useSession, t, getLocale } = props;
|
|
530
756
|
const modelStore = getModelStore(sessionId);
|
|
@@ -536,6 +762,8 @@ window.__ModuleLoader__.load({
|
|
|
536
762
|
const isCodex = isCodexProvider(resolveActiveProvider(directorySnap, projectedSelection));
|
|
537
763
|
const quotaSnap = React.useSyncExternalStore(quotaStore.subscribe, quotaStore.getSnapshot);
|
|
538
764
|
const [open, setOpen] = React.useState(false);
|
|
765
|
+
const [detailsOpen, setDetailsOpen] = React.useState(true);
|
|
766
|
+
const [confirmingCreditKey, setConfirmingCreditKey] = React.useState(null);
|
|
539
767
|
const rootRef = React.useRef(null);
|
|
540
768
|
const prevRunningRef = React.useRef(running);
|
|
541
769
|
const prevSessionIdRef = React.useRef(sessionId);
|
|
@@ -547,8 +775,8 @@ window.__ModuleLoader__.load({
|
|
|
547
775
|
prevRunningRef.current = running;
|
|
548
776
|
return;
|
|
549
777
|
}
|
|
550
|
-
quotaStore.refresh(false);
|
|
551
778
|
if (sameSession && didTurnFinish(prevRunningRef.current, running)) quotaStore.refresh(true);
|
|
779
|
+
else if (!sameSession || !running || quotaStore.getSnapshot().quota === null) quotaStore.refresh(false);
|
|
552
780
|
prevRunningRef.current = running;
|
|
553
781
|
}, [
|
|
554
782
|
isCodex,
|
|
@@ -556,6 +784,20 @@ window.__ModuleLoader__.load({
|
|
|
556
784
|
sessionId,
|
|
557
785
|
quotaStore
|
|
558
786
|
]);
|
|
787
|
+
React.useEffect(() => {
|
|
788
|
+
if (!isCodex || typeof globalThis.setInterval !== "function") return;
|
|
789
|
+
const intervalMs = running ? ACTIVE_QUOTA_SYNC_INTERVAL_MS : IDLE_QUOTA_SYNC_INTERVAL_MS;
|
|
790
|
+
const timer = globalThis.setInterval(() => {
|
|
791
|
+
quotaStore.refresh(false);
|
|
792
|
+
}, intervalMs);
|
|
793
|
+
return () => {
|
|
794
|
+
globalThis.clearInterval(timer);
|
|
795
|
+
};
|
|
796
|
+
}, [
|
|
797
|
+
isCodex,
|
|
798
|
+
running,
|
|
799
|
+
quotaStore
|
|
800
|
+
]);
|
|
559
801
|
React.useEffect(() => {
|
|
560
802
|
if (!open || !isCodex || typeof globalThis.document === "undefined") return;
|
|
561
803
|
const onPointerDown = (event) => {
|
|
@@ -595,6 +837,71 @@ window.__ModuleLoader__.load({
|
|
|
595
837
|
const isFastMode = resolveSessionFastMode(quotaSnap, sessionId, parentSessionId);
|
|
596
838
|
const secondaryReached = Boolean(quota?.secondary && quota.secondary.usedPercent >= 100);
|
|
597
839
|
const primaryReached = Boolean(quota?.limitReached && !secondaryReached);
|
|
840
|
+
const resetCredits = quota?.resetCredits;
|
|
841
|
+
const availableResetCount = resetCredits?.availableCount ?? 0;
|
|
842
|
+
const applicableResetCount = resetCredits?.applicableAvailableCount ?? availableResetCount;
|
|
843
|
+
const nowSec = Math.floor(nowMs / 1e3);
|
|
844
|
+
const displayCredits = availableResetCount > 0 ? resetCredits?.credits && resetCredits.credits.length > 0 ? resetCredits.credits.map((item) => ({
|
|
845
|
+
id: item.id,
|
|
846
|
+
slotKey: item.id,
|
|
847
|
+
title: item.title,
|
|
848
|
+
expiresAt: item.expiresAt
|
|
849
|
+
})) : Array.from({ length: availableResetCount }, (_unused, idx) => ({
|
|
850
|
+
id: null,
|
|
851
|
+
slotKey: `placeholder-${idx}`,
|
|
852
|
+
title: null,
|
|
853
|
+
expiresAt: null
|
|
854
|
+
})) : [];
|
|
855
|
+
const renderResetCreditsSection = () => {
|
|
856
|
+
const firstCard = displayCredits[0];
|
|
857
|
+
const subDesc = availableResetCount === 0 ? t("quota.resetZeroDesc") : firstCard !== void 0 && firstCard.expiresAt !== null ? `${t("quota.resetNextExpiryPrefix")}${formatResetCreditExpiry(firstCard.expiresAt, nowMs, locale)}` : t("quota.resetReadyDesc");
|
|
858
|
+
return h("div", { className: "dsh-codex-reset-section" }, h("div", { className: "dsh-codex-reset-row" }, h("div", { className: "dsh-codex-fast-info" }, h("div", { className: "dsh-codex-fast-title" }, renderTicketIcon(12), h("span", null, t("quota.resetTitle"))), h("div", { className: "dsh-codex-fast-desc" }, subDesc)), availableResetCount === 0 ? h("span", {
|
|
859
|
+
className: "dsh-codex-reset-pill",
|
|
860
|
+
"data-zero": "true"
|
|
861
|
+
}, t("quota.resetZero")) : h("button", {
|
|
862
|
+
type: "button",
|
|
863
|
+
className: "dsh-codex-reset-pill",
|
|
864
|
+
"data-zero": "false",
|
|
865
|
+
"aria-expanded": detailsOpen,
|
|
866
|
+
onClick: () => {
|
|
867
|
+
setDetailsOpen(!detailsOpen);
|
|
868
|
+
setConfirmingCreditKey(null);
|
|
869
|
+
}
|
|
870
|
+
}, `${availableResetCount}${t("quota.resetAvailableSuffix")} ${detailsOpen ? "▴" : "▾"}`)), availableResetCount > 0 && detailsOpen ? h("div", { className: "dsh-codex-reset-list" }, ...displayCredits.map((item, idx) => {
|
|
871
|
+
const itemKey = item.slotKey;
|
|
872
|
+
const itemTitle = item.title ?? `${t("quota.resetItemDefaultTitle")} #${idx + 1}`;
|
|
873
|
+
const itemExpiry = formatResetCreditExpiry(item.expiresAt, nowMs, locale);
|
|
874
|
+
const isItemExpired = item.expiresAt !== null && item.expiresAt <= nowSec;
|
|
875
|
+
const isRedeemingAny = quotaSnap.redeeming === true;
|
|
876
|
+
const isRedeemingThis = isRedeemingAny && (item.id !== null ? quotaSnap.redeemingCreditId === item.id : quotaSnap.redeemingCreditId === null);
|
|
877
|
+
const isConfirming = confirmingCreditKey === itemKey;
|
|
878
|
+
return h("div", {
|
|
879
|
+
key: itemKey,
|
|
880
|
+
className: "dsh-codex-reset-card"
|
|
881
|
+
}, h("div", { className: "dsh-codex-reset-item" }, h("div", { className: "dsh-codex-reset-item-info" }, h("div", { className: "dsh-codex-reset-item-title" }, itemTitle), h("div", { className: "dsh-codex-reset-item-expiry" }, itemExpiry)), h("button", {
|
|
882
|
+
type: "button",
|
|
883
|
+
className: "dsh-codex-reset-use-btn",
|
|
884
|
+
disabled: applicableResetCount <= 0 || isRedeemingAny || isItemExpired,
|
|
885
|
+
onClick: () => {
|
|
886
|
+
setConfirmingCreditKey(isConfirming ? null : itemKey);
|
|
887
|
+
}
|
|
888
|
+
}, isRedeemingThis ? t("quota.resetUsingBtn") : t("quota.resetUseBtn"))), isConfirming ? h("div", { className: "dsh-codex-reset-confirm" }, h("span", { className: "dsh-codex-reset-confirm-text" }, t("quota.resetConfirmPrompt")), h("div", { className: "dsh-codex-reset-confirm-actions" }, h("button", {
|
|
889
|
+
type: "button",
|
|
890
|
+
className: "dsh-codex-reset-confirm-cancel",
|
|
891
|
+
onClick: () => {
|
|
892
|
+
setConfirmingCreditKey(null);
|
|
893
|
+
}
|
|
894
|
+
}, t("quota.resetConfirmNo")), h("button", {
|
|
895
|
+
type: "button",
|
|
896
|
+
className: "dsh-codex-reset-confirm-submit",
|
|
897
|
+
disabled: isRedeemingAny || isItemExpired,
|
|
898
|
+
onClick: async () => {
|
|
899
|
+
setConfirmingCreditKey(null);
|
|
900
|
+
await quotaStore.consumeResetCredit(item.id ?? void 0);
|
|
901
|
+
}
|
|
902
|
+
}, t("quota.resetConfirmYes")))) : null);
|
|
903
|
+
})) : null);
|
|
904
|
+
};
|
|
598
905
|
return h("span", {
|
|
599
906
|
ref: rootRef,
|
|
600
907
|
className: "dsh-codex-quota-root",
|
|
@@ -609,9 +916,8 @@ window.__ModuleLoader__.load({
|
|
|
609
916
|
"aria-expanded": open,
|
|
610
917
|
title: `${t("quota.title")} (${pillText})`,
|
|
611
918
|
onClick: () => {
|
|
612
|
-
|
|
613
|
-
setOpen(
|
|
614
|
-
if (nextOpen) quotaStore.refresh(false);
|
|
919
|
+
setConfirmingCreditKey(null);
|
|
920
|
+
setOpen(!open);
|
|
615
921
|
}
|
|
616
922
|
}, h("svg", {
|
|
617
923
|
viewBox: "0 0 14 14",
|
|
@@ -644,7 +950,7 @@ window.__ModuleLoader__.load({
|
|
|
644
950
|
onClick: () => {
|
|
645
951
|
quotaStore.refresh(true);
|
|
646
952
|
}
|
|
647
|
-
}, quotaSnap.loading ? t("quota.refreshing") : t("quota.refresh"))), quota !== null ? renderWindowSection(formatWindowTitle(quota.primary.windowSeconds, locale, DEFAULT_PRIMARY_WINDOW_SECONDS), quota.primary, primaryReached) : quotaSnap.loading || quotaSnap.error === null ? h("div", { className: "dsh-codex-quota-meta" }, t("quota.loading")) : null, quota?.secondary !== null && quota?.secondary !== void 0 ? renderWindowSection(formatWindowTitle(quota.secondary.windowSeconds, locale, DEFAULT_SECONDARY_WINDOW_SECONDS), quota.secondary, secondaryReached) : null, h("div", { className: "dsh-codex-fast-row" }, h("div", { className: "dsh-codex-fast-info" }, h("div", { className: "dsh-codex-fast-title" }, renderFastBolt(12), h("span", null, t("quota.fastTitle"))), h("div", { className: "dsh-codex-fast-desc" }, t("quota.fastDesc"))), h("button", {
|
|
953
|
+
}, quotaSnap.loading ? t("quota.refreshing") : t("quota.refresh"))), quota !== null ? renderWindowSection(formatWindowTitle(quota.primary.windowSeconds, locale, DEFAULT_PRIMARY_WINDOW_SECONDS), quota.primary, primaryReached) : quotaSnap.loading || quotaSnap.error === null ? h("div", { className: "dsh-codex-quota-meta" }, t("quota.loading")) : null, quota?.secondary !== null && quota?.secondary !== void 0 ? renderWindowSection(formatWindowTitle(quota.secondary.windowSeconds, locale, DEFAULT_SECONDARY_WINDOW_SECONDS), quota.secondary, secondaryReached) : null, quota !== null ? renderResetCreditsSection() : null, h("div", { className: "dsh-codex-fast-row" }, h("div", { className: "dsh-codex-fast-info" }, h("div", { className: "dsh-codex-fast-title" }, renderFastBolt(12), h("span", null, t("quota.fastTitle"))), h("div", { className: "dsh-codex-fast-desc" }, t("quota.fastDesc"))), h("button", {
|
|
648
954
|
type: "button",
|
|
649
955
|
role: "switch",
|
|
650
956
|
className: "dsh-codex-fast-switch",
|
|
@@ -654,7 +960,7 @@ window.__ModuleLoader__.load({
|
|
|
654
960
|
onClick: () => {
|
|
655
961
|
quotaStore.setFastMode(sessionId, !isFastMode, parentSessionId);
|
|
656
962
|
}
|
|
657
|
-
}, h("span", { className: "dsh-codex-fast-switch-thumb" }))), quotaSnap.error !== null ? h("div", { className: "dsh-codex-quota-error" }, quotaSnap.error) : null) : null);
|
|
963
|
+
}, h("span", { className: "dsh-codex-fast-switch-thumb" }))), quotaSnap.error !== null || quotaSnap.redeemNoticeOutcome != null ? h("div", { className: "dsh-codex-quota-error" }, quotaSnap.error !== null ? quotaSnap.error : t(REDEEM_OUTCOME_LOCALE_KEYS[quotaSnap.redeemNoticeOutcome])) : null) : null);
|
|
658
964
|
}
|
|
659
965
|
const inject = ["slots", "locale"];
|
|
660
966
|
function apply(ctx) {
|
|
@@ -713,6 +1019,7 @@ window.__ModuleLoader__.load({
|
|
|
713
1019
|
formatWindowTitle,
|
|
714
1020
|
formatQuotaSummary,
|
|
715
1021
|
formatResetCountdown,
|
|
1022
|
+
formatResetCreditExpiry,
|
|
716
1023
|
createQuotaStore,
|
|
717
1024
|
CodexQuotaPill
|
|
718
1025
|
});
|
package/lib/index.d.ts
CHANGED
|
@@ -132,27 +132,51 @@ interface CodexQuotaWindow {
|
|
|
132
132
|
readonly windowSeconds?: number | null | undefined;
|
|
133
133
|
readonly resetAt: number | null;
|
|
134
134
|
}
|
|
135
|
+
interface CodexResetCreditItem {
|
|
136
|
+
readonly id: string;
|
|
137
|
+
readonly resetType: string;
|
|
138
|
+
readonly status: string;
|
|
139
|
+
readonly grantedAt: number | null;
|
|
140
|
+
readonly expiresAt: number | null;
|
|
141
|
+
readonly title: string | null;
|
|
142
|
+
readonly description: string | null;
|
|
143
|
+
}
|
|
144
|
+
interface CodexResetCreditsSnapshot {
|
|
145
|
+
readonly availableCount: number;
|
|
146
|
+
readonly applicableAvailableCount: number;
|
|
147
|
+
readonly credits: readonly CodexResetCreditItem[] | null;
|
|
148
|
+
}
|
|
135
149
|
interface CodexQuotaSnapshot {
|
|
136
150
|
readonly planType: string;
|
|
137
151
|
readonly limitReached: boolean;
|
|
138
152
|
readonly primary: CodexQuotaWindow;
|
|
139
153
|
readonly secondary: CodexQuotaWindow | null;
|
|
154
|
+
readonly resetCredits?: CodexResetCreditsSnapshot | undefined;
|
|
140
155
|
readonly fetchedAt: number;
|
|
141
156
|
}
|
|
142
|
-
type
|
|
143
|
-
|
|
144
|
-
readonly quota: CodexQuotaSnapshot;
|
|
157
|
+
type ConsumeResetCreditOutcome = 'reset' | 'nothing_to_reset' | 'no_credit' | 'already_redeemed';
|
|
158
|
+
interface CodexRouteStateProjection {
|
|
145
159
|
readonly defaultFastMode?: boolean | undefined;
|
|
146
160
|
readonly sessionFastModes?: Readonly<Record<string, boolean>> | undefined;
|
|
147
161
|
readonly sessionParents?: Readonly<Record<string, string>> | undefined;
|
|
148
|
-
}
|
|
162
|
+
}
|
|
163
|
+
type CodexConsumeResetCreditResponse = ({
|
|
164
|
+
readonly ok: true;
|
|
165
|
+
readonly outcome: ConsumeResetCreditOutcome;
|
|
166
|
+
readonly quota?: CodexQuotaSnapshot | undefined;
|
|
167
|
+
} & CodexRouteStateProjection) | ({
|
|
149
168
|
readonly ok: false;
|
|
150
169
|
readonly error: string;
|
|
151
170
|
readonly quota?: CodexQuotaSnapshot | undefined;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
readonly
|
|
155
|
-
|
|
171
|
+
} & CodexRouteStateProjection);
|
|
172
|
+
type CodexQuotaResponse = ({
|
|
173
|
+
readonly ok: true;
|
|
174
|
+
readonly quota: CodexQuotaSnapshot;
|
|
175
|
+
} & CodexRouteStateProjection) | ({
|
|
176
|
+
readonly ok: false;
|
|
177
|
+
readonly error: string;
|
|
178
|
+
readonly quota?: CodexQuotaSnapshot | undefined;
|
|
179
|
+
} & CodexRouteStateProjection);
|
|
156
180
|
interface UsageFetcherOptions {
|
|
157
181
|
readonly resolveCredentials: () => Promise<CodexResolvedCredentials>;
|
|
158
182
|
readonly buildHeaders: (authHeaders: Record<string, string>) => Record<string, string>;
|
|
@@ -160,11 +184,22 @@ interface UsageFetcherOptions {
|
|
|
160
184
|
readonly now?: (() => number) | undefined;
|
|
161
185
|
readonly ttlMs?: number | undefined;
|
|
162
186
|
readonly minForceIntervalMs?: number | undefined;
|
|
187
|
+
readonly autoRefresh?: boolean | undefined;
|
|
163
188
|
}
|
|
189
|
+
declare function parseResetCreditsPayload(raw: unknown, nowMs?: number | undefined): {
|
|
190
|
+
readonly availableCount: number;
|
|
191
|
+
readonly credits: readonly CodexResetCreditItem[];
|
|
192
|
+
} | null;
|
|
164
193
|
declare function parseUsagePayload(raw: unknown, fetchedAt: number): CodexQuotaSnapshot | null;
|
|
165
194
|
declare function createUsageFetcher(options: UsageFetcherOptions): {
|
|
166
195
|
getUsage: (force?: boolean) => Promise<CodexQuotaResponse>;
|
|
196
|
+
consumeResetCredit: (params?: {
|
|
197
|
+
readonly creditId?: string | undefined;
|
|
198
|
+
readonly redeemRequestId?: string | undefined;
|
|
199
|
+
}) => Promise<CodexConsumeResetCreditResponse>;
|
|
200
|
+
onRequestStart: () => void;
|
|
167
201
|
invalidate: () => void;
|
|
202
|
+
dispose: () => void;
|
|
168
203
|
};
|
|
169
204
|
//#endregion
|
|
170
205
|
//#region src/index.d.ts
|
|
@@ -199,7 +234,8 @@ export declare const __test: Readonly<{
|
|
|
199
234
|
normalizeCodexSessionId: typeof normalizeCodexSessionId;
|
|
200
235
|
toCodexWireSessionId: typeof toCodexWireSessionId;
|
|
201
236
|
parseUsagePayload: typeof parseUsagePayload;
|
|
237
|
+
parseResetCreditsPayload: typeof parseResetCreditsPayload;
|
|
202
238
|
createUsageFetcher: typeof createUsageFetcher;
|
|
203
239
|
}>;
|
|
204
240
|
//#endregion
|
|
205
|
-
export type { CodexModelMetadata, CodexQuotaResponse, CodexQuotaSnapshot, CodexQuotaWindow, CodexResolvedCredentials, ParsedModelsCatalog, PluginContextLike, PluginLogLevel, RawModelsCache };
|
|
241
|
+
export type { CodexConsumeResetCreditResponse, CodexModelMetadata, CodexQuotaResponse, CodexQuotaSnapshot, CodexQuotaWindow, CodexResetCreditItem, CodexResetCreditsSnapshot, CodexResolvedCredentials, ConsumeResetCreditOutcome, ParsedModelsCatalog, PluginContextLike, PluginLogLevel, RawModelsCache };
|
package/lib/index.js
CHANGED
|
@@ -361,6 +361,8 @@ function safeErrorMessage(error) {
|
|
|
361
361
|
return (error instanceof Error ? error.message : String(error)).replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted]").replace(/\b(Bearer\s+)[^\s,;'"&]+/giu, "$1[redacted]").replace(/(\b(?:code|token|access_token|refresh_token|id_token)=)[^&\s"']+/giu, "$1[redacted]").replace(/("(?:access_token|refresh_token|id_token)"\s*:\s*")[^"]+"/giu, "$1[redacted]\"").slice(0, 1e3);
|
|
362
362
|
}
|
|
363
363
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
364
|
+
const CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
|
|
365
|
+
const CODEX_RESET_CREDITS_CONSUME_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume";
|
|
364
366
|
const USAGE_ROUTE_PATH = "/api/codex-subscription/usage";
|
|
365
367
|
const DEFAULT_USAGE_TTL_MS = 6e4;
|
|
366
368
|
const USAGE_FETCH_TIMEOUT_MS = 15e3;
|
|
@@ -385,6 +387,77 @@ function parseWindow(raw, fetchedAtMs) {
|
|
|
385
387
|
resetAt
|
|
386
388
|
};
|
|
387
389
|
}
|
|
390
|
+
function parseTimestampSeconds(raw) {
|
|
391
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) return Math.round(raw);
|
|
392
|
+
if (typeof raw === "string") {
|
|
393
|
+
const trimmed = raw.trim();
|
|
394
|
+
if (trimmed === "") return null;
|
|
395
|
+
const parsedMs = Date.parse(trimmed);
|
|
396
|
+
if (Number.isFinite(parsedMs) && parsedMs > 0) return Math.floor(parsedMs / 1e3);
|
|
397
|
+
}
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
function nonEmptyString(raw) {
|
|
401
|
+
if (typeof raw !== "string") return null;
|
|
402
|
+
const trimmed = raw.trim();
|
|
403
|
+
return trimmed !== "" ? trimmed : null;
|
|
404
|
+
}
|
|
405
|
+
function parseResetCreditsPayload(raw, nowMs) {
|
|
406
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
407
|
+
const record = raw;
|
|
408
|
+
const rawList = Array.isArray(record.credits) ? record.credits : null;
|
|
409
|
+
if (rawList === null) return null;
|
|
410
|
+
const nowSec = typeof nowMs === "number" && Number.isFinite(nowMs) && nowMs > 0 ? Math.floor(nowMs / 1e3) : null;
|
|
411
|
+
const credits = [];
|
|
412
|
+
for (const entry of rawList) {
|
|
413
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
414
|
+
const item = entry;
|
|
415
|
+
const id = nonEmptyString(item.id);
|
|
416
|
+
if (id === null) continue;
|
|
417
|
+
const statusRaw = (nonEmptyString(item.status) ?? "available").toLowerCase();
|
|
418
|
+
if (statusRaw !== "available") continue;
|
|
419
|
+
const expiresAt = parseTimestampSeconds(item.expires_at);
|
|
420
|
+
if (nowSec !== null && expiresAt !== null && expiresAt <= nowSec) continue;
|
|
421
|
+
const resetType = nonEmptyString(item.reset_type) ?? "codex_rate_limits";
|
|
422
|
+
const grantedAt = parseTimestampSeconds(item.granted_at);
|
|
423
|
+
const title = nonEmptyString(item.title);
|
|
424
|
+
const description = nonEmptyString(item.description);
|
|
425
|
+
credits.push({
|
|
426
|
+
id,
|
|
427
|
+
resetType,
|
|
428
|
+
status: statusRaw,
|
|
429
|
+
grantedAt,
|
|
430
|
+
expiresAt,
|
|
431
|
+
title,
|
|
432
|
+
description
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
credits.sort((a, b) => {
|
|
436
|
+
if (a.expiresAt !== null && b.expiresAt !== null) {
|
|
437
|
+
if (a.expiresAt !== b.expiresAt) return a.expiresAt - b.expiresAt;
|
|
438
|
+
} else if (a.expiresAt !== null) return -1;
|
|
439
|
+
else if (b.expiresAt !== null) return 1;
|
|
440
|
+
return (a.grantedAt ?? 0) - (b.grantedAt ?? 0);
|
|
441
|
+
});
|
|
442
|
+
const availRaw = record.available_count;
|
|
443
|
+
return {
|
|
444
|
+
availableCount: rawList.length > 0 ? credits.length : typeof availRaw === "number" && Number.isFinite(availRaw) && availRaw >= 0 ? Math.round(availRaw) : 0,
|
|
445
|
+
credits
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function parseUsageResetCreditsSummary(raw) {
|
|
449
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
450
|
+
const record = raw;
|
|
451
|
+
const availRaw = record.available_count;
|
|
452
|
+
if (typeof availRaw !== "number" || !Number.isFinite(availRaw) || availRaw < 0) return;
|
|
453
|
+
const availableCount = Math.round(availRaw);
|
|
454
|
+
const appRaw = record.applicable_available_count;
|
|
455
|
+
return {
|
|
456
|
+
availableCount,
|
|
457
|
+
applicableAvailableCount: typeof appRaw === "number" && Number.isFinite(appRaw) && appRaw >= 0 ? Math.min(availableCount, Math.round(appRaw)) : availableCount,
|
|
458
|
+
credits: availableCount === 0 ? [] : null
|
|
459
|
+
};
|
|
460
|
+
}
|
|
388
461
|
function parseUsagePayload(raw, fetchedAt) {
|
|
389
462
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
390
463
|
const record = raw;
|
|
@@ -394,53 +467,145 @@ function parseUsagePayload(raw, fetchedAt) {
|
|
|
394
467
|
const primary = parseWindow(rateRecord.primary_window, fetchedAt);
|
|
395
468
|
if (primary === null) return null;
|
|
396
469
|
const secondary = parseWindow(rateRecord.secondary_window, fetchedAt);
|
|
470
|
+
const planType = typeof record.plan_type === "string" && record.plan_type.trim() !== "" ? record.plan_type.trim() : "plus";
|
|
471
|
+
const limitReached = rateRecord.limit_reached === true || rateRecord.allowed === false || primary.usedPercent >= 100 || secondary !== null && secondary.usedPercent >= 100;
|
|
472
|
+
const resetCredits = parseUsageResetCreditsSummary(record.rate_limit_reset_credits);
|
|
397
473
|
return {
|
|
398
|
-
planType
|
|
399
|
-
limitReached
|
|
474
|
+
planType,
|
|
475
|
+
limitReached,
|
|
400
476
|
primary,
|
|
401
477
|
secondary,
|
|
478
|
+
...resetCredits !== void 0 ? { resetCredits } : {},
|
|
402
479
|
fetchedAt
|
|
403
480
|
};
|
|
404
481
|
}
|
|
482
|
+
function isWindowResetDue(snapshot, nowMs) {
|
|
483
|
+
const nowSec = nowMs / 1e3;
|
|
484
|
+
const fetchedSec = snapshot.fetchedAt / 1e3;
|
|
485
|
+
const primaryReset = snapshot.primary.resetAt;
|
|
486
|
+
if (primaryReset !== null && fetchedSec < primaryReset && nowSec >= primaryReset) return true;
|
|
487
|
+
const secondaryReset = snapshot.secondary?.resetAt ?? null;
|
|
488
|
+
if (secondaryReset !== null && fetchedSec < secondaryReset && nowSec >= secondaryReset) return true;
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
function normalizeConsumeOutcome(raw) {
|
|
492
|
+
if (typeof raw !== "string") return null;
|
|
493
|
+
switch (raw.trim()) {
|
|
494
|
+
case "reset":
|
|
495
|
+
case "nothing_to_reset":
|
|
496
|
+
case "no_credit":
|
|
497
|
+
case "already_redeemed": return raw.trim();
|
|
498
|
+
default: return null;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function generateRedeemRequestId(nowMs) {
|
|
502
|
+
const rand = Math.random().toString(16).slice(2, 14).padEnd(12, "0");
|
|
503
|
+
return `redeem-${nowMs.toString(16)}-${rand}`;
|
|
504
|
+
}
|
|
405
505
|
function createUsageFetcher(options) {
|
|
406
|
-
const
|
|
506
|
+
const getFetch = () => options.fetchImpl ?? globalThis.fetch;
|
|
407
507
|
const nowFn = options.now ?? Date.now;
|
|
408
508
|
const ttlMs = options.ttlMs ?? 6e4;
|
|
409
509
|
const minForceIntervalMs = options.minForceIntervalMs ?? 5e3;
|
|
510
|
+
const autoRefresh = options.autoRefresh === true;
|
|
410
511
|
let cached;
|
|
411
|
-
let
|
|
512
|
+
let lastAttemptAt;
|
|
513
|
+
let hasConsumer = false;
|
|
514
|
+
let dirtyRevision = 0;
|
|
515
|
+
let fetchedRevision = 0;
|
|
412
516
|
let inflight;
|
|
517
|
+
let trailingTimer;
|
|
518
|
+
let disposed = false;
|
|
519
|
+
const isStale = () => dirtyRevision > fetchedRevision;
|
|
520
|
+
async function resolveAuthHeaders(extra) {
|
|
521
|
+
const creds = await options.resolveCredentials();
|
|
522
|
+
return options.buildHeaders({
|
|
523
|
+
Authorization: `Bearer ${creds.accessToken}`,
|
|
524
|
+
Accept: "application/json",
|
|
525
|
+
...creds.accountId !== void 0 ? { "ChatGPT-Account-Id": creds.accountId } : {},
|
|
526
|
+
...extra
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
function clearTrailingTimer() {
|
|
530
|
+
if (trailingTimer !== void 0) {
|
|
531
|
+
clearTimeout(trailingTimer);
|
|
532
|
+
trailingTimer = void 0;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function scheduleBackgroundRefresh(deferIfCached = false) {
|
|
536
|
+
if (!autoRefresh || !hasConsumer || disposed || !isStale() || inflight !== void 0) return;
|
|
537
|
+
const baseTime = cached?.fetchedAt ?? lastAttemptAt;
|
|
538
|
+
if (baseTime === void 0) {
|
|
539
|
+
runSingleFlight();
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const ageMs = Math.max(0, nowFn() - baseTime);
|
|
543
|
+
if (!deferIfCached && ageMs >= ttlMs) {
|
|
544
|
+
runSingleFlight();
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (trailingTimer !== void 0) return;
|
|
548
|
+
const delayMs = deferIfCached ? Math.max(1, ageMs >= ttlMs ? ttlMs : ttlMs - ageMs) : Math.max(1, ttlMs - ageMs);
|
|
549
|
+
trailingTimer = setTimeout(() => {
|
|
550
|
+
trailingTimer = void 0;
|
|
551
|
+
if (disposed || !isStale()) return;
|
|
552
|
+
runSingleFlight();
|
|
553
|
+
}, delayMs);
|
|
554
|
+
trailingTimer.unref?.();
|
|
555
|
+
}
|
|
413
556
|
async function fetchFresh() {
|
|
557
|
+
const revAtStart = dirtyRevision;
|
|
414
558
|
try {
|
|
415
|
-
const
|
|
416
|
-
const headers =
|
|
417
|
-
Authorization: `Bearer ${creds.accessToken}`,
|
|
418
|
-
Accept: "application/json",
|
|
419
|
-
...creds.accountId !== void 0 ? { "ChatGPT-Account-Id": creds.accountId } : {}
|
|
420
|
-
});
|
|
559
|
+
const fetchFn = getFetch();
|
|
560
|
+
const headers = await resolveAuthHeaders();
|
|
421
561
|
const response = await fetchFn(CODEX_USAGE_URL, {
|
|
422
562
|
method: "GET",
|
|
423
563
|
headers,
|
|
424
564
|
signal: AbortSignal.timeout(USAGE_FETCH_TIMEOUT_MS)
|
|
425
565
|
});
|
|
566
|
+
const finishedAt = nowFn();
|
|
567
|
+
lastAttemptAt = finishedAt;
|
|
568
|
+
fetchedRevision = Math.max(fetchedRevision, revAtStart);
|
|
426
569
|
if (!response.ok) return {
|
|
427
570
|
ok: false,
|
|
428
571
|
error: `Codex 限额查询失败 (HTTP ${response.status})`,
|
|
429
572
|
...cached !== void 0 ? { quota: cached } : {}
|
|
430
573
|
};
|
|
431
|
-
|
|
574
|
+
let parsed = parseUsagePayload(await response.json(), finishedAt);
|
|
432
575
|
if (parsed === null) return {
|
|
433
576
|
ok: false,
|
|
434
577
|
error: "Codex 限额响应格式无法解析",
|
|
435
578
|
...cached !== void 0 ? { quota: cached } : {}
|
|
436
579
|
};
|
|
580
|
+
if (parsed.resetCredits !== void 0 && parsed.resetCredits.availableCount > 0) try {
|
|
581
|
+
const detailRes = await fetchFn(CODEX_RESET_CREDITS_URL, {
|
|
582
|
+
method: "GET",
|
|
583
|
+
headers,
|
|
584
|
+
signal: AbortSignal.timeout(USAGE_FETCH_TIMEOUT_MS)
|
|
585
|
+
});
|
|
586
|
+
if (detailRes.ok) {
|
|
587
|
+
const detailParsed = parseResetCreditsPayload(await detailRes.json(), finishedAt);
|
|
588
|
+
if (detailParsed !== null) {
|
|
589
|
+
const nextAvailable = detailParsed.availableCount;
|
|
590
|
+
parsed = {
|
|
591
|
+
...parsed,
|
|
592
|
+
resetCredits: {
|
|
593
|
+
availableCount: nextAvailable,
|
|
594
|
+
applicableAvailableCount: Math.min(nextAvailable, parsed.resetCredits.applicableAvailableCount),
|
|
595
|
+
credits: detailParsed.credits
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
} catch {}
|
|
437
601
|
cached = parsed;
|
|
438
|
-
stale = false;
|
|
439
602
|
return {
|
|
440
603
|
ok: true,
|
|
441
604
|
quota: parsed
|
|
442
605
|
};
|
|
443
606
|
} catch (error) {
|
|
607
|
+
lastAttemptAt = nowFn();
|
|
608
|
+
fetchedRevision = Math.max(fetchedRevision, revAtStart);
|
|
444
609
|
return {
|
|
445
610
|
ok: false,
|
|
446
611
|
error: safeErrorMessage(error),
|
|
@@ -448,26 +613,98 @@ function createUsageFetcher(options) {
|
|
|
448
613
|
};
|
|
449
614
|
}
|
|
450
615
|
}
|
|
616
|
+
function runSingleFlight() {
|
|
617
|
+
clearTrailingTimer();
|
|
618
|
+
if (!inflight) inflight = fetchFresh().finally(() => {
|
|
619
|
+
inflight = void 0;
|
|
620
|
+
if (autoRefresh && !disposed && isStale()) scheduleBackgroundRefresh();
|
|
621
|
+
});
|
|
622
|
+
return inflight;
|
|
623
|
+
}
|
|
451
624
|
return {
|
|
625
|
+
onRequestStart() {
|
|
626
|
+
dirtyRevision += 1;
|
|
627
|
+
scheduleBackgroundRefresh(true);
|
|
628
|
+
},
|
|
452
629
|
invalidate() {
|
|
453
|
-
|
|
630
|
+
dirtyRevision += 1;
|
|
631
|
+
scheduleBackgroundRefresh(false);
|
|
454
632
|
},
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
633
|
+
dispose() {
|
|
634
|
+
disposed = true;
|
|
635
|
+
clearTrailingTimer();
|
|
636
|
+
},
|
|
637
|
+
async consumeResetCredit(params) {
|
|
638
|
+
const creditId = nonEmptyString(params?.creditId) ?? void 0;
|
|
639
|
+
const redeemRequestId = nonEmptyString(params?.redeemRequestId) ?? generateRedeemRequestId(nowFn());
|
|
640
|
+
try {
|
|
641
|
+
const fetchFn = getFetch();
|
|
642
|
+
const headers = await resolveAuthHeaders({ "Content-Type": "application/json" });
|
|
643
|
+
const response = await fetchFn(CODEX_RESET_CREDITS_CONSUME_URL, {
|
|
644
|
+
method: "POST",
|
|
645
|
+
headers,
|
|
646
|
+
body: JSON.stringify({
|
|
647
|
+
redeem_request_id: redeemRequestId,
|
|
648
|
+
...creditId !== void 0 ? { credit_id: creditId } : {}
|
|
649
|
+
}),
|
|
650
|
+
signal: AbortSignal.timeout(USAGE_FETCH_TIMEOUT_MS)
|
|
651
|
+
});
|
|
652
|
+
if (!response.ok) return {
|
|
653
|
+
ok: false,
|
|
654
|
+
error: `Codex 重置卡核销失败 (HTTP ${response.status})`,
|
|
655
|
+
...cached !== void 0 ? { quota: cached } : {}
|
|
656
|
+
};
|
|
657
|
+
const outcome = normalizeConsumeOutcome((await response.json())?.outcome);
|
|
658
|
+
if (outcome === null) return {
|
|
659
|
+
ok: false,
|
|
660
|
+
error: "Codex 重置卡核销响应格式无法解析",
|
|
661
|
+
...cached !== void 0 ? { quota: cached } : {}
|
|
662
|
+
};
|
|
663
|
+
dirtyRevision += 1;
|
|
664
|
+
if (inflight !== void 0) await inflight;
|
|
665
|
+
const refreshed = isStale() ? await runSingleFlight() : cached !== void 0 ? {
|
|
459
666
|
ok: true,
|
|
460
667
|
quota: cached
|
|
668
|
+
} : await runSingleFlight();
|
|
669
|
+
const latestQuota = refreshed.ok ? refreshed.quota : cached;
|
|
670
|
+
return {
|
|
671
|
+
ok: true,
|
|
672
|
+
outcome,
|
|
673
|
+
...latestQuota !== void 0 ? { quota: latestQuota } : {}
|
|
461
674
|
};
|
|
462
|
-
|
|
675
|
+
} catch (error) {
|
|
676
|
+
return {
|
|
677
|
+
ok: false,
|
|
678
|
+
error: safeErrorMessage(error),
|
|
679
|
+
...cached !== void 0 ? { quota: cached } : {}
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
},
|
|
683
|
+
async getUsage(force = false) {
|
|
684
|
+
hasConsumer = true;
|
|
685
|
+
if (cached !== void 0) {
|
|
686
|
+
const nowMs = nowFn();
|
|
687
|
+
const ageMs = nowMs - cached.fetchedAt;
|
|
688
|
+
const stale = isStale();
|
|
689
|
+
const resetDue = isWindowResetDue(cached, nowMs);
|
|
690
|
+
if (!force) {
|
|
691
|
+
if (ageMs >= 0 && ageMs < ttlMs && (!stale || autoRefresh)) {
|
|
692
|
+
if (stale) scheduleBackgroundRefresh(false);
|
|
693
|
+
return {
|
|
694
|
+
ok: true,
|
|
695
|
+
quota: cached
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
if (autoRefresh && !stale && !resetDue) return {
|
|
699
|
+
ok: true,
|
|
700
|
+
quota: cached
|
|
701
|
+
};
|
|
702
|
+
} else if (!stale && ageMs >= 0 && ageMs < minForceIntervalMs) return {
|
|
463
703
|
ok: true,
|
|
464
704
|
quota: cached
|
|
465
705
|
};
|
|
466
706
|
}
|
|
467
|
-
|
|
468
|
-
inflight = void 0;
|
|
469
|
-
});
|
|
470
|
-
return inflight;
|
|
707
|
+
return runSingleFlight();
|
|
471
708
|
}
|
|
472
709
|
};
|
|
473
710
|
}
|
|
@@ -1101,7 +1338,7 @@ function apply(ctx, config = {}) {
|
|
|
1101
1338
|
});
|
|
1102
1339
|
function wrapOptions(catalog, model, options = {}) {
|
|
1103
1340
|
const modelMeta = catalog.metadataById.get(model?.id);
|
|
1104
|
-
const { temperature: _temperature, sessionId, onPayload, ...rest } = options;
|
|
1341
|
+
const { temperature: _temperature, sessionId, onPayload, onProviderStreamEvent, ...rest } = options;
|
|
1105
1342
|
const initiatorId = (ctx.get?.("agents"))?.currentInitiator?.()?.id;
|
|
1106
1343
|
const normalizedSessionId = normalizeCodexSessionId(sessionId) ?? normalizeCodexSessionId(initiatorId);
|
|
1107
1344
|
const wireSessionId = toCodexWireSessionId(normalizedSessionId);
|
|
@@ -1117,12 +1354,18 @@ function apply(ctx, config = {}) {
|
|
|
1117
1354
|
}),
|
|
1118
1355
|
async onPayload(payload, requestModel) {
|
|
1119
1356
|
return sanitizeCodexPayload(await onPayload?.(payload, requestModel) ?? payload, modelMeta, getEffectiveFastMode(normalizedSessionId));
|
|
1357
|
+
},
|
|
1358
|
+
async onProviderStreamEvent(event, eventModel) {
|
|
1359
|
+
await onProviderStreamEvent?.(event, eventModel);
|
|
1360
|
+
const eventType = event !== null && typeof event === "object" ? event.type : void 0;
|
|
1361
|
+
if (eventType === "response.completed" || eventType === "response.done" || eventType === "response.incomplete") usageFetcher.invalidate();
|
|
1120
1362
|
}
|
|
1121
1363
|
};
|
|
1122
1364
|
}
|
|
1123
1365
|
const usageFetcher = createUsageFetcher({
|
|
1124
1366
|
resolveCredentials: () => authManager.resolveCredentials(),
|
|
1125
|
-
buildHeaders: (authHeaders) => buildCodexHeaders(authHeaders, ensureSnapshot().catalog.clientVersion)
|
|
1367
|
+
buildHeaders: (authHeaders) => buildCodexHeaders(authHeaders, ensureSnapshot().catalog.clientVersion),
|
|
1368
|
+
autoRefresh: true
|
|
1126
1369
|
});
|
|
1127
1370
|
function createSnapshotPiProvider(snapshotCatalog) {
|
|
1128
1371
|
return Object.freeze({
|
|
@@ -1135,11 +1378,11 @@ function apply(ctx, config = {}) {
|
|
|
1135
1378
|
}),
|
|
1136
1379
|
getModels: () => snapshotCatalog.models,
|
|
1137
1380
|
stream: (model, context, options) => {
|
|
1138
|
-
usageFetcher.
|
|
1381
|
+
usageFetcher.onRequestStart();
|
|
1139
1382
|
return baseProvider.stream(model, context, wrapOptions(snapshotCatalog, model, options));
|
|
1140
1383
|
},
|
|
1141
1384
|
streamSimple: (model, context, options) => {
|
|
1142
|
-
usageFetcher.
|
|
1385
|
+
usageFetcher.onRequestStart();
|
|
1143
1386
|
return baseProvider.streamSimple(model, context, wrapOptions(snapshotCatalog, model, options));
|
|
1144
1387
|
}
|
|
1145
1388
|
});
|
|
@@ -1311,6 +1554,7 @@ function apply(ctx, config = {}) {
|
|
|
1311
1554
|
return () => {
|
|
1312
1555
|
watcherDisposed = true;
|
|
1313
1556
|
closeModelsCacheWatchers();
|
|
1557
|
+
usageFetcher.dispose();
|
|
1314
1558
|
};
|
|
1315
1559
|
}, "codex-subscription: models_cache watcher");
|
|
1316
1560
|
else {
|
|
@@ -1318,6 +1562,7 @@ function apply(ctx, config = {}) {
|
|
|
1318
1562
|
ctx.on?.("dispose", () => {
|
|
1319
1563
|
watcherDisposed = true;
|
|
1320
1564
|
closeModelsCacheWatchers();
|
|
1565
|
+
usageFetcher.dispose();
|
|
1321
1566
|
});
|
|
1322
1567
|
}
|
|
1323
1568
|
const mountUsageRoute = (connCtx) => {
|
|
@@ -1337,6 +1582,33 @@ function apply(ctx, config = {}) {
|
|
|
1337
1582
|
body = null;
|
|
1338
1583
|
}
|
|
1339
1584
|
const record = body !== null && typeof body === "object" && !Array.isArray(body) ? body : null;
|
|
1585
|
+
if (record !== null && record.action === "consumeResetCredit") {
|
|
1586
|
+
if (record.creditId !== void 0 && typeof record.creditId !== "string" || record.redeemRequestId !== void 0 && typeof record.redeemRequestId !== "string") return new Response(JSON.stringify({
|
|
1587
|
+
ok: false,
|
|
1588
|
+
error: "Invalid request body: creditId and redeemRequestId must be strings when provided"
|
|
1589
|
+
}), {
|
|
1590
|
+
status: 400,
|
|
1591
|
+
headers: { "content-type": "application/json" }
|
|
1592
|
+
});
|
|
1593
|
+
const consumeResult = await usageFetcher.consumeResetCredit({
|
|
1594
|
+
creditId: record.creditId,
|
|
1595
|
+
redeemRequestId: record.redeemRequestId
|
|
1596
|
+
});
|
|
1597
|
+
appendPluginLog(getCodexHome(), ctx, consumeResult.ok ? "INFO" : "WARN", "reset_credit.consume", {
|
|
1598
|
+
creditId: record.creditId ?? null,
|
|
1599
|
+
ok: consumeResult.ok,
|
|
1600
|
+
...consumeResult.ok ? { outcome: consumeResult.outcome } : { error: consumeResult.error }
|
|
1601
|
+
});
|
|
1602
|
+
return new Response(JSON.stringify({
|
|
1603
|
+
...consumeResult,
|
|
1604
|
+
defaultFastMode: getConfigFastMode(),
|
|
1605
|
+
sessionFastModes: buildProjectedSessionFastModes(),
|
|
1606
|
+
sessionParents: Object.fromEntries(sessionParents)
|
|
1607
|
+
}), {
|
|
1608
|
+
status: 200,
|
|
1609
|
+
headers: { "content-type": "application/json" }
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1340
1612
|
if (record === null || typeof record.fastMode !== "boolean" || record.sessionId !== void 0 && typeof record.sessionId !== "string") return new Response(JSON.stringify({
|
|
1341
1613
|
ok: false,
|
|
1342
1614
|
error: "Invalid request body: boolean fastMode is required"
|
|
@@ -1439,6 +1711,7 @@ const __test = Object.freeze({
|
|
|
1439
1711
|
normalizeCodexSessionId,
|
|
1440
1712
|
toCodexWireSessionId,
|
|
1441
1713
|
parseUsagePayload,
|
|
1714
|
+
parseResetCreditsPayload,
|
|
1442
1715
|
createUsageFetcher
|
|
1443
1716
|
});
|
|
1444
1717
|
//#endregion
|