@narumitw/pi-usage 0.62.0 → 0.63.0

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.
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/openai-companion-auth.ts"],
4
+ "sourcesContent": ["import type { ExtensionContext } from \"@earendil-works/pi-coding-agent\";\nimport { fingerprintResolvedAuth } from \"./core.js\";\nimport { fallbackOAuthCredentialCandidates, type OAuthCredentialCandidateReader } from \"./oauth-credential-source.js\";\nimport { openAIAuthAccessToken } from \"./providers/openai-chatgpt.js\";\nimport { adapterForProvider, resolveUsageAuth } from \"./query.js\";\nimport type { ResolvedUsageAuth } from \"./types.js\";\n\n// A native app registration is routing metadata, not cryptographic same-account proof.\n// Keep this read-only boundary separate from legacy Codex querying and reset mutation policy.\nexport async function resolveOpenAICompanionAuth(\n ctx: ExtensionContext,\n native: ResolvedUsageAuth,\n salt: Uint8Array,\n credentialReader: (providerId: string) => unknown,\n candidateReader?: OAuthCredentialCandidateReader,\n): Promise<ResolvedUsageAuth> {\n const unavailable = () => ({\n ...native,\n fingerprint: fingerprintResolvedAuth(\n { apiKey: JSON.stringify([native.fingerprint, native.openaiClientId, null]) },\n salt,\n ),\n });\n if (candidateReader?.waitUntilReady && !(await candidateReader.waitUntilReady(ctx, \"openai-codex\"))) {\n throw new Error(\"Companion Codex OAuth credential readiness failed closed.\");\n }\n const adapter = adapterForProvider(\"openai-codex\");\n if (!adapter) throw new Error(\"Companion Codex usage support is unavailable.\");\n const companion = await resolveUsageAuth(ctx, adapter, salt, credentialReader);\n if (!companion) return unavailable();\n if (companion.source !== \"OAuth\")\n throw new Error(\"Companion usage requires /login openai-codex OAuth, not an API key.\");\n // Legacy querying is more permissive. Enforce explicit Authorization removals and duplicates\n // against preserved fresh provider auth before using its normalized request headers.\n const access = openAIAuthAccessToken(companion.auth);\n if (ctx.model?.provider === \"openai-codex\") {\n const selected = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);\n if (\n !selected.ok ||\n (selected.baseUrl && new URL(selected.baseUrl).origin !== \"https://chatgpt.com\") ||\n openAIAuthAccessToken(selected) !== access\n ) {\n throw new Error(\"Selected Codex model authorization does not match the companion OAuth credential.\");\n }\n }\n if (!access || access !== openAIAuthAccessToken(companion)) {\n throw new Error(\"Companion Codex runtime authorization did not match its OAuth credential.\");\n }\n const accountId = codexAccountId(access);\n if (!accountId) throw new Error(\"Companion Codex token did not contain a valid account ID.\");\n const offered = candidateReader\n ? await candidateReader(ctx, \"openai-codex\")\n : fallbackOAuthCredentialCandidates(\"openai-codex\", credentialReader);\n if (!offered.ok) throw new Error(\"Companion Codex OAuth credential discovery failed closed.\");\n const matches = new Set<string>();\n for (const candidate of offered.candidates) {\n const credential = object(candidate);\n if (credential?.type !== \"oauth\" || credential.access !== access) continue;\n if (\n credential.accountId !== accountId ||\n typeof credential.refresh !== \"string\" ||\n !credential.refresh ||\n typeof credential.expires !== \"number\" ||\n !Number.isFinite(credential.expires)\n ) {\n throw new Error(\"The matching companion Codex OAuth credential was incomplete or had a different account ID.\");\n }\n matches.add(credential.refresh);\n }\n if (matches.size !== 1) throw new Error(\"Companion Codex OAuth must uniquely match the fresh runtime credential.\");\n const headers = { Authorization: `Bearer ${access}`, \"ChatGPT-Account-Id\": accountId };\n return {\n ...native,\n openaiCompanion: { headers },\n fingerprint: fingerprintResolvedAuth(\n { apiKey: JSON.stringify([native.fingerprint, native.openaiClientId, companion.fingerprint, headers]) },\n salt,\n ),\n secrets: [\n ...new Set([...native.secrets, ...companion.secrets, ...matches, access, accountId, headers.Authorization]),\n ],\n };\n}\n\nfunction codexAccountId(access: string): string | undefined {\n try {\n const parts = access.split(\".\");\n if (parts.length !== 3 || !parts[1]) return undefined;\n const payload = object(JSON.parse(Buffer.from(parts[1], \"base64url\").toString(\"utf8\")));\n const account = object(payload?.[\"https://api.openai.com/auth\"])?.chatgpt_account_id;\n return typeof account === \"string\" && account.length > 0 && account.length <= 512 && /^[\\x21-\\x7e]+$/u.test(account)\n ? account\n : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction object(value: unknown): Record<string, unknown> | undefined {\n return value && typeof value === \"object\" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -3,7 +3,7 @@
3
3
  import {
4
4
  CHATGPT_USAGE_SETTINGS_URL,
5
5
  fetchProviderJson
6
- } from "./chunk-WEXCFYZX.ts";
6
+ } from "./chunk-ENCC7IBP.ts";
7
7
 
8
8
  // src/providers/openai-companion-usage.ts
9
9
  var USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
@@ -117,4 +117,4 @@ export {
117
117
  matchingOpenAIAppBuckets,
118
118
  queryOpenAICompanionUsage
119
119
  };
120
- //# sourceMappingURL=openai-companion-usage-Y3WTMUB2.ts.map
120
+ //# sourceMappingURL=openai-companion-usage-4NOBHGP7.ts.map
package/dist/index.ts CHANGED
@@ -45,7 +45,7 @@ import {
45
45
  runWithConcurrency,
46
46
  sanitizeDisplayText,
47
47
  usageAdapters
48
- } from "./chunks/chunk-WEXCFYZX.ts";
48
+ } from "./chunks/chunk-ENCC7IBP.ts";
49
49
 
50
50
  // src/codex-fast.ts
51
51
  import { calculateCost, hasApi } from "@earendil-works/pi-ai";
@@ -425,7 +425,9 @@ function formatUsageStatusline(report, model, now = Date.now(), showCodexResetCo
425
425
  return `chatgpt ${["chatgpt-plan", "chatgpt-app"].map((group) => {
426
426
  const windows = report.buckets.filter((bucket) => bucket.groupId === group).map((bucket) => {
427
427
  const reset = formatResetCountdown(bucket.resetsAt, now);
428
- return `${bucket.remaining === void 0 ? "unavailable" : `${bucket.remaining.toFixed(0)}%`} ${reset ? `\u21BB ${reset}` : formatWindowLabel(bucket.windowMinutes, "weekly", true)}`;
428
+ const window = reset ? `\u21BB ${reset}` : formatWindowLabel(bucket.windowMinutes, "weekly", true);
429
+ if (group === "chatgpt-app") return window;
430
+ return `${bucket.remaining === void 0 ? "unavailable" : `${bucket.remaining.toFixed(0)}%`} ${window}`;
429
431
  });
430
432
  return `${group === "chatgpt-plan" ? "plan" : "app"} ${windows.join(" ")}`;
431
433
  }).join(" \xB7 ")}`;
@@ -840,9 +842,8 @@ function formatOpenAICompanionReport(lines, report) {
840
842
  for (const group of ["chatgpt-plan", "chatgpt-app"]) {
841
843
  lines.push(group === "chatgpt-plan" ? "Plan limits:" : "App limits:");
842
844
  for (const bucket of report.buckets.filter((bucket2) => bucket2.groupId === group)) {
843
- lines.push(
844
- `${formatWindowLabel(bucket.windowMinutes, "weekly", false).padEnd(VALUE_COLUMN)}${formatPercentBucket(bucket)}`
845
- );
845
+ const value = group === "chatgpt-app" ? bucket.resetsAt === void 0 ? "Reset time unavailable" : `resets ${formatReset(bucket.resetsAt)}` : formatPercentBucket(bucket);
846
+ lines.push(`${formatWindowLabel(bucket.windowMinutes, "weekly", false).padEnd(VALUE_COLUMN)}${value}`);
846
847
  }
847
848
  }
848
849
  for (const metric of report.metrics)
@@ -1000,7 +1001,6 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
1000
1001
  var USAGE_SETTINGS_FILE = "pi-usage.json";
1001
1002
  var MAX_USAGE_SETTINGS_BYTES = 64 * 1024;
1002
1003
  var DEFAULT_USAGE_SETTINGS = Object.freeze({
1003
- openaiCompanionUsage: false,
1004
1004
  codexFastMode: false,
1005
1005
  codexStatusResetCountdown: true,
1006
1006
  codexStatusPercentage: "remaining",
@@ -1011,7 +1011,6 @@ function usageSettingsPath() {
1011
1011
  }
1012
1012
  function normalizeUsageSettings(value) {
1013
1013
  if (!isRecord2(value)) return void 0;
1014
- if (Object.hasOwn(value, "openaiCompanionUsage") && typeof value.openaiCompanionUsage !== "boolean") return void 0;
1015
1014
  if (Object.hasOwn(value, "codexFastMode") && typeof value.codexFastMode !== "boolean") {
1016
1015
  return void 0;
1017
1016
  }
@@ -1031,7 +1030,6 @@ function normalizeUsageSettings(value) {
1031
1030
  effectiveTargets.fireworks = value.fireworksAccountId;
1032
1031
  }
1033
1032
  return {
1034
- openaiCompanionUsage: value.openaiCompanionUsage === true,
1035
1033
  codexFastMode: typeof value.codexFastMode === "boolean" ? value.codexFastMode : DEFAULT_USAGE_SETTINGS.codexFastMode,
1036
1034
  codexStatusResetCountdown: typeof value.codexStatusResetCountdown === "boolean" ? value.codexStatusResetCountdown : DEFAULT_USAGE_SETTINGS.codexStatusResetCountdown,
1037
1035
  codexStatusPercentage: value.codexStatusPercentage === "used" ? "used" : DEFAULT_USAGE_SETTINGS.codexStatusPercentage,
@@ -1451,156 +1449,121 @@ async function showUsageSettings(ctx, settingsRuntime, parentSignal, isCurrent,
1451
1449
  const { HorizontalRule, renderBoundedFrame } = await import("@narumitw/pi-tui-kit");
1452
1450
  if (parentSignal.aborted || !isCurrent()) return false;
1453
1451
  let changed = false;
1454
- let selectedId;
1455
- for (; ; ) {
1456
- const result = await ctx.ui.custom((tui, theme, _keybindings, done) => {
1457
- const localController = new AbortController();
1458
- const signal = AbortSignal.any([parentSignal, localController.signal]);
1459
- let closing = false;
1460
- let saveQueue = Promise.resolve();
1461
- const state = settingsRuntime.get();
1462
- const items = [
1463
- {
1464
- id: "codexFastMode",
1465
- label: "Codex Fast mode",
1466
- description: "Use faster Codex routing at increased plan allowance consumption.",
1467
- currentValue: state.settings.codexFastMode ? ON : OFF,
1468
- values: [OFF, ON]
1469
- },
1470
- {
1471
- id: "codexStatusResetCountdown",
1472
- label: "Codex reset countdown",
1473
- description: "Show time remaining until each Codex usage limit resets.",
1474
- currentValue: state.settings.codexStatusResetCountdown ? ON : OFF,
1475
- values: [OFF, ON]
1476
- },
1477
- {
1478
- id: "codexStatusPercentage",
1479
- label: "Codex percentage",
1480
- description: "Show remaining or used Codex quota in the statusline.",
1481
- currentValue: settingValueLabel("codexStatusPercentage", state.settings.codexStatusPercentage),
1482
- values: [REMAINING, USED]
1483
- },
1484
- {
1485
- id: "openaiCompanionUsage",
1486
- label: "Experimental ChatGPT companion usage",
1487
- description: "Read undocumented plan/app usage using a same-account Codex login; matching registration does not prove identity.",
1488
- currentValue: state.settings.openaiCompanionUsage ? ON : OFF,
1489
- values: [OFF, ON]
1490
- }
1491
- ];
1492
- const rule = new HorizontalRule({ ruleStyle: (text) => theme.fg("border", text) });
1493
- let settingsList;
1494
- const close = (result2) => {
1495
- if (closing) return;
1496
- closing = true;
1497
- localController.abort();
1498
- done(result2);
1499
- };
1500
- const cancel = () => close(changed);
1501
- const queueUpdate = (id, requested, display) => {
1502
- saveQueue = saveQueue.then(async () => {
1503
- if (signal.aborted || !isCurrent()) return;
1504
- const previous = settingsRuntime.get().settings[id];
1505
- if (settingsRuntime.get().kind === "invalid") {
1506
- settingsList.updateValue(id, settingValueLabel(id, previous));
1507
- if (!signal.aborted && isCurrent()) {
1508
- ctx.ui.notify("Repair pi-usage.json and reload before changing settings.", "error");
1509
- tui.requestRender();
1510
- }
1511
- return;
1512
- }
1513
- try {
1514
- if (id === "openaiCompanionUsage" && requested === true && previous !== true) {
1515
- selectedId = id;
1516
- close("consent");
1517
- return;
1518
- }
1519
- const patch = {};
1520
- patch[id] = requested;
1521
- await settingsRuntime.update(patch, signal);
1522
- } catch (error) {
1523
- if (signal.aborted || !isCurrent()) return;
1524
- settingsList.updateValue(id, settingValueLabel(id, previous));
1525
- ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
1452
+ return ctx.ui.custom((tui, theme, _keybindings, done) => {
1453
+ const localController = new AbortController();
1454
+ const signal = AbortSignal.any([parentSignal, localController.signal]);
1455
+ let closing = false;
1456
+ let saveQueue = Promise.resolve();
1457
+ const state = settingsRuntime.get();
1458
+ const items = [
1459
+ {
1460
+ id: "codexFastMode",
1461
+ label: "Codex Fast mode",
1462
+ description: "Use faster Codex routing at increased plan allowance consumption.",
1463
+ currentValue: state.settings.codexFastMode ? ON : OFF,
1464
+ values: [OFF, ON]
1465
+ },
1466
+ {
1467
+ id: "codexStatusResetCountdown",
1468
+ label: "Codex reset countdown",
1469
+ description: "Show time remaining until each Codex usage limit resets.",
1470
+ currentValue: state.settings.codexStatusResetCountdown ? ON : OFF,
1471
+ values: [OFF, ON]
1472
+ },
1473
+ {
1474
+ id: "codexStatusPercentage",
1475
+ label: "Codex percentage",
1476
+ description: "Show remaining or used Codex quota in the statusline.",
1477
+ currentValue: settingValueLabel("codexStatusPercentage", state.settings.codexStatusPercentage),
1478
+ values: [REMAINING, USED]
1479
+ }
1480
+ ];
1481
+ const rule = new HorizontalRule({ ruleStyle: (text) => theme.fg("border", text) });
1482
+ let settingsList;
1483
+ const close = (result) => {
1484
+ if (closing) return;
1485
+ closing = true;
1486
+ localController.abort();
1487
+ done(result);
1488
+ };
1489
+ const cancel = () => close(changed);
1490
+ const queueUpdate = (id, requested, display) => {
1491
+ saveQueue = saveQueue.then(async () => {
1492
+ if (signal.aborted || !isCurrent()) return;
1493
+ const previous = settingsRuntime.get().settings[id];
1494
+ if (settingsRuntime.get().kind === "invalid") {
1495
+ settingsList.updateValue(id, settingValueLabel(id, previous));
1496
+ if (!signal.aborted && isCurrent()) {
1497
+ ctx.ui.notify("Repair pi-usage.json and reload before changing settings.", "error");
1526
1498
  tui.requestRender();
1527
- return;
1528
- }
1529
- if (previous !== requested) {
1530
- changed = true;
1531
- onApplied(id);
1532
1499
  }
1500
+ return;
1501
+ }
1502
+ try {
1503
+ const patch = {};
1504
+ patch[id] = requested;
1505
+ await settingsRuntime.update(patch, signal);
1506
+ } catch (error) {
1533
1507
  if (signal.aborted || !isCurrent()) return;
1534
- settingsList.updateValue(id, display);
1535
- tui.requestRender();
1536
- });
1537
- };
1538
- settingsList = new SettingsList(
1539
- items,
1540
- items.length + 2,
1541
- getSettingsListTheme(),
1542
- (id, value) => {
1543
- if (closing || signal.aborted || !isCurrent()) return;
1544
- selectedId = id;
1545
- if (id === "codexStatusPercentage") {
1546
- queueUpdate(id, value === USED ? "used" : "remaining", value);
1547
- } else if (id === "codexFastMode" || id === "codexStatusResetCountdown" || id === "openaiCompanionUsage") {
1548
- queueUpdate(id, value === ON, value);
1549
- }
1550
- },
1551
- cancel
1552
- );
1553
- if (selectedId) settingsList.selectItem(selectedId);
1554
- parentSignal.addEventListener("abort", cancel, { once: true });
1555
- return {
1556
- render(width) {
1557
- const title = new Text(theme.fg("accent", theme.bold("pi-usage Settings")), 1, 0).render(width);
1558
- const content = settingsList.render(width);
1559
- const focusedRow = content.findIndex((line) => /^[→›]\s/u.test(stripVTControlCharacters(line)));
1560
- const terminalRows = Number.isFinite(tui.terminal?.rows) ? Math.floor(tui.terminal.rows) : 24;
1561
- const [ruleLine = ""] = rule.render(width);
1562
- return renderBoundedFrame({
1563
- width,
1564
- maxRows: Math.max(1, terminalRows - 3),
1565
- rule: ruleLine,
1566
- title,
1567
- content,
1568
- priorityRows: focusedRow < 0 ? [] : [focusedRow],
1569
- focusedRow
1570
- });
1571
- },
1572
- invalidate: () => settingsList.invalidate(),
1573
- handleInput(data) {
1574
- if (closing) return;
1575
- if (matchesKey(data, Key.ctrl("c"))) cancel();
1576
- else settingsList.handleInput(data);
1508
+ settingsList.updateValue(id, settingValueLabel(id, previous));
1509
+ ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
1577
1510
  tui.requestRender();
1578
- },
1579
- dispose() {
1580
- localController.abort();
1581
- parentSignal.removeEventListener("abort", cancel);
1511
+ return;
1582
1512
  }
1583
- };
1584
- });
1585
- if (result !== "consent" || parentSignal.aborted || !isCurrent()) return changed;
1586
- try {
1587
- const accepted = await ctx.ui.confirm(
1588
- "Enable experimental ChatGPT companion usage?",
1589
- "Requires /login openai-codex with the same ChatGPT account/workspace as native OpenAI. Only the companion token and its matching account ID are sent to undocumented ChatGPT usage endpoints. Registration matching is not independent proof of the same user/workspace. No reset or allowance mutations are performed.",
1590
- { signal: parentSignal }
1591
- );
1592
- if (parentSignal.aborted || !isCurrent()) return changed;
1593
- if (accepted && !settingsRuntime.get().settings.openaiCompanionUsage) {
1594
- await settingsRuntime.update({ openaiCompanionUsage: true }, parentSignal);
1595
- changed = true;
1596
- onApplied("openaiCompanionUsage");
1513
+ if (previous !== requested) {
1514
+ changed = true;
1515
+ onApplied(id);
1516
+ }
1517
+ if (signal.aborted || !isCurrent()) return;
1518
+ settingsList.updateValue(id, display);
1519
+ tui.requestRender();
1520
+ });
1521
+ };
1522
+ settingsList = new SettingsList(
1523
+ items,
1524
+ items.length + 2,
1525
+ getSettingsListTheme(),
1526
+ (id, value) => {
1527
+ if (closing || signal.aborted || !isCurrent()) return;
1528
+ if (id === "codexStatusPercentage") {
1529
+ queueUpdate(id, value === USED ? "used" : "remaining", value);
1530
+ } else if (id === "codexFastMode" || id === "codexStatusResetCountdown") {
1531
+ queueUpdate(id, value === ON, value);
1532
+ }
1533
+ },
1534
+ cancel
1535
+ );
1536
+ parentSignal.addEventListener("abort", cancel, { once: true });
1537
+ return {
1538
+ render(width) {
1539
+ const title = new Text(theme.fg("accent", theme.bold("pi-usage Settings")), 1, 0).render(width);
1540
+ const content = settingsList.render(width);
1541
+ const focusedRow = content.findIndex((line) => /^[→›]\s/u.test(stripVTControlCharacters(line)));
1542
+ const terminalRows = Number.isFinite(tui.terminal?.rows) ? Math.floor(tui.terminal.rows) : 24;
1543
+ const [ruleLine = ""] = rule.render(width);
1544
+ return renderBoundedFrame({
1545
+ width,
1546
+ maxRows: Math.max(1, terminalRows - 3),
1547
+ rule: ruleLine,
1548
+ title,
1549
+ content,
1550
+ priorityRows: focusedRow < 0 ? [] : [focusedRow],
1551
+ focusedRow
1552
+ });
1553
+ },
1554
+ invalidate: () => settingsList.invalidate(),
1555
+ handleInput(data) {
1556
+ if (closing) return;
1557
+ if (matchesKey(data, Key.ctrl("c"))) cancel();
1558
+ else settingsList.handleInput(data);
1559
+ tui.requestRender();
1560
+ },
1561
+ dispose() {
1562
+ localController.abort();
1563
+ parentSignal.removeEventListener("abort", cancel);
1597
1564
  }
1598
- } catch (error) {
1599
- if (parentSignal.aborted || !isCurrent()) return changed;
1600
- ctx.ui.notify(`Could not enable ChatGPT companion usage: ${errorMessage(error)}`, "error");
1601
- }
1602
- if (parentSignal.aborted || !isCurrent()) return changed;
1603
- }
1565
+ };
1566
+ });
1604
1567
  }
1605
1568
 
1606
1569
  // src/usage.ts
@@ -1625,14 +1588,7 @@ function usageExtension(pi, dependencies = {}) {
1625
1588
  const credentialCandidates = createOAuthCredentialCandidateReader(pi, credentialReader);
1626
1589
  const createRedemptionId = dependencies.createRedemptionId ?? randomUUID2;
1627
1590
  const settingsRuntime = dependencies.settingsRuntime ?? createUsageSettingsRuntime();
1628
- const resolveUsageAuth2 = (ctx, adapter, salt, reader, candidates) => resolveUsageAuth(
1629
- ctx,
1630
- adapter,
1631
- salt,
1632
- reader,
1633
- candidates,
1634
- settingsRuntime.get().settings.openaiCompanionUsage
1635
- );
1591
+ const resolveUsageAuth2 = resolveUsageAuth;
1636
1592
  const cache = new UsageCache(CACHE_TTL_MS);
1637
1593
  const failureBackoff = /* @__PURE__ */ new Map();
1638
1594
  const latestQueries = /* @__PURE__ */ new Map();
@@ -1754,7 +1710,6 @@ function usageExtension(pi, dependencies = {}) {
1754
1710
  const expectedSessionId = ctx.sessionManager.getSessionId();
1755
1711
  const expectedModelIdentity = modelIdentity(ctx.model);
1756
1712
  const expectedTargetId = adapter.targets ? settingsRuntime.get().settings.selectedTargets[adapter.id] : void 0;
1757
- const expectedCompanionUsage = settingsRuntime.get().settings.openaiCompanionUsage;
1758
1713
  const providerName = providerDisplayName(ctx, adapter.id);
1759
1714
  let auth;
1760
1715
  try {
@@ -1795,7 +1750,7 @@ function usageExtension(pi, dependencies = {}) {
1795
1750
  "zai",
1796
1751
  "zai-coding-cn"
1797
1752
  ].includes(adapter.id);
1798
- const requestContextChanged = () => expectedSessionGeneration !== sessionGeneration || ctx.sessionManager.getSessionId() !== expectedSessionId || modelIdentity(ctx.model) !== expectedModelIdentity || adapter.id === "openai" && settingsRuntime.get().settings.openaiCompanionUsage !== expectedCompanionUsage || adapter.targets !== void 0 && settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedTargetId;
1753
+ const requestContextChanged = () => expectedSessionGeneration !== sessionGeneration || ctx.sessionManager.getSessionId() !== expectedSessionId || modelIdentity(ctx.model) !== expectedModelIdentity || adapter.targets !== void 0 && settingsRuntime.get().settings.selectedTargets[adapter.id] !== expectedTargetId;
1799
1754
  if (requiresRequestBoundaryGuard && requestContextChanged()) throw abortError();
1800
1755
  if (!auth) {
1801
1756
  if (adapter.id === "openai") invalidateProviderState(adapter.id);
@@ -2444,16 +2399,6 @@ function usageExtension(pi, dependencies = {}) {
2444
2399
  controller.signal,
2445
2400
  () => statusGeneration === menuGeneration && !controller.signal.aborted,
2446
2401
  (id) => {
2447
- if (id === "openaiCompanionUsage" && statusGeneration === menuGeneration && !controller.signal.aborted) {
2448
- for (const active of nativeQueryControllers) active.abort();
2449
- invalidateProviderState("openai");
2450
- if (ctx.model?.provider === "openai") {
2451
- statusController?.abort();
2452
- statusController = void 0;
2453
- clearStatusTimers();
2454
- safeSetStatus(ctx, void 0);
2455
- }
2456
- }
2457
2402
  if ((id === "codexStatusResetCountdown" || id === "codexStatusPercentage") && stableCurrent && statusGeneration === menuGeneration && !controller.signal.aborted) {
2458
2403
  publishStableCurrent(ctx, stableCurrent);
2459
2404
  }