@jingyi0605/codingns4dsh 0.2.1-rc.2 → 0.2.1-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/README.en.md +6 -4
  2. package/README.md +6 -6
  3. package/data/build/dist/bootstrap/dsh-connection-adapter.d.ts +1 -1
  4. package/data/build/dist/bootstrap/index.d.ts +1 -1
  5. package/data/build/dist/client/bundle.js +5224 -290
  6. package/data/build/dist/client/bundle.js.map +1 -1
  7. package/data/build/dist/client/client.magpie-model-tag.js +155 -5
  8. package/data/build/dist/client/client.magpie-model-tag.js.map +1 -1
  9. package/data/build/dist/client/client.native-workspace-store.js +30 -4
  10. package/data/build/dist/client/client.native-workspace-store.js.map +1 -1
  11. package/data/build/dist/client/client.transport.js +747 -48
  12. package/data/build/dist/client/client.transport.js.map +1 -1
  13. package/data/build/dist/client/desktop-assistant.js +953 -185
  14. package/data/build/dist/client/feature-requirement.d.ts +43 -0
  15. package/data/build/dist/client/feature-requirement.d.ts.map +1 -0
  16. package/data/build/dist/client/feature-requirement.js +105 -0
  17. package/data/build/dist/client/feature-requirement.js.map +1 -0
  18. package/data/build/dist/client/features/index.d.ts +1 -0
  19. package/data/build/dist/client/features/index.d.ts.map +1 -1
  20. package/data/build/dist/client/features/index.js +3 -0
  21. package/data/build/dist/client/features/index.js.map +1 -1
  22. package/data/build/dist/client/features/magpie-management-controls.d.ts +46 -0
  23. package/data/build/dist/client/features/magpie-management-controls.d.ts.map +1 -0
  24. package/data/build/dist/client/features/magpie-management-controls.js +794 -0
  25. package/data/build/dist/client/features/magpie-management-controls.js.map +1 -0
  26. package/data/build/dist/client/features/magpie-management-page.d.ts +129 -0
  27. package/data/build/dist/client/features/magpie-management-page.d.ts.map +1 -0
  28. package/data/build/dist/client/features/magpie-management-page.js +1279 -0
  29. package/data/build/dist/client/features/magpie-management-page.js.map +1 -0
  30. package/data/build/dist/client/features/magpie-management.d.ts +7 -0
  31. package/data/build/dist/client/features/magpie-management.d.ts.map +1 -0
  32. package/data/build/dist/client/features/magpie-management.js +154 -0
  33. package/data/build/dist/client/features/magpie-management.js.map +1 -0
  34. package/data/build/dist/client/features/magpie-mark.d.ts +10 -0
  35. package/data/build/dist/client/features/magpie-mark.d.ts.map +1 -0
  36. package/data/build/dist/client/features/magpie-mark.js +21 -0
  37. package/data/build/dist/client/features/magpie-mark.js.map +1 -0
  38. package/data/build/dist/client/features/magpie-provider-logo.d.ts +11 -0
  39. package/data/build/dist/client/features/magpie-provider-logo.d.ts.map +1 -0
  40. package/data/build/dist/client/features/magpie-provider-logo.js +35 -0
  41. package/data/build/dist/client/features/magpie-provider-logo.js.map +1 -0
  42. package/data/build/dist/client/features/subscription-quota-groups.d.ts +193 -0
  43. package/data/build/dist/client/features/subscription-quota-groups.d.ts.map +1 -0
  44. package/data/build/dist/client/features/subscription-quota-groups.js +548 -0
  45. package/data/build/dist/client/features/subscription-quota-groups.js.map +1 -0
  46. package/data/build/dist/client/git-management.d.ts.map +1 -1
  47. package/data/build/dist/client/git-management.js +11 -9
  48. package/data/build/dist/client/git-management.js.map +1 -1
  49. package/data/build/dist/client/git-panel-styles.js +1 -0
  50. package/data/build/dist/client/git-panel-styles.js.map +1 -1
  51. package/data/build/dist/client/index.d.ts +1 -1
  52. package/data/build/dist/client/index.d.ts.map +1 -1
  53. package/data/build/dist/client/index.js +1 -1
  54. package/data/build/dist/client/index.js.map +1 -1
  55. package/data/build/dist/client/locale.d.ts.map +1 -1
  56. package/data/build/dist/client/locale.js +7 -0
  57. package/data/build/dist/client/locale.js.map +1 -1
  58. package/data/build/dist/client/locales/magpie.d.ts +4 -0
  59. package/data/build/dist/client/locales/magpie.d.ts.map +1 -0
  60. package/data/build/dist/client/locales/magpie.js +644 -0
  61. package/data/build/dist/client/locales/magpie.js.map +1 -0
  62. package/data/build/dist/client/locales/usage.d.ts.map +1 -1
  63. package/data/build/dist/client/locales/usage.js +52 -0
  64. package/data/build/dist/client/locales/usage.js.map +1 -1
  65. package/data/build/dist/client/magpie-model-tag.d.ts +9 -1
  66. package/data/build/dist/client/magpie-model-tag.d.ts.map +1 -1
  67. package/data/build/dist/client/magpie-model-tag.js +15 -3
  68. package/data/build/dist/client/magpie-model-tag.js.map +1 -1
  69. package/data/build/dist/client/provider-icons.d.ts +8 -0
  70. package/data/build/dist/client/provider-icons.d.ts.map +1 -1
  71. package/data/build/dist/client/provider-icons.js +122 -2
  72. package/data/build/dist/client/provider-icons.js.map +1 -1
  73. package/data/build/dist/client/settings-bridge.d.ts +1 -0
  74. package/data/build/dist/client/settings-bridge.d.ts.map +1 -1
  75. package/data/build/dist/client/settings-bridge.js +29 -10
  76. package/data/build/dist/client/settings-bridge.js.map +1 -1
  77. package/data/build/dist/client/settings-section.d.ts.map +1 -1
  78. package/data/build/dist/client/settings-section.js +21 -3
  79. package/data/build/dist/client/settings-section.js.map +1 -1
  80. package/data/build/dist/client/style.css +224 -221
  81. package/data/build/dist/client/subscription-slot.d.ts +22 -1
  82. package/data/build/dist/client/subscription-slot.d.ts.map +1 -1
  83. package/data/build/dist/client/subscription-slot.js +374 -25
  84. package/data/build/dist/client/subscription-slot.js.map +1 -1
  85. package/data/build/dist/dsh-capabilities/client/global-panel-adapter.d.ts +27 -0
  86. package/data/build/dist/dsh-capabilities/client/global-panel-adapter.d.ts.map +1 -0
  87. package/data/build/dist/dsh-capabilities/client/global-panel-adapter.js +109 -0
  88. package/data/build/dist/dsh-capabilities/client/global-panel-adapter.js.map +1 -0
  89. package/data/build/dist/dsh-capabilities/host/session-title-adapter.d.ts +2 -1
  90. package/data/build/dist/dsh-capabilities/host/session-title-adapter.d.ts.map +1 -1
  91. package/data/build/dist/dsh-capabilities/host/session-title-adapter.js +32 -2
  92. package/data/build/dist/dsh-capabilities/host/session-title-adapter.js.map +1 -1
  93. package/data/build/dist/dsh-capabilities/matrix.d.ts.map +1 -1
  94. package/data/build/dist/dsh-capabilities/matrix.js +12 -11
  95. package/data/build/dist/dsh-capabilities/matrix.js.map +1 -1
  96. package/data/build/dist/dsh-capabilities/routes.d.ts.map +1 -1
  97. package/data/build/dist/dsh-capabilities/routes.js +12 -9
  98. package/data/build/dist/dsh-capabilities/routes.js.map +1 -1
  99. package/data/build/dist/dsh-capabilities/types.d.ts +1 -1
  100. package/data/build/dist/dsh-capabilities/types.d.ts.map +1 -1
  101. package/data/build/dist/dsh-capabilities/types.js.map +1 -1
  102. package/data/build/dist/host/cli-adapters/acp-cli-driver.d.ts +27 -0
  103. package/data/build/dist/host/cli-adapters/acp-cli-driver.d.ts.map +1 -1
  104. package/data/build/dist/host/cli-adapters/acp-cli-driver.js +88 -13
  105. package/data/build/dist/host/cli-adapters/acp-cli-driver.js.map +1 -1
  106. package/data/build/dist/host/cli-adapters/codex-driver.d.ts +10 -0
  107. package/data/build/dist/host/cli-adapters/codex-driver.d.ts.map +1 -1
  108. package/data/build/dist/host/cli-adapters/codex-driver.js +32 -1
  109. package/data/build/dist/host/cli-adapters/codex-driver.js.map +1 -1
  110. package/data/build/dist/host/cli-adapters/command-code-acp-loader.d.ts +6 -5
  111. package/data/build/dist/host/cli-adapters/command-code-acp-loader.d.ts.map +1 -1
  112. package/data/build/dist/host/cli-adapters/command-code-acp-loader.js +72 -12
  113. package/data/build/dist/host/cli-adapters/command-code-acp-loader.js.map +1 -1
  114. package/data/build/dist/host/cli-adapters/command-code-driver.d.ts.map +1 -1
  115. package/data/build/dist/host/cli-adapters/command-code-driver.js +26 -1
  116. package/data/build/dist/host/cli-adapters/command-code-driver.js.map +1 -1
  117. package/data/build/dist/host/cli-adapters/dsh-message-projector.d.ts.map +1 -1
  118. package/data/build/dist/host/cli-adapters/dsh-message-projector.js +15 -2
  119. package/data/build/dist/host/cli-adapters/dsh-message-projector.js.map +1 -1
  120. package/data/build/dist/host/cli-adapters/feature.d.ts +13 -0
  121. package/data/build/dist/host/cli-adapters/feature.d.ts.map +1 -1
  122. package/data/build/dist/host/cli-adapters/feature.js +438 -6
  123. package/data/build/dist/host/cli-adapters/feature.js.map +1 -1
  124. package/data/build/dist/host/cli-adapters/grok-driver.js +5 -0
  125. package/data/build/dist/host/cli-adapters/grok-driver.js.map +1 -1
  126. package/data/build/dist/host/cli-adapters/magpie-detection.d.ts +72 -0
  127. package/data/build/dist/host/cli-adapters/magpie-detection.d.ts.map +1 -0
  128. package/data/build/dist/host/cli-adapters/magpie-detection.js +312 -0
  129. package/data/build/dist/host/cli-adapters/magpie-detection.js.map +1 -0
  130. package/data/build/dist/host/cli-adapters/magpie-management.d.ts +50 -0
  131. package/data/build/dist/host/cli-adapters/magpie-management.d.ts.map +1 -0
  132. package/data/build/dist/host/cli-adapters/magpie-management.js +1343 -0
  133. package/data/build/dist/host/cli-adapters/magpie-management.js.map +1 -0
  134. package/data/build/dist/host/cli-adapters/magpie-update.d.ts +59 -0
  135. package/data/build/dist/host/cli-adapters/magpie-update.d.ts.map +1 -0
  136. package/data/build/dist/host/cli-adapters/magpie-update.js +249 -0
  137. package/data/build/dist/host/cli-adapters/magpie-update.js.map +1 -0
  138. package/data/build/dist/host/cli-adapters/magpie-usage.d.ts +70 -8
  139. package/data/build/dist/host/cli-adapters/magpie-usage.d.ts.map +1 -1
  140. package/data/build/dist/host/cli-adapters/magpie-usage.js +1075 -125
  141. package/data/build/dist/host/cli-adapters/magpie-usage.js.map +1 -1
  142. package/data/build/dist/host/cli-adapters/magpie-web.d.ts +69 -0
  143. package/data/build/dist/host/cli-adapters/magpie-web.d.ts.map +1 -0
  144. package/data/build/dist/host/cli-adapters/magpie-web.js +485 -0
  145. package/data/build/dist/host/cli-adapters/magpie-web.js.map +1 -0
  146. package/data/build/dist/host/cli-adapters/process-utils.d.ts +2 -0
  147. package/data/build/dist/host/cli-adapters/process-utils.d.ts.map +1 -1
  148. package/data/build/dist/host/cli-adapters/process-utils.js +8 -0
  149. package/data/build/dist/host/cli-adapters/process-utils.js.map +1 -1
  150. package/data/build/dist/host/cli-adapters/session-performance-projection.d.ts +6 -0
  151. package/data/build/dist/host/cli-adapters/session-performance-projection.d.ts.map +1 -1
  152. package/data/build/dist/host/cli-adapters/session-performance-projection.js +25 -0
  153. package/data/build/dist/host/cli-adapters/session-performance-projection.js.map +1 -1
  154. package/data/build/dist/host/dsh-gateway-feature.d.ts.map +1 -1
  155. package/data/build/dist/host/dsh-gateway-feature.js +26 -3
  156. package/data/build/dist/host/dsh-gateway-feature.js.map +1 -1
  157. package/data/build/dist/host/features/host-status.d.ts +25 -0
  158. package/data/build/dist/host/features/host-status.d.ts.map +1 -1
  159. package/data/build/dist/host/features/host-status.js +60 -6
  160. package/data/build/dist/host/features/host-status.js.map +1 -1
  161. package/data/build/dist/host/features/lan-access-dsh.d.ts.map +1 -1
  162. package/data/build/dist/host/features/lan-access-dsh.js +61 -10
  163. package/data/build/dist/host/features/lan-access-dsh.js.map +1 -1
  164. package/data/build/dist/host/features/session-translation-protection.d.ts.map +1 -1
  165. package/data/build/dist/host/features/session-translation-protection.js +2 -1
  166. package/data/build/dist/host/features/session-translation-protection.js.map +1 -1
  167. package/data/build/dist/host/lan-access-dsh.d.ts.map +1 -1
  168. package/data/build/dist/host/lan-access-dsh.js +6 -2
  169. package/data/build/dist/host/lan-access-dsh.js.map +1 -1
  170. package/data/build/dist/host/modules/pwa/pwa-session-notifications.js +2 -1
  171. package/data/build/dist/host/modules/pwa/pwa-session-notifications.js.map +1 -1
  172. package/data/build/dist/host/rpc.d.ts +15 -2
  173. package/data/build/dist/host/rpc.d.ts.map +1 -1
  174. package/data/build/dist/host/rpc.js +47 -6
  175. package/data/build/dist/host/rpc.js.map +1 -1
  176. package/data/build/dist/host/settings.d.ts.map +1 -1
  177. package/data/build/dist/host/settings.js +5 -0
  178. package/data/build/dist/host/settings.js.map +1 -1
  179. package/data/build/dist/shared/contracts/config.d.ts +18 -0
  180. package/data/build/dist/shared/contracts/config.d.ts.map +1 -1
  181. package/data/build/dist/shared/contracts/config.js +8 -0
  182. package/data/build/dist/shared/contracts/config.js.map +1 -1
  183. package/data/build/dist/shared/contracts/errors.d.ts +2 -2
  184. package/data/build/dist/shared/contracts/feature.d.ts +7 -0
  185. package/data/build/dist/shared/contracts/feature.d.ts.map +1 -1
  186. package/data/build/dist/shared/contracts/magpie-management.d.ts +414 -0
  187. package/data/build/dist/shared/contracts/magpie-management.d.ts.map +1 -0
  188. package/data/build/dist/shared/contracts/magpie-management.js +2 -0
  189. package/data/build/dist/shared/contracts/magpie-management.js.map +1 -0
  190. package/data/build/dist/shared/contracts/subscription.d.ts +77 -1
  191. package/data/build/dist/shared/contracts/subscription.d.ts.map +1 -1
  192. package/data/build/dist/shared/contracts/subscription.js.map +1 -1
  193. package/data/build/dist/shared/contracts/version.d.ts +4 -4
  194. package/data/build/dist/shared/contracts/version.js +3 -3
  195. package/data/build/dist/shared/index.d.ts +3 -1
  196. package/data/build/dist/shared/index.d.ts.map +1 -1
  197. package/data/build/dist/shared/index.js +2 -1
  198. package/data/build/dist/shared/index.js.map +1 -1
  199. package/data/build/dist/shared/magpie-provider-id.d.ts +12 -0
  200. package/data/build/dist/shared/magpie-provider-id.d.ts.map +1 -0
  201. package/data/build/dist/shared/magpie-provider-id.js +16 -0
  202. package/data/build/dist/shared/magpie-provider-id.js.map +1 -0
  203. package/data/build/dist/shared/session-title.d.ts.map +1 -1
  204. package/data/build/dist/shared/session-title.js +10 -4
  205. package/data/build/dist/shared/session-title.js.map +1 -1
  206. package/package.json +29 -29
  207. package/profile/README.md +2 -2
  208. package/profile/package.json +3 -3
  209. package/profile/version.json +3 -3
  210. package/version.json +3 -3
@@ -1,5 +1,6 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { commandEnvironment, resolveCommandPath, runAsyncCommand, WINDOWS, withCommandSignal } from './process-utils.js';
3
+ import { parseMagpieWebProviders } from './magpie-management.js';
3
4
  const MAGPIE_CATALOG_TTL_MS = 60_000;
4
5
  const MAGPIE_MODEL_METADATA_TTL_MS = 60_000;
5
6
  const MAGPIE_FAILURE_CACHE_TTL_MS = 5_000;
@@ -7,9 +8,14 @@ const MAX_MAGPIE_MODEL_METADATA_CACHE_ENTRIES = 4;
7
8
  const MAGPIE_MINIMUM_CLI_VERSION = [0, 1, 1295];
8
9
  const MAGPIE_CLI_TIMEOUT_MS = 10_000;
9
10
  const MAGPIE_HTTP_TIMEOUT_MS = 3_000;
11
+ const MAGPIE_DASHBOARD_CACHE_TTL_MS = 15_000;
12
+ const MAGPIE_PROVIDER_CATALOG_TTL_MS = 15_000;
10
13
  const MAX_MAGPIE_CLI_OUTPUT_BYTES = 512 * 1024;
11
14
  const MAX_MAGPIE_RESPONSE_BYTES = 2 * 1024 * 1024;
15
+ const MAX_MAGPIE_ICON_FETCHES = 64;
12
16
  const MAX_MAGPIE_MODELS = 10_000;
17
+ const MAX_MAGPIE_SESSION_SCAN_ROWS = 10_000;
18
+ const MAX_MAGPIE_SESSION_IDENTITIES = 16;
13
19
  const MAX_MODEL_TOKEN_LIMIT = 1_000_000_000;
14
20
  const MAGPIE_MODEL_ID_PATTERN = '[A-Za-z0-9][A-Za-z0-9._-]*(?:/[A-Za-z0-9][A-Za-z0-9._:/-]*)+';
15
21
  const MAGPIE_AGENT_BY_ADAPTER = {
@@ -39,11 +45,7 @@ const MAGPIE_USAGE_AGENT_ALIASES = {
39
45
  // usage ledger, while its verified model catalog uses `commandcode`.
40
46
  commandcode: ['commandcode', 'codingns4dsh'],
41
47
  };
42
- /**
43
- * Reads Magpie's public local Gateway API only after its CLI confirms that the
44
- * current Agent model is in that Agent's catalog. No Agent or Magpie config
45
- * files are opened here; the provider session ID is supplied by the Host.
46
- */
48
+ /** Reads usage from the authenticated local Magpie Web API and exact-matches session identity. */
47
49
  export class MagpieUsageService {
48
50
  readModels;
49
51
  readVersion;
@@ -51,6 +53,8 @@ export class MagpieUsageService {
51
53
  now;
52
54
  timeoutMs;
53
55
  lifetimeSignal;
56
+ webApi;
57
+ onDashboardTiming;
54
58
  /** Agent CLI verification remains isolated; model metadata below is Gateway-wide and shared. */
55
59
  catalogs = new Map();
56
60
  catalogLoads = new Map();
@@ -59,6 +63,10 @@ export class MagpieUsageService {
59
63
  modelMetadataLoads = new Map();
60
64
  versionCheck;
61
65
  versionLoad;
66
+ dashboardCache = new Map();
67
+ dashboardRefreshes = new Map();
68
+ quotaLogoCache = new Map();
69
+ providerCatalog;
62
70
  constructor(options = {}) {
63
71
  this.readModels = options.readModels ?? readMagpieModels;
64
72
  // An injected catalog reader represents the whole fake CLI boundary in
@@ -70,6 +78,8 @@ export class MagpieUsageService {
70
78
  this.now = options.now ?? Date.now;
71
79
  this.timeoutMs = options.timeoutMs ?? MAGPIE_HTTP_TIMEOUT_MS;
72
80
  this.lifetimeSignal = options.lifetimeSignal;
81
+ this.webApi = options.webApi;
82
+ this.onDashboardTiming = options.onDashboardTiming;
73
83
  }
74
84
  async read(session, signal, includeRoute = false, dshSessionId) {
75
85
  signal?.throwIfAborted();
@@ -77,10 +87,6 @@ export class MagpieUsageService {
77
87
  const requestedModelId = cleanText(session.modelId);
78
88
  if (agent === null || requestedModelId === null)
79
89
  return null;
80
- const catalog = await this.loadCatalog(agent, signal);
81
- const modelId = resolveCatalogModelId(agent, requestedModelId, catalog, requestedModelId);
82
- if (catalog === null || modelId === null)
83
- return null;
84
90
  const providerSessionId = cleanText(session.providerSessionId);
85
91
  const nativeSessionId = cleanText(dshSessionId);
86
92
  // Native DSH requests use the DSH session ID as their Magpie session
@@ -88,7 +94,19 @@ export class MagpieUsageService {
88
94
  const sessionId = providerSessionId !== null && providerSessionId !== nativeSessionId
89
95
  ? providerSessionId
90
96
  : agent === 'dsh' ? nativeSessionId : null;
91
- return this.readFromGateway(agent, requestedModelId, catalog.gatewayOrigin, sessionId, signal, includeRoute);
97
+ // Command Code's ACP child tags loopback Magpie requests with the DSH
98
+ // session ID injected by the Host, so the ledger key differs from the ACP
99
+ // session ID shown in the panel. Older Hosts without that ID keep the
100
+ // provider ID so the panel still reports the same "no usage" state.
101
+ const magpieSessionId = agent === 'commandcode' ? nativeSessionId ?? sessionId : sessionId;
102
+ // During a rolling update the ACP child may still tag requests with its ACP
103
+ // session ID; fall back to it when the DSH-ID ledger row is absent.
104
+ const fallbackSessionId = agent === 'commandcode' && nativeSessionId !== null && sessionId !== null && nativeSessionId !== sessionId
105
+ ? sessionId
106
+ : null;
107
+ if (magpieSessionId === null)
108
+ return null;
109
+ return this.readFromWeb(agent, requestedModelId, sessionId, magpieSessionId, fallbackSessionId, signal, includeRoute);
92
110
  }
93
111
  /**
94
112
  * Mark Magpie models and enrich their public metadata. The marker is
@@ -174,47 +192,279 @@ export class MagpieUsageService {
174
192
  const metadata = await this.loadModelMetadata(catalog, signal);
175
193
  return metadata?.get(id) ?? null;
176
194
  }
177
- async readFromGateway(agent, modelId, gatewayOrigin, sessionId, signal, includeRoute) {
195
+ /** Read bounded, graph-ready usage and quota data through authenticated Magpie Web. */
196
+ async readDashboard(period, signal) {
197
+ signal?.throwIfAborted();
198
+ const cached = this.dashboardCache.get(period);
199
+ if (cached !== undefined) {
200
+ if (this.now() - cached.loadedAt >= MAGPIE_DASHBOARD_CACHE_TTL_MS) {
201
+ void this.refreshDashboard(period).catch(() => undefined);
202
+ }
203
+ return cached.dashboard;
204
+ }
205
+ return this.refreshDashboard(period, signal);
206
+ }
207
+ /** Stream cumulative dashboard snapshots as each independent Web API part is ready. */
208
+ async *readDashboardStream(period, signal, force = false) {
209
+ signal?.throwIfAborted();
210
+ const cached = this.dashboardCache.get(period);
211
+ if (!force && cached !== undefined) {
212
+ const stale = this.now() - cached.loadedAt >= MAGPIE_DASHBOARD_CACHE_TTL_MS;
213
+ yield { part: 'cache', dashboard: cached.dashboard, stale };
214
+ if (!stale)
215
+ return;
216
+ }
217
+ let latest;
218
+ for await (const chunk of this.readDashboardNetwork(period, signal)) {
219
+ latest = chunk.dashboard;
220
+ yield chunk;
221
+ }
222
+ if (latest !== undefined)
223
+ this.dashboardCache.set(period, { dashboard: latest, loadedAt: this.now() });
224
+ }
225
+ async refreshDashboard(period, signal) {
226
+ const pending = this.dashboardRefreshes.get(period);
227
+ if (pending !== undefined)
228
+ return awaitWithSignal(pending, signal);
229
+ const request = (async () => {
230
+ let latest;
231
+ for await (const chunk of this.readDashboardNetwork(period, signal))
232
+ latest = chunk.dashboard;
233
+ if (latest === undefined)
234
+ throw new Error('Magpie dashboard returned no snapshot');
235
+ this.dashboardCache.set(period, { dashboard: latest, loadedAt: this.now() });
236
+ return latest;
237
+ })().finally(() => {
238
+ if (this.dashboardRefreshes.get(period) === request)
239
+ this.dashboardRefreshes.delete(period);
240
+ });
241
+ this.dashboardRefreshes.set(period, request);
242
+ return awaitWithSignal(request, signal);
243
+ }
244
+ async *readDashboardNetwork(period, signal) {
245
+ const capturedAt = new Date(this.now()).toISOString();
246
+ let dashboard = emptyMagpieDashboard(period, capturedAt);
247
+ let quotas = null;
248
+ let logoLoad;
249
+ const results = new Map([
250
+ ['usage', this.timedDashboardRead('usage', () => this.readWebJson(`/api/usage?period=${magpieWebPeriod(period)}`, signal))],
251
+ ['quotas', this.timedDashboardRead('quotas', () => this.readWebJson('/api/usage/quotas', signal))],
252
+ ['history', this.timedDashboardRead('history', () => this.readWebJson('/api/usage/quotas/history?days=30', signal))],
253
+ ]);
254
+ const completed = new Map();
255
+ while (results.size > 0) {
256
+ const result = await Promise.race(results.values());
257
+ results.delete(result.part);
258
+ const response = asRecord(result.value);
259
+ const partOk = result.ok && response?.ok === true;
260
+ const partValue = partOk ? response.value : undefined;
261
+ completed.set(result.part, partOk);
262
+ if (result.part === 'usage') {
263
+ const parsed = partOk ? parseMagpieDashboardUsage(partValue) : null;
264
+ dashboard = {
265
+ ...dashboard,
266
+ usageStatus: parsed === null ? 'unavailable' : 'available',
267
+ aggregate: parsed?.aggregate ?? null,
268
+ series: parsed?.series ?? [],
269
+ agents: parsed?.agents ?? [],
270
+ providers: parsed?.providers ?? [],
271
+ models: parsed?.models ?? [],
272
+ accounts: parsed?.accounts ?? [],
273
+ sessions: parsed?.sessions ?? [],
274
+ sessionCount: parsed?.sessionCount ?? null,
275
+ };
276
+ }
277
+ else if (result.part === 'quotas') {
278
+ quotas = partOk ? parseMagpieQuotaCards(partValue) : null;
279
+ dashboard = { ...dashboard, quotaStatus: quotas === null ? 'unavailable' : 'available', quotas: quotas ?? [] };
280
+ if (quotas !== null)
281
+ logoLoad = this.timedDashboardRead('quota-logos', () => this.attachQuotaLogos(quotas, signal)).then((result) => result.value);
282
+ }
283
+ else {
284
+ const parsed = partOk ? parseMagpieQuotaHistory(partValue) : null;
285
+ dashboard = { ...dashboard, quotaHistoryStatus: parsed === null ? 'unavailable' : 'available', quotaHistory: parsed ?? [] };
286
+ }
287
+ yield { part: result.part, dashboard, stale: false };
288
+ }
289
+ dashboard = { ...dashboard, availabilityIssue: [...completed.values()].every((ok) => !ok) ? 'web-api-unavailable' : null };
290
+ yield { part: 'complete', dashboard, stale: false };
291
+ if (logoLoad !== undefined) {
292
+ dashboard = { ...dashboard, quotas: await logoLoad };
293
+ yield { part: 'logos', dashboard, stale: false };
294
+ }
295
+ }
296
+ async timedDashboardRead(part, read) {
297
+ const started = this.now();
298
+ try {
299
+ return { part: part === 'quota-logos' ? 'logos' : part, value: await read(), ok: true };
300
+ }
301
+ finally {
302
+ this.onDashboardTiming?.({ part, elapsedMs: Math.max(0, this.now() - started) });
303
+ }
304
+ }
305
+ /** Read one bounded Web API ledger page and exact-match session identities on Host. */
306
+ async readRequestPage(options, signal) {
307
+ signal?.throwIfAborted();
308
+ if (!isMagpieDashboardPeriod(options.period)
309
+ || !Number.isSafeInteger(options.offset) || options.offset < 0 || options.offset > 1_000_000
310
+ || !Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > 100)
311
+ return null;
312
+ const sessionId = options.sessionId === undefined ? null : cleanText(options.sessionId);
313
+ const sessionIds = options.sessionIds === undefined
314
+ ? []
315
+ : options.sessionIds.map(cleanText);
316
+ const sessionAgent = options.sessionAgent === undefined ? null : cleanText(options.sessionAgent);
317
+ if ((options.sessionId !== undefined && sessionId === null)
318
+ || sessionIds.some((value) => value === null)
319
+ || sessionIds.length > MAX_MAGPIE_SESSION_IDENTITIES
320
+ || (options.sessionAgent !== undefined && (sessionAgent === null || !isMagpieAgentId(sessionAgent)))
321
+ || ((sessionId === null) !== (sessionAgent === null))
322
+ || (sessionIds.length > 0 && sessionId === null))
323
+ return null;
324
+ const requestedSessionIds = sessionId === null
325
+ ? []
326
+ : [...new Set([sessionId, ...sessionIds.filter((value) => value !== null)])];
327
+ if (requestedSessionIds.length > MAX_MAGPIE_SESSION_IDENTITIES)
328
+ return null;
329
+ const readCandidatePage = async (offset, limit, querySessionId) => {
330
+ const query = new URLSearchParams({ period: magpieWebPeriod(options.period), limit: String(limit), offset: String(offset) });
331
+ if (querySessionId !== null)
332
+ query.set('q', querySessionId);
333
+ const response = await this.readWebJson(`/api/usage/requests?${query.toString()}`, signal);
334
+ if (!response.ok)
335
+ return null;
336
+ const root = asRecord(response.value);
337
+ const rows = root?.rows;
338
+ if (!Array.isArray(rows) || rows.length > 500)
339
+ return null;
340
+ return { rows, total: safeCount(root?.total) };
341
+ };
342
+ if (sessionId === null) {
343
+ const page = await readCandidatePage(options.offset, options.limit, null);
344
+ if (page === null)
345
+ return null;
346
+ const providerRecords = page.rows.some((row) => routeText(asRecord(row)?.agent) === 'codex')
347
+ ? await this.readProviderCatalog(signal)
348
+ : [];
349
+ const items = parseMagpieRequestRows(page.rows, null, null, providerRecords);
350
+ return {
351
+ period: options.period,
352
+ offset: options.offset,
353
+ limit: options.limit,
354
+ total: page.total,
355
+ hasMore: page.total === null ? page.rows.length >= options.limit : options.offset + page.rows.length < page.total,
356
+ sessionId,
357
+ sessionAgent,
358
+ items: items.slice(0, options.limit),
359
+ };
360
+ }
361
+ // Magpie's `q` is a free-text candidate search, not an exact session filter.
362
+ // Fetch candidates from the beginning and apply the exact filter before paging;
363
+ // otherwise unrelated rows can consume the requested page and hide valid rows.
364
+ //
365
+ // Scanning stops one match past the requested page: enough to fill it and to prove
366
+ // whether another page exists. `total` therefore stays `null` while the identity's
367
+ // candidate window is not exhausted; the page that reads the window's end reports
368
+ // the exact count. Aggregated DSH sessions scan every contributing identity so one
369
+ // provider conversation cannot hide another.
370
+ const candidateLimit = 100;
371
+ const requestedEnd = options.offset + options.limit;
372
+ const rawRowsByKey = new Map();
373
+ let fullyScanned = true;
374
+ const perIdentityScanLimit = Math.floor(MAX_MAGPIE_SESSION_SCAN_ROWS / requestedSessionIds.length);
375
+ const acceptedAgents = MAGPIE_USAGE_AGENT_ALIASES[sessionAgent] ?? [sessionAgent];
376
+ const acceptedSessionIds = new Set(requestedSessionIds);
377
+ const matchesIn = () => [...rawRowsByKey.values()].reduce((count, row) => {
378
+ const record = asRecord(row);
379
+ if (record === null)
380
+ return count;
381
+ if (!acceptedSessionIds.has(cleanText(record.session) ?? '') && !acceptedSessionIds.has(cleanText(record.native_session) ?? ''))
382
+ return count;
383
+ return acceptedAgents.includes(cleanText(record.agent) ?? '') ? count + 1 : count;
384
+ }, 0);
385
+ for (const querySessionId of requestedSessionIds) {
386
+ let candidateOffset = 0;
387
+ let reachedEnd = false;
388
+ while (!reachedEnd && candidateOffset < perIdentityScanLimit) {
389
+ const pageLimit = Math.min(candidateLimit, perIdentityScanLimit - candidateOffset);
390
+ const page = await readCandidatePage(candidateOffset, pageLimit, querySessionId);
391
+ if (page === null)
392
+ return null;
393
+ for (const row of page.rows) {
394
+ const key = magpieRequestRowKey(row);
395
+ if (!rawRowsByKey.has(key))
396
+ rawRowsByKey.set(key, row);
397
+ }
398
+ const pageLength = page.rows.length;
399
+ const nextOffset = candidateOffset + pageLength;
400
+ reachedEnd = pageLength === 0
401
+ || nextOffset <= candidateOffset
402
+ || (page.total === null ? pageLength < pageLimit : nextOffset >= page.total);
403
+ candidateOffset = nextOffset;
404
+ // Enough rows for this page plus one more: no need to read the rest of the window.
405
+ if (!reachedEnd && matchesIn() > requestedEnd)
406
+ break;
407
+ }
408
+ if (!reachedEnd)
409
+ fullyScanned = false;
410
+ }
411
+ const rawRows = [...rawRowsByKey.values()];
412
+ const providerRecords = rawRows.some((row) => routeText(asRecord(row)?.agent) === 'codex')
413
+ ? await this.readProviderCatalog(signal)
414
+ : [];
415
+ const matching = parseMagpieRequestRows(rawRows, sessionId, sessionAgent, providerRecords, requestedSessionIds);
416
+ const total = fullyScanned ? matching.length : null;
417
+ return {
418
+ period: options.period,
419
+ offset: options.offset,
420
+ limit: options.limit,
421
+ total,
422
+ hasMore: !fullyScanned || matching.length > requestedEnd,
423
+ sessionId,
424
+ sessionAgent,
425
+ items: matching.slice(options.offset, requestedEnd),
426
+ };
427
+ }
428
+ async readFromWeb(agent, modelId, sessionId, magpieSessionId, fallbackSessionId, signal, includeRoute) {
178
429
  const capturedAt = new Date(this.now()).toISOString();
179
- if (gatewayOrigin === null)
180
- return unavailableMagpieSnapshot(agent, modelId, sessionId, this.now(), capturedAt);
181
430
  const [usageResult, quotaResult] = await Promise.all([
182
- this.readGatewayJson(gatewayOrigin, '/v1/magpie/usage?period=today', signal),
183
- this.readGatewayJson(gatewayOrigin, '/v1/magpie/quotas', signal),
431
+ this.readWebJson('/api/usage?period=today', signal),
432
+ this.readWebJson('/api/usage/quotas', signal),
184
433
  ]);
185
434
  const aggregate = usageResult.ok ? readTotals(usageResult.value) : null;
186
- const sessionUsage = usageResult.ok && sessionId !== null
187
- ? readSession(usageResult.value, agent, sessionId)
435
+ const primarySession = usageResult.ok && magpieSessionId !== null
436
+ ? readSession(usageResult.value, agent, magpieSessionId)
437
+ : null;
438
+ const fallbackSession = primarySession === null && usageResult.ok && fallbackSessionId !== null
439
+ ? readSession(usageResult.value, agent, fallbackSessionId)
188
440
  : null;
189
- // A providerSessionId is not enough to prove that Magpie knows this Agent
190
- // session. Require the exact Agent + ID pair in Magpie's own usage result
191
- // before asking the route endpoint, and then verify its echoed identity.
192
- const canReadRoute = includeRoute && sessionId !== null && sessionId.length <= 128 && sessionUsage !== null;
193
- const routeResultPromise = canReadRoute
194
- ? this.readGatewayJson(gatewayOrigin, `/v1/magpie/route?session=${encodeURIComponent(sessionId)}`, signal)
195
- : Promise.resolve({ ok: false });
196
- const routeHistoryResultPromise = canReadRoute
197
- ? this.readGatewayJson(gatewayOrigin, `/v1/magpie/usage/requests?period=today&q=${encodeURIComponent(sessionId)}&limit=100`, signal)
198
- : Promise.resolve({ ok: false });
199
- const [routeResult, routeHistoryResult] = await Promise.all([routeResultPromise, routeHistoryResultPromise]);
200
- const routeRead = sessionId === null
201
- ? { status: 'session-unavailable', route: null }
202
- : !includeRoute
203
- ? { status: 'not-requested', route: null }
204
- : !canReadRoute
205
- ? { status: 'unavailable', route: null }
206
- : !routeResult.ok
207
- ? { status: 'unavailable', route: null }
208
- : readRoute(routeResult.value, sessionId);
209
- const routeHistory = routeHistoryResult.ok && sessionId !== null && sessionUsage !== null
210
- ? readSessionRouteHistory(routeHistoryResult.value, agent, sessionId)
441
+ const sessionUsage = primarySession ?? fallbackSession;
442
+ const resolvedSessionId = primarySession !== null ? magpieSessionId : fallbackSession !== null ? fallbackSessionId : magpieSessionId;
443
+ if (sessionUsage === null)
444
+ return null;
445
+ const canReadRoute = includeRoute && resolvedSessionId !== null && resolvedSessionId.length <= 128;
446
+ const routeHistoryResult = canReadRoute
447
+ ? await this.readWebJson(`/api/usage/requests?period=today&offset=0&limit=100&q=${encodeURIComponent(resolvedSessionId)}`, signal)
448
+ : { ok: false };
449
+ const routeRows = routeHistoryResult.ok ? asRecord(routeHistoryResult.value)?.rows : null;
450
+ const routeHistoryValid = Array.isArray(routeRows) && routeRows.length <= 500;
451
+ const providerRecords = agent === 'codex' && routeHistoryValid
452
+ && routeRows.some((row) => routeText(asRecord(row)?.agent) === 'codex')
453
+ ? await this.readProviderCatalog(signal)
454
+ : [];
455
+ const routeHistory = routeHistoryResult.ok && routeHistoryValid && resolvedSessionId !== null
456
+ ? readSessionRouteHistory(routeHistoryResult.value, agent, resolvedSessionId, providerRecords)
211
457
  : [];
212
- const route = routeRead.route === null
213
- ? routeHistory[0]?.route ?? null
214
- : enrichRouteWithHistory(routeRead.route, routeHistory);
458
+ const route = routeHistory[0]?.route ?? null;
215
459
  const routeStatus = route !== null
216
460
  ? 'available'
217
- : routeRead.status;
461
+ : resolvedSessionId === null
462
+ ? 'session-unavailable'
463
+ : !includeRoute
464
+ ? 'not-requested'
465
+ : !canReadRoute || !routeHistoryValid
466
+ ? 'unavailable'
467
+ : 'no-route';
218
468
  const allQuotas = quotaResult.ok ? parseMagpieQuotas(quotaResult.value) : null;
219
469
  const routeProviders = [...new Set([
220
470
  providerFromRoute(route),
@@ -224,9 +474,15 @@ export class MagpieUsageService {
224
474
  ? routeProviders
225
475
  : [providerFromModelId(modelId, agent)].filter((provider) => provider !== null);
226
476
  const upstreamProvider = upstreamProviders[0] ?? null;
227
- const quotas = allQuotas === null
477
+ const sessionQuotas = allQuotas === null
228
478
  ? []
229
479
  : filterSessionQuotas(allQuotas, upstreamProviders, routeHistory);
480
+ const matchingQuotas = markSessionAccounts(sessionQuotas, routeHistory, route);
481
+ const quotas = allQuotas === null ? [] : await this.attachQuotaLogos(matchingQuotas, signal);
482
+ // 同厂商的其它账号单独下发:客户端分组展示,但绝不参与本会话余量。
483
+ const siblingQuotas = allQuotas === null
484
+ ? []
485
+ : await this.attachQuotaLogos(otherProviderAccounts(allQuotas, upstreamProviders, sessionQuotas), signal);
230
486
  return {
231
487
  agent,
232
488
  modelId,
@@ -234,6 +490,7 @@ export class MagpieUsageService {
234
490
  usageStatus: aggregate === null ? 'unavailable' : 'available',
235
491
  aggregate,
236
492
  sessionId,
493
+ ...(resolvedSessionId === null ? {} : { requestSessionId: resolvedSessionId }),
237
494
  session: sessionUsage,
238
495
  routeStatus,
239
496
  route,
@@ -241,8 +498,47 @@ export class MagpieUsageService {
241
498
  upstreamProviders,
242
499
  quotaStatus: allQuotas === null ? 'unavailable' : upstreamProviders.length === 0 ? 'provider-unavailable' : 'available',
243
500
  quotas,
501
+ ...(siblingQuotas.length === 0 ? {} : { providerAccounts: siblingQuotas }),
244
502
  };
245
503
  }
504
+ async attachQuotaLogos(quotas, signal) {
505
+ const requestAssetDataUrl = this.webApi?.requestAssetDataUrl;
506
+ if (typeof requestAssetDataUrl !== 'function')
507
+ return quotas;
508
+ const icons = [...new Set(quotas
509
+ .map((quota) => quota.icon)
510
+ .filter((icon) => icon !== undefined))].slice(0, MAX_MAGPIE_ICON_FETCHES);
511
+ if (icons.length === 0)
512
+ return quotas;
513
+ const logos = new Map();
514
+ await Promise.all(icons.map(async (icon) => {
515
+ const cached = this.quotaLogoCache.get(icon);
516
+ const logo = cached === undefined ? await this.readMagpieLogoDataUrl(icon, signal) : cached;
517
+ if (cached === undefined)
518
+ this.quotaLogoCache.set(icon, logo);
519
+ logos.set(icon, logo);
520
+ }));
521
+ return quotas.map((quota) => {
522
+ const logoDataUrl = quota.icon === undefined ? null : logos.get(quota.icon) ?? null;
523
+ return logoDataUrl === null ? quota : { ...quota, logoDataUrl };
524
+ });
525
+ }
526
+ async readMagpieLogoDataUrl(icon, signal) {
527
+ const requestAssetDataUrl = this.webApi?.requestAssetDataUrl;
528
+ if (typeof requestAssetDataUrl !== 'function')
529
+ return null;
530
+ for (const path of magpieIconAssetPaths(icon)) {
531
+ try {
532
+ const value = await requestAssetDataUrl.call(this.webApi, path, signal === undefined ? {} : { signal });
533
+ if (value !== null && value !== '')
534
+ return value;
535
+ }
536
+ catch {
537
+ signal?.throwIfAborted();
538
+ }
539
+ }
540
+ return null;
541
+ }
246
542
  async loadCatalog(agent, signal) {
247
543
  const cached = this.catalogs.get(agent);
248
544
  const cacheTtl = cached?.catalog === null ? MAGPIE_FAILURE_CACHE_TTL_MS : MAGPIE_CATALOG_TTL_MS;
@@ -264,7 +560,12 @@ export class MagpieUsageService {
264
560
  .catch(() => null);
265
561
  const [supported, catalog] = await Promise.all([supportedRequest, catalogRequest]);
266
562
  const result = supported ? catalog : null;
267
- this.catalogs.set(agent, { catalog: result, loadedAt: this.now() });
563
+ const unavailableIssue = result !== null
564
+ ? null
565
+ : !supported
566
+ ? this.versionCheck?.executableAvailable === true ? 'cli-version-unsupported' : 'cli-unavailable'
567
+ : 'model-catalog-unavailable';
568
+ this.catalogs.set(agent, { catalog: result, unavailableIssue, loadedAt: this.now() });
268
569
  return result;
269
570
  })
270
571
  .catch(() => null)
@@ -320,13 +621,13 @@ export class MagpieUsageService {
320
621
  if (this.versionLoad !== undefined)
321
622
  return awaitWithSignal(this.versionLoad, signal);
322
623
  const load = this.readVersion(this.lifetimeSignal)
323
- .then((value) => parseMagpieCliVersion(value))
324
- .then((supported) => {
325
- this.versionCheck = { supported, checkedAt: this.now() };
624
+ .then((value) => {
625
+ const supported = parseMagpieCliVersion(value);
626
+ this.versionCheck = { supported, executableAvailable: value !== null, checkedAt: this.now() };
326
627
  return supported;
327
628
  })
328
629
  .catch(() => {
329
- this.versionCheck = { supported: false, checkedAt: this.now() };
630
+ this.versionCheck = { supported: false, executableAvailable: false, checkedAt: this.now() };
330
631
  return false;
331
632
  })
332
633
  .finally(() => { if (this.versionLoad === load)
@@ -363,22 +664,29 @@ export class MagpieUsageService {
363
664
  return { ok: false };
364
665
  }
365
666
  }
366
- }
367
- function unavailableMagpieSnapshot(agent, modelId, sessionId, now, capturedAt = new Date(now).toISOString()) {
368
- return {
369
- agent,
370
- modelId,
371
- capturedAt,
372
- usageStatus: 'unavailable',
373
- aggregate: null,
374
- sessionId,
375
- session: null,
376
- routeStatus: sessionId === null ? 'session-unavailable' : 'unavailable',
377
- route: null,
378
- upstreamProvider: providerFromModelId(modelId, agent),
379
- quotaStatus: 'unavailable',
380
- quotas: [],
381
- };
667
+ async readWebJson(path, signal) {
668
+ if (this.webApi === undefined)
669
+ return { ok: false };
670
+ try {
671
+ return { ok: true, value: await this.webApi.requestJson(path, signal === undefined ? {} : { signal }) };
672
+ }
673
+ catch {
674
+ signal?.throwIfAborted();
675
+ return { ok: false };
676
+ }
677
+ }
678
+ /** Provider management data is the canonical source when a usage row mislabels its upstream. */
679
+ async readProviderCatalog(signal) {
680
+ const cached = this.providerCatalog;
681
+ if (cached !== undefined && this.now() - cached.loadedAt < MAGPIE_PROVIDER_CATALOG_TTL_MS)
682
+ return cached.records;
683
+ const response = await this.readWebJson('/api/providers', signal);
684
+ const parsed = response.ok ? parseMagpieWebProviders(response.value) : null;
685
+ const records = parsed?.records ?? [];
686
+ if (parsed !== null)
687
+ this.providerCatalog = { records, loadedAt: this.now() };
688
+ return records;
689
+ }
382
690
  }
383
691
  function awaitWithSignal(task, signal) {
384
692
  if (signal === undefined)
@@ -635,13 +943,142 @@ async function runMagpieCommand(args, signal) {
635
943
  }
636
944
  /**
637
945
  * Magpie IDs normally match adapter IDs. Keep naming differences explicit;
638
- * every result is still checked against Magpie's own catalog output before
639
- * any Gateway request is made.
946
+ * it is used for exact Agent identity matching in Magpie usage records.
640
947
  */
641
948
  export function magpieAgentForAdapter(adapterId) {
642
949
  const agent = MAGPIE_AGENT_BY_ADAPTER[adapterId];
643
950
  return agent !== undefined && isMagpieAgentId(agent) ? agent : null;
644
951
  }
952
+ /** Return the exact Magpie Agent identities accepted for one adapter. */
953
+ export function magpieUsageAgentAliases(adapterId) {
954
+ const agent = magpieAgentForAdapter(adapterId);
955
+ return agent === null ? [] : MAGPIE_USAGE_AGENT_ALIASES[agent] ?? [agent];
956
+ }
957
+ /**
958
+ * Enrich dashboard session rows only after an exact `(Agent, session ID)` match.
959
+ * Provider session IDs and DSH IDs are both candidates because Command Code and
960
+ * native DSH use different identities across Magpie versions.
961
+ */
962
+ export function attachMagpieSessionNames(dashboard, bindings) {
963
+ if (!Array.isArray(dashboard.sessions))
964
+ return dashboard;
965
+ const owners = new Map();
966
+ const ambiguous = new Set();
967
+ const ownerByGroup = new Map();
968
+ for (const binding of bindings) {
969
+ const dshSessionId = cleanText(binding.dshSessionId);
970
+ const name = cleanSessionName(binding.name);
971
+ if (dshSessionId === null)
972
+ continue;
973
+ // Magpie versions may record either the provider conversation ID or the
974
+ // DSH ID; both exact identities represent the same DSH session.
975
+ const providerSessionId = cleanText(binding.providerSessionId);
976
+ const ids = new Set([dshSessionId, providerSessionId].filter((value) => value !== null));
977
+ const groupKey = `dsh\u0000${dshSessionId}`;
978
+ const owner = ownerByGroup.get(groupKey) ?? {
979
+ groupKey,
980
+ dshSessionId,
981
+ agent: magpieAgentForAdapter(binding.adapterId) ?? binding.adapterId,
982
+ name,
983
+ ids: [...new Set(ids)],
984
+ };
985
+ if (ownerByGroup.has(groupKey)) {
986
+ ownerByGroup.set(groupKey, {
987
+ ...owner,
988
+ name: owner.name === null || name === null || owner.name === name ? owner.name ?? name : null,
989
+ ids: [...new Set([...owner.ids, ...ids])],
990
+ });
991
+ }
992
+ else {
993
+ ownerByGroup.set(groupKey, owner);
994
+ }
995
+ const currentOwner = ownerByGroup.get(groupKey);
996
+ for (const agent of magpieUsageAgentAliases(binding.adapterId)) {
997
+ for (const id of ids) {
998
+ const key = `${agent}\u0000${id}`;
999
+ if (ambiguous.has(key))
1000
+ continue;
1001
+ const previous = owners.get(key);
1002
+ if (previous !== undefined && previous.groupKey !== groupKey) {
1003
+ owners.delete(key);
1004
+ ambiguous.add(key);
1005
+ }
1006
+ else {
1007
+ owners.set(key, currentOwner);
1008
+ }
1009
+ }
1010
+ }
1011
+ }
1012
+ for (const [key, owner] of owners) {
1013
+ const currentOwner = ownerByGroup.get(owner.groupKey);
1014
+ if (currentOwner !== undefined)
1015
+ owners.set(key, currentOwner);
1016
+ }
1017
+ const grouped = new Map();
1018
+ for (const row of dashboard.sessions) {
1019
+ const aliasKey = `${row.agent}\u0000${row.id}`;
1020
+ const owner = ambiguous.has(aliasKey) ? undefined : owners.get(aliasKey);
1021
+ const groupKey = owner?.groupKey ?? `raw\u0000${row.agent}\u0000${row.id}`;
1022
+ const current = grouped.get(groupKey);
1023
+ if (current === undefined) {
1024
+ const ids = new Set(owner?.ids ?? [row.id]);
1025
+ ids.add(row.id);
1026
+ const session = owner === undefined
1027
+ ? row
1028
+ : {
1029
+ ...row,
1030
+ id: owner.dshSessionId,
1031
+ agent: owner.agent,
1032
+ ...(owner.name === null ? {} : { dshSessionName: owner.name }),
1033
+ sessionIds: [...ids],
1034
+ };
1035
+ grouped.set(groupKey, { session, ids });
1036
+ continue;
1037
+ }
1038
+ for (const id of owner?.ids ?? [row.id])
1039
+ current.ids.add(id);
1040
+ current.ids.add(row.id);
1041
+ current.session = mergeMagpieUsageSessions(current.session, row, owner);
1042
+ }
1043
+ const sessions = [...grouped.values()].map(({ session, ids }) => {
1044
+ if (ids.size <= 1 && session.sessionIds === undefined)
1045
+ return session;
1046
+ return { ...session, sessionIds: [...ids] };
1047
+ });
1048
+ if (sessions.length === dashboard.sessions.length && sessions.every((session, index) => session === dashboard.sessions[index]))
1049
+ return dashboard;
1050
+ return { ...dashboard, sessions };
1051
+ }
1052
+ function mergeMagpieUsageSessions(current, row, owner) {
1053
+ return {
1054
+ ...current,
1055
+ id: owner?.dshSessionId ?? current.id,
1056
+ agent: owner?.agent ?? current.agent,
1057
+ calls: sumMagpieUsageMetric(current.calls, row.calls),
1058
+ errors: sumMagpieUsageMetric(current.errors, row.errors),
1059
+ retried: sumMagpieUsageMetric(current.retried, row.retried),
1060
+ inputTokens: sumMagpieUsageMetric(current.inputTokens, row.inputTokens),
1061
+ outputTokens: sumMagpieUsageMetric(current.outputTokens, row.outputTokens),
1062
+ cacheReadTokens: sumMagpieUsageMetric(current.cacheReadTokens, row.cacheReadTokens),
1063
+ cacheWriteTokens: sumMagpieUsageMetric(current.cacheWriteTokens, row.cacheWriteTokens),
1064
+ reasoningTokens: sumMagpieUsageMetric(current.reasoningTokens, row.reasoningTokens),
1065
+ cost: sumMagpieUsageMetric(current.cost, row.cost),
1066
+ unpriced: sumMagpieUsageMetric(current.unpriced, row.unpriced),
1067
+ credits: sumMagpieUsageMetric(current.credits, row.credits),
1068
+ credited: sumMagpieUsageMetric(current.credited, row.credited),
1069
+ timed: sumMagpieUsageMetric(current.timed, row.timed),
1070
+ ttftMs: sumMagpieUsageMetric(current.ttftMs, row.ttftMs),
1071
+ decodeMs: sumMagpieUsageMetric(current.decodeMs, row.decodeMs),
1072
+ decodeTokens: sumMagpieUsageMetric(current.decodeTokens, row.decodeTokens),
1073
+ decodeCount: sumMagpieUsageMetric(current.decodeCount, row.decodeCount),
1074
+ ...(current.dshSessionName === undefined && owner?.name !== null && owner?.name !== undefined
1075
+ ? { dshSessionName: owner.name }
1076
+ : {}),
1077
+ };
1078
+ }
1079
+ function sumMagpieUsageMetric(left, right) {
1080
+ return left === null && right === null ? null : (left ?? 0) + (right ?? 0);
1081
+ }
645
1082
  function isMagpieAgentId(value) {
646
1083
  return value.length <= 64 && /^[a-z0-9][a-z0-9-]*$/u.test(value);
647
1084
  }
@@ -728,48 +1165,8 @@ function readSession(value, agent, sessionId) {
728
1165
  const usageAgent = cleanText(record?.agent);
729
1166
  return record !== null && usageAgent !== null && totals !== null ? { id: sessionId, agent: usageAgent, ...totals } : null;
730
1167
  }
731
- function readRoute(value, expectedSessionId) {
732
- const envelope = asRecord(value);
733
- if (envelope === null || cleanText(envelope.session) !== expectedSessionId || !('route' in envelope))
734
- return { status: 'unavailable', route: null };
735
- if (envelope.route === null)
736
- return { status: 'no-route', route: null };
737
- const record = asRecord(envelope.route);
738
- if (record === null)
739
- return { status: 'unavailable', route: null };
740
- const tries = Array.isArray(record.tries)
741
- ? record.tries.slice(0, 20).flatMap((item) => {
742
- const attempt = asRecord(item);
743
- if (attempt === null)
744
- return [];
745
- const model = routeText(attempt.model);
746
- const provider = routeText(attempt.provider);
747
- return [{
748
- model: model !== null && provider !== null && !model.includes('/') ? `${provider}/${model}` : model ?? provider,
749
- failed: routeText(attempt.fail) !== null,
750
- }];
751
- })
752
- : [];
753
- return {
754
- status: 'available',
755
- route: {
756
- asked: routeText(record.asked),
757
- group: routeText(record.group),
758
- rule: readRule(record.rule),
759
- model: routeText(record.model),
760
- effort: routeText(record.effort),
761
- tries,
762
- done: typeof record.done === 'boolean' ? record.done : null,
763
- status: routeStatusText(record.status),
764
- served: routeText(record.served),
765
- routeId: safeRouteId(record.id),
766
- time: routeText(record.time),
767
- providerAccount: routeText(record.providerAccount),
768
- },
769
- };
770
- }
771
1168
  /** Read the durable request ledger when Magpie's short in-memory trace has expired. */
772
- function readSessionRouteHistory(value, agent, sessionId) {
1169
+ function readSessionRouteHistory(value, agent, sessionId, providerRecords = []) {
773
1170
  const rows = asRecord(value)?.rows;
774
1171
  if (!Array.isArray(rows) || rows.length > 500)
775
1172
  return [];
@@ -779,9 +1176,11 @@ function readSessionRouteHistory(value, agent, sessionId) {
779
1176
  const rowAgent = cleanText(record?.agent);
780
1177
  const session = cleanText(record?.session);
781
1178
  const nativeSession = cleanText(record?.native_session);
782
- const provider = routeText(record?.provider);
783
1179
  if (record === null || rowAgent === null || !usageAgents.includes(rowAgent)
784
- || (session !== sessionId && nativeSession !== sessionId) || provider === null)
1180
+ || (session !== sessionId && nativeSession !== sessionId))
1181
+ return [];
1182
+ const provider = normalizeMagpieRequestProvider(record, providerRecords);
1183
+ if (provider === null)
785
1184
  return [];
786
1185
  const requested = routeText(record.req);
787
1186
  const model = routeText(record.model);
@@ -814,18 +1213,19 @@ function routeTime(value) {
814
1213
  const time = Date.parse(value);
815
1214
  return Number.isFinite(time) ? time : 0;
816
1215
  }
817
- function enrichRouteWithHistory(route, history) {
818
- if (route.routeId === undefined || route.routeId === null)
819
- return route;
820
- const record = history.find((item) => item.route.routeId === route.routeId);
821
- if (record === undefined)
822
- return route;
823
- return {
824
- ...route,
825
- time: route.time ?? record.route.time ?? null,
826
- providerAccount: route.providerAccount ?? record.account,
827
- };
828
- }
1216
+ /**
1217
+ * 选出本会话确实使用过的上游账号配额。
1218
+ *
1219
+ * 归属必须严格:请求历史里没有出现的账号不能算作本会话的配额。
1220
+ *
1221
+ * - 账号维度能对上:只留命中的账号。
1222
+ * - 上游不提供账号名(Codex 这类量贩账号没有 `user`),无从按账号筛选:保留该厂商
1223
+ * 全部候选。过去这里返回空数组,`quotaStatus` 仍是 `available`,界面就只剩下
1224
+ * "上游没有报告配额窗口"的空态 —— 明明识别出了 `glorinfo-api-platform`,却看不到
1225
+ * 它的额度。
1226
+ * - 有账号记录但一条都不匹配:返回空,宁可显示"没有可用配额"也不能把别的账号的额度
1227
+ * 说成本会话的。
1228
+ */
829
1229
  function filterSessionQuotas(quotas, providers, history) {
830
1230
  return providers.flatMap((provider) => {
831
1231
  const candidates = quotas.filter((quota) => quota.provider === provider);
@@ -834,7 +1234,53 @@ function filterSessionQuotas(quotas, providers, history) {
834
1234
  .map((record) => record.account));
835
1235
  if (accounts.size === 0)
836
1236
  return candidates;
837
- return candidates.filter((quota) => quota.user !== null && accounts.has(quota.user));
1237
+ const matched = candidates.filter((quota) => quota.user !== null && accounts.has(quota.user));
1238
+ return matched.length > 0 ? matched : candidates.filter((quota) => quota.user === null);
1239
+ });
1240
+ }
1241
+ /**
1242
+ * 与本会话同一厂商、但本会话没用过的其它账号。
1243
+ *
1244
+ * 单独返回而不是并进 `quotas`:客户端用它们做"同厂商账号分组"的补充展示,
1245
+ * 但这些账号的额度绝不参与本会话余量判断。顺序沿用上游。
1246
+ */
1247
+ function otherProviderAccounts(quotas, providers, sessionQuotas) {
1248
+ const claimed = new Set(sessionQuotas.map((quota) => `${quota.provider}\u0000${quota.user ?? ''}`));
1249
+ return providers.flatMap((provider) => quotas
1250
+ .filter((quota) => quota.provider === provider)
1251
+ .filter((quota) => quota.user !== null && !claimed.has(`${quota.provider}\u0000${quota.user}`)));
1252
+ }
1253
+ /**
1254
+ * 标记本会话实际使用过的账号。
1255
+ *
1256
+ * 只有能追溯到会话请求记录的账号才标 `active`;route 的账号字段优先于请求历史,
1257
+ * 因为它来自 Magpie 对这一次请求的精确归属。
1258
+ *
1259
+ * 账号名比对必须同时限定厂商:不同厂商的账号名会重名(WorkBuddy 与 WorkBuddy AI 都有
1260
+ * `zj59122704501@gmail.com`),只比名字会把别的厂商的账号标成本会话正在使用。
1261
+ * 无法确认时保持未标记,客户端据此显示"未知活动账号",而不是随便挑一个账号当"正在使用"。
1262
+ */
1263
+ function markSessionAccounts(quotas, history, route) {
1264
+ const used = new Set(history
1265
+ .filter((record) => record.account !== null)
1266
+ .map((record) => `${record.provider}\u0000${record.account}`));
1267
+ const routedProvider = providerFromRoute(route);
1268
+ const routedAccount = route?.providerAccount ?? null;
1269
+ const routedKey = routedAccount === null || routedProvider === null ? null : `${routedProvider}\u0000${routedAccount}`;
1270
+ if (used.size === 0 && routedKey === null)
1271
+ return quotas;
1272
+ // 每个厂商只承认一个活动账号:精确路由优先,其次是首个命中的历史账号。
1273
+ const claimed = new Set();
1274
+ return quotas.map((quota) => {
1275
+ if (quota.user === null)
1276
+ return quota;
1277
+ const key = `${quota.provider}\u0000${quota.user}`;
1278
+ if (claimed.has(key))
1279
+ return quota;
1280
+ if (key !== routedKey && !used.has(key))
1281
+ return quota;
1282
+ claimed.add(key);
1283
+ return { ...quota, active: true };
838
1284
  });
839
1285
  }
840
1286
  function safeRouteId(value) {
@@ -866,6 +1312,7 @@ export function parseMagpieQuotas(value) {
866
1312
  quotas.push({
867
1313
  provider,
868
1314
  name,
1315
+ ...optionalQuotaIcon(record.icon),
869
1316
  kind: routeText(record.kind),
870
1317
  user: routeText(record.user),
871
1318
  plan: routeText(record.plan),
@@ -873,22 +1320,519 @@ export function parseMagpieQuotas(value) {
873
1320
  balance: routeText(record.balance),
874
1321
  readAt: routeText(record.readAt),
875
1322
  windows,
1323
+ ...sessionQuotaDaily(record),
1324
+ ...readQuotaAccountMeta(record),
876
1325
  });
877
1326
  }
878
1327
  return quotas;
879
1328
  }
1329
+ /** Normalize the live aggregate and per-dimension usage response. */
1330
+ export function parseMagpieDashboardUsage(value) {
1331
+ const root = asRecord(value);
1332
+ if (root === null)
1333
+ return null;
1334
+ const hasMetrics = ['calls', 'errors', 'input', 'output', 'cache_read', 'cache_write', 'cost'].some((key) => key in root);
1335
+ if (!hasMetrics && !Array.isArray(root.series) && !Array.isArray(root.sessions))
1336
+ return null;
1337
+ const series = readMagpieSeries(root.series);
1338
+ const agents = readMagpieUsageGroups(root.agents, 'agent');
1339
+ const providers = readMagpieUsageGroups(root.providerKeys ?? root.providers, 'provider');
1340
+ const models = readMagpieUsageGroups(root.models, 'model');
1341
+ const accounts = readMagpieUsageGroups(root.accounts, 'account');
1342
+ const sessions = readMagpieUsageSessions(root.sessions);
1343
+ if (series === null || agents === null || providers === null || models === null || accounts === null || sessions === null)
1344
+ return null;
1345
+ return {
1346
+ aggregate: readMagpieUsageMetrics(root),
1347
+ series,
1348
+ agents,
1349
+ providers,
1350
+ models,
1351
+ accounts,
1352
+ sessions,
1353
+ sessionCount: safeCount(root.sessionCount),
1354
+ };
1355
+ }
1356
+ /** Normalize quota cards while preserving nullable values and bounded history. */
1357
+ export function parseMagpieQuotaCards(value) {
1358
+ const root = asRecord(value);
1359
+ const rows = Array.isArray(value) ? value : Array.isArray(root?.data) ? root.data : null;
1360
+ if (rows === null || rows.length > 512)
1361
+ return null;
1362
+ const quotas = [];
1363
+ for (const row of rows) {
1364
+ const record = asRecord(row);
1365
+ const provider = routeText(record?.provider);
1366
+ const name = routeText(record?.name);
1367
+ if (record === null || provider === null || name === null
1368
+ || !Array.isArray(record.windows) || record.windows.length > 64)
1369
+ return null;
1370
+ const windows = record.windows.flatMap((item) => {
1371
+ const window = asRecord(item);
1372
+ const windowName = routeText(window?.name);
1373
+ if (window === null || windowName === null)
1374
+ return [];
1375
+ return [{
1376
+ name: windowName,
1377
+ usedPercent: quotaUsedPercent(window),
1378
+ remainingPercent: quotaRemainingPercent(window),
1379
+ resetsAt: routeText(window.resetsAt),
1380
+ display: routeText(window.display),
1381
+ amount: safeDecimal(window.amount),
1382
+ limit: safeDecimal(window.limit),
1383
+ unit: routeText(window.unit),
1384
+ holdsWhy: routeText(window.holdsWhy),
1385
+ runsOutAt: routeText(window.runsOutAt),
1386
+ runsOut: safeDecimal(window.runsOut),
1387
+ }];
1388
+ });
1389
+ if (windows.length !== record.windows.length)
1390
+ return null;
1391
+ const dailyRecord = record.daily === undefined ? null : asRecord(record.daily);
1392
+ const dailyPoints = dailyRecord === null ? [] : dailyRecord.days;
1393
+ if (dailyRecord !== null && (!Array.isArray(dailyPoints) || dailyPoints.length > 366))
1394
+ return null;
1395
+ const days = (Array.isArray(dailyPoints) ? dailyPoints : []).flatMap((item) => {
1396
+ const point = asRecord(item);
1397
+ const day = routeText(point?.day);
1398
+ if (point === null || day === null)
1399
+ return [];
1400
+ const used = safeDecimal(point.used);
1401
+ return [{ day, used }];
1402
+ });
1403
+ if (days.length !== (Array.isArray(dailyPoints) ? dailyPoints.length : 0))
1404
+ return null;
1405
+ quotas.push({
1406
+ provider,
1407
+ name,
1408
+ ...optionalQuotaIcon(record.icon),
1409
+ kind: routeText(record.kind),
1410
+ user: routeText(record.user),
1411
+ plan: routeText(record.plan),
1412
+ alias: routeText(record.alias),
1413
+ balance: routeText(record.balance),
1414
+ readAt: routeText(record.readAt),
1415
+ windows,
1416
+ daily: { since: routeText(dailyRecord?.since), days },
1417
+ ...readQuotaAccountMeta(record),
1418
+ });
1419
+ }
1420
+ return quotas;
1421
+ }
1422
+ export function parseMagpieQuotaHistory(value) {
1423
+ const root = asRecord(value);
1424
+ const rows = Array.isArray(value) ? value : Array.isArray(root?.data) ? root.data : null;
1425
+ if (rows === null || rows.length > 512)
1426
+ return null;
1427
+ const history = [];
1428
+ for (const row of rows) {
1429
+ const record = asRecord(row);
1430
+ const provider = routeText(record?.provider);
1431
+ if (record === null || provider === null || !Array.isArray(record.lines) || record.lines.length > 64)
1432
+ return null;
1433
+ const lines = record.lines.flatMap((item) => {
1434
+ const line = asRecord(item);
1435
+ const name = routeText(line?.name);
1436
+ if (line === null || name === null || !Array.isArray(line.points) || line.points.length > 2_000)
1437
+ return [];
1438
+ const points = line.points.flatMap((pointValue) => {
1439
+ const point = asRecord(pointValue);
1440
+ const at = routeText(point?.at);
1441
+ return point === null || at === null ? [] : [{
1442
+ at,
1443
+ left: safeDecimal(point.left),
1444
+ start: routeText(point.start),
1445
+ resetsAt: routeText(point.resetsAt),
1446
+ }];
1447
+ });
1448
+ if (points.length !== line.points.length)
1449
+ return [];
1450
+ return [{ name, points }];
1451
+ });
1452
+ if (lines.length !== record.lines.length)
1453
+ return null;
1454
+ history.push({ provider, user: routeText(record.user), lines });
1455
+ }
1456
+ return history;
1457
+ }
1458
+ function readMagpieUsageMetrics(record) {
1459
+ return {
1460
+ calls: safeCount(record.calls),
1461
+ errors: safeCount(record.errors),
1462
+ retried: safeCount(record.retried),
1463
+ inputTokens: safeCount(record.input),
1464
+ outputTokens: safeCount(record.output),
1465
+ cacheReadTokens: safeCount(record.cache_read),
1466
+ cacheWriteTokens: safeCount(record.cache_write),
1467
+ reasoningTokens: safeCount(record.reasoning),
1468
+ cost: safeDecimal(record.cost),
1469
+ unpriced: safeDecimal(record.unpriced),
1470
+ credits: safeDecimal(record.credits),
1471
+ credited: safeDecimal(record.credited),
1472
+ timed: safeCount(record.timed),
1473
+ ttftMs: safeDecimal(record.ttft_ms),
1474
+ decodeMs: safeDecimal(record.decode_ms),
1475
+ decodeTokens: safeCount(record.decode_out),
1476
+ decodeCount: safeCount(record.decode_n),
1477
+ };
1478
+ }
1479
+ function readMagpieSeries(value) {
1480
+ if (value === undefined)
1481
+ return [];
1482
+ if (!Array.isArray(value) || value.length > 2_000)
1483
+ return null;
1484
+ return value.flatMap((item) => {
1485
+ const record = asRecord(item);
1486
+ return record === null ? [] : [{
1487
+ time: routeText(record.time),
1488
+ label: routeText(record.label),
1489
+ ...readMagpieUsageMetrics(record),
1490
+ }];
1491
+ });
1492
+ }
1493
+ function readMagpieUsageGroups(value, dimension) {
1494
+ if (value === undefined)
1495
+ return [];
1496
+ if (!Array.isArray(value) || value.length > 2_000)
1497
+ return null;
1498
+ const idKeys = {
1499
+ agent: ['id', 'agent'],
1500
+ provider: ['provider', 'id'],
1501
+ model: ['model', 'id'],
1502
+ account: ['account', 'id'],
1503
+ };
1504
+ const groups = value.flatMap((item) => {
1505
+ const record = asRecord(item);
1506
+ if (record === null)
1507
+ return [];
1508
+ const id = firstRouteText(record, idKeys[dimension]);
1509
+ if (id === null)
1510
+ return [];
1511
+ return [{
1512
+ id,
1513
+ name: routeText(record.name),
1514
+ provider: routeText(record.provider),
1515
+ model: routeText(record.model),
1516
+ account: routeText(record.account),
1517
+ agent: routeText(record.agent ?? record.id),
1518
+ ...readMagpieUsageMetrics(record),
1519
+ }];
1520
+ });
1521
+ if (dimension !== 'provider')
1522
+ return groups;
1523
+ const byProvider = new Map();
1524
+ for (const group of groups) {
1525
+ const current = byProvider.get(group.id);
1526
+ if (current === undefined) {
1527
+ byProvider.set(group.id, group);
1528
+ continue;
1529
+ }
1530
+ byProvider.set(group.id, {
1531
+ ...current,
1532
+ calls: sumNullable(current.calls, group.calls),
1533
+ errors: sumNullable(current.errors, group.errors),
1534
+ retried: sumNullable(current.retried, group.retried),
1535
+ inputTokens: sumNullable(current.inputTokens, group.inputTokens),
1536
+ outputTokens: sumNullable(current.outputTokens, group.outputTokens),
1537
+ cacheReadTokens: sumNullable(current.cacheReadTokens, group.cacheReadTokens),
1538
+ cacheWriteTokens: sumNullable(current.cacheWriteTokens, group.cacheWriteTokens),
1539
+ reasoningTokens: sumNullable(current.reasoningTokens, group.reasoningTokens),
1540
+ cost: sumNullable(current.cost, group.cost),
1541
+ unpriced: sumNullable(current.unpriced, group.unpriced),
1542
+ credits: sumNullable(current.credits, group.credits),
1543
+ credited: sumNullable(current.credited, group.credited),
1544
+ timed: sumNullable(current.timed, group.timed),
1545
+ ttftMs: null,
1546
+ decodeMs: null,
1547
+ decodeTokens: sumNullable(current.decodeTokens, group.decodeTokens),
1548
+ decodeCount: sumNullable(current.decodeCount, group.decodeCount),
1549
+ });
1550
+ }
1551
+ return [...byProvider.values()];
1552
+ }
1553
+ function readMagpieUsageSessions(value) {
1554
+ if (value === undefined)
1555
+ return [];
1556
+ if (!Array.isArray(value) || value.length > 2_000)
1557
+ return null;
1558
+ return value.flatMap((item) => {
1559
+ const record = asRecord(item);
1560
+ const id = cleanText(record?.id);
1561
+ const agent = cleanText(record?.agent);
1562
+ return record === null || id === null || agent === null ? [] : [{ id, agent, ...readMagpieUsageMetrics(record) }];
1563
+ });
1564
+ }
1565
+ function parseMagpieRequestRows(rows, sessionId, sessionAgent, providerRecords = [], sessionIds = sessionId === null ? [] : [sessionId]) {
1566
+ const acceptedAgents = sessionAgent === null ? null : MAGPIE_USAGE_AGENT_ALIASES[sessionAgent] ?? [sessionAgent];
1567
+ const acceptedSessionIds = new Set(sessionIds);
1568
+ return rows.flatMap((row) => {
1569
+ const record = asRecord(row);
1570
+ if (record === null)
1571
+ return [];
1572
+ const candidateSession = cleanText(record.session);
1573
+ const nativeSession = cleanText(record.native_session);
1574
+ const agent = cleanText(record.agent);
1575
+ if (sessionId !== null
1576
+ && (!acceptedSessionIds.has(candidateSession ?? '') && !acceptedSessionIds.has(nativeSession ?? '')
1577
+ || agent === null || acceptedAgents === null || !acceptedAgents.includes(agent)))
1578
+ return [];
1579
+ const item = parseMagpieRequest(record, providerRecords);
1580
+ return item === null ? [] : [sessionId === null ? item : { ...item, sessionId }];
1581
+ });
1582
+ }
1583
+ function magpieRequestRowKey(value) {
1584
+ const record = asRecord(value);
1585
+ const routeId = safeRouteId(record?.route_id);
1586
+ return routeId === null ? JSON.stringify(record) : `route:${routeId}`;
1587
+ }
1588
+ function parseMagpieRequest(record, providerRecords = []) {
1589
+ const agent = routeText(record.agent);
1590
+ const provider = normalizeMagpieRequestProvider(record, providerRecords);
1591
+ const requestedModel = routeText(record.req);
1592
+ const servedModel = routeText(record.served);
1593
+ const group = requestedModel?.startsWith('group/') === true ? requestedModel.slice('group/'.length) : null;
1594
+ return {
1595
+ routeId: safeRouteId(record.route_id),
1596
+ time: routeText(record.t),
1597
+ agent,
1598
+ provider,
1599
+ account: normalizeMagpieRequestAccount(record, provider, providerRecords),
1600
+ requestedModel,
1601
+ servedModel,
1602
+ group,
1603
+ effort: routeText(record.effort),
1604
+ inputTokens: safeCount(record.in),
1605
+ outputTokens: safeCount(record.out),
1606
+ cacheReadTokens: safeCount(record.cache_read),
1607
+ cacheWriteTokens: safeCount(record.cache_write),
1608
+ reasoningTokens: safeCount(record.reasoning),
1609
+ cost: safeDecimal(record.cost),
1610
+ priced: typeof record.priced === 'boolean' ? record.priced : null,
1611
+ status: routeStatusText(record.status),
1612
+ durationMs: safeDecimal(record.ms),
1613
+ ttftMs: safeDecimal(record.ttft_ms),
1614
+ firstTextMs: safeDecimal(record.first_text_ms),
1615
+ firstReasoningMs: safeDecimal(record.first_reasoning_ms),
1616
+ flowMs: safeDecimal(record.flow_ms),
1617
+ sessionId: cleanText(record.session) ?? cleanText(record.native_session),
1618
+ };
1619
+ }
1620
+ /** Session-filtered rows can omit the account label; use only the matching provider's safe catalog label. */
1621
+ function normalizeMagpieRequestAccount(record, provider, providerRecords) {
1622
+ const account = routeText(record.providerAccount);
1623
+ if (account !== null || provider === null)
1624
+ return account;
1625
+ const matches = providerRecords.filter((item) => item.id === provider);
1626
+ return matches.length === 1 ? matches[0].account : null;
1627
+ }
1628
+ /**
1629
+ * Magpie's usage ledger can retain a stale provider id after an Agent provider
1630
+ * is renamed. Reconcile only Codex rows against the read-only provider catalog;
1631
+ * arbitrary provider ids must remain untouched when the evidence is ambiguous.
1632
+ */
1633
+ function normalizeMagpieRequestProvider(record, providerRecords) {
1634
+ const provider = routeText(record.provider);
1635
+ if (provider === null || routeText(record.agent)?.toLocaleLowerCase() !== 'codex')
1636
+ return provider;
1637
+ const codexProviders = providerRecords.filter((item) => item.accountAgent?.toLocaleLowerCase() === 'codex');
1638
+ if (codexProviders.length === 0)
1639
+ return provider;
1640
+ const modelValues = [record.model, record.served, record.req]
1641
+ .map(routeText)
1642
+ .filter((value) => value !== null);
1643
+ const matches = codexProviders.filter((candidate) => candidate.models.some((model) => modelValues.some((value) => magpieModelMatchesProvider(value, model.id, candidate.id))));
1644
+ if (matches.length === 1)
1645
+ return matches[0].id;
1646
+ return codexProviders.length === 1 ? codexProviders[0].id : provider;
1647
+ }
1648
+ function magpieModelMatchesProvider(value, modelId, providerId) {
1649
+ if (value === modelId)
1650
+ return true;
1651
+ const prefix = `${providerId}/`;
1652
+ return modelId.startsWith(prefix) && value === modelId.slice(prefix.length);
1653
+ }
1654
+ function firstRouteText(record, keys) {
1655
+ for (const key of keys) {
1656
+ const value = routeText(record[key]);
1657
+ if (value !== null)
1658
+ return value;
1659
+ }
1660
+ return null;
1661
+ }
1662
+ function sumNullable(left, right) {
1663
+ return left === null && right === null ? null : (left ?? 0) + (right ?? 0);
1664
+ }
1665
+ function isMagpieDashboardPeriod(value) {
1666
+ return value === 'today' || value === 'week' || value === 'month' || value === 'all';
1667
+ }
1668
+ function magpieWebPeriod(period) {
1669
+ switch (period) {
1670
+ case 'today': return 'today';
1671
+ case 'week': return '7d';
1672
+ case 'month': return '30d';
1673
+ case 'all': return 'all';
1674
+ }
1675
+ }
1676
+ function emptyMagpieDashboard(period, capturedAt) {
1677
+ return {
1678
+ period,
1679
+ capturedAt,
1680
+ availabilityIssue: null,
1681
+ usageStatus: 'unavailable',
1682
+ aggregate: null,
1683
+ series: [],
1684
+ agents: [],
1685
+ providers: [],
1686
+ models: [],
1687
+ accounts: [],
1688
+ sessions: [],
1689
+ sessionCount: null,
1690
+ quotaStatus: 'unavailable',
1691
+ quotas: [],
1692
+ quotaHistoryStatus: 'unavailable',
1693
+ quotaHistory: [],
1694
+ };
1695
+ }
1696
+ /**
1697
+ * 窗口使用的百分比。
1698
+ *
1699
+ * Magpie 的 `used` 始终是 0–100 的百分比(见其 `auth.usage` 契约),但 `remaining`
1700
+ * 不一定:同一份数据里既有 `remaining: 96` 这种百分比,也有 `remaining: 86.57`
1701
+ * 配 `amount: 322.25 / limit: 2400` 这种"剩余量"。凡是带 `limit`(或 `amount`)的窗口,
1702
+ * `remaining` 一律按数量解释,不能直接当成 0–100 的百分比,否则 WorkBuddy 的
1703
+ * `322 / 2400` 会被读成"剩余 86.57%",和 `display` 里的数量对不上。
1704
+ */
1705
+ function quotaUsedPercent(record) {
1706
+ return safePercent(record.used);
1707
+ }
1708
+ function quotaRemainingPercent(record) {
1709
+ const counted = safeDecimal(record.limit) !== null || safeDecimal(record.amount) !== null;
1710
+ if (counted) {
1711
+ const used = quotaUsedPercent(record);
1712
+ return used === null ? null : 100 - used;
1713
+ }
1714
+ return safePercent(record.remaining);
1715
+ }
880
1716
  function readQuotaWindow(record, name) {
881
1717
  return {
882
1718
  name,
883
- usedPercent: safePercent(record.used),
884
- remainingPercent: safePercent(record.remaining),
1719
+ usedPercent: quotaUsedPercent(record),
1720
+ remainingPercent: quotaRemainingPercent(record),
885
1721
  resetsAt: routeText(record.resetsAt),
886
1722
  display: routeText(record.display),
887
1723
  amount: safeDecimal(record.amount),
888
1724
  limit: safeDecimal(record.limit),
889
1725
  unit: routeText(record.unit),
890
1726
  holdsWhy: routeText(record.holdsWhy),
1727
+ startsAt: routeText(record.startsAt),
1728
+ runsOut: safeDecimal(record.runsOut),
1729
+ runsOutAt: routeText(record.runsOutAt),
1730
+ };
1731
+ }
1732
+ /**
1733
+ * Read the daily check-in result of providers that have one (WorkBuddy, Trae, MiniMax).
1734
+ *
1735
+ * `outcome` stays an upstream string: the client only localizes the outcomes it knows, so a
1736
+ * new vendor state never gets presented as a successful check-in.
1737
+ */
1738
+ function readQuotaCheckin(value) {
1739
+ const record = asRecord(value);
1740
+ if (record === null)
1741
+ return null;
1742
+ const checkin = {
1743
+ day: routeText(record.day),
1744
+ at: routeText(record.at),
1745
+ outcome: routeText(record.outcome),
1746
+ credit: safeDecimal(record.credit),
1747
+ streak: safeCount(record.streak),
891
1748
  };
1749
+ return Object.values(checkin).every((item) => item === null) ? null : checkin;
1750
+ }
1751
+ function readQuotaDaily(value) {
1752
+ const record = asRecord(value);
1753
+ if (record === null)
1754
+ return undefined;
1755
+ const points = record.days;
1756
+ if (!Array.isArray(points) || points.length > 366)
1757
+ return undefined;
1758
+ const days = points.flatMap((item) => {
1759
+ const point = asRecord(item);
1760
+ const day = routeText(point?.day);
1761
+ return point === null || day === null ? [] : [{ day, used: safeDecimal(point.used) }];
1762
+ });
1763
+ if (days.length !== points.length)
1764
+ return undefined;
1765
+ return { since: routeText(record.since), days };
1766
+ }
1767
+ /**
1768
+ * Read a provider's remaining reset cards without exposing the vendor's raw record.
1769
+ *
1770
+ * `count` 缺失与 0 不同:上游在 0 张时会省略整个对象,因此这里只在对象存在时才返回,
1771
+ * 客户端把缺失显示为"未知"而不是"没有可用重置卡"。
1772
+ */
1773
+ function readQuotaResets(value) {
1774
+ const record = asRecord(value);
1775
+ if (record === null)
1776
+ return null;
1777
+ const eachPoints = record.each;
1778
+ const each = Array.isArray(eachPoints)
1779
+ ? eachPoints.flatMap((item) => {
1780
+ const point = asRecord(item);
1781
+ if (point === null)
1782
+ return [];
1783
+ return [{ window: routeText(point.window), until: routeText(point.until) }];
1784
+ }).slice(0, 64)
1785
+ : [];
1786
+ return {
1787
+ count: safeCount(record.count),
1788
+ until: routeText(record.until),
1789
+ each,
1790
+ byWindow: record.byWindow === true,
1791
+ fiveHour: safeCount(record.fiveHour),
1792
+ weekly: safeCount(record.weekly),
1793
+ };
1794
+ }
1795
+ /** 会话级配额卡只带上 `daily`;上游缺字段时整个属性不出现。 */
1796
+ function sessionQuotaDaily(record) {
1797
+ const daily = readQuotaDaily(record.daily);
1798
+ return daily === undefined ? {} : { daily };
1799
+ }
1800
+ /**
1801
+ * Read the account-level fields shared by both quota parsers.
1802
+ *
1803
+ * `/api/usage/quotas` (session popover) and the dashboard read the same rows; keeping these
1804
+ * fields in one place stops the two views drifting apart. Absent values stay `undefined` so
1805
+ * older Magpie builds keep working.
1806
+ */
1807
+ function readQuotaAccountMeta(record) {
1808
+ const checkin = readQuotaCheckin(record.checkin);
1809
+ const resets = readQuotaResets(record.resets);
1810
+ return {
1811
+ ...(typeof record.active === 'boolean' ? { active: record.active } : {}),
1812
+ ...(checkin === null ? {} : { checkin }),
1813
+ ...(record.checkins === true ? { checkins: true } : {}),
1814
+ error: routeText(record.error),
1815
+ until: routeText(record.until),
1816
+ renew: routeText(record.renew),
1817
+ ...(resets === null ? {} : { resets }),
1818
+ credits: routeText(record.credits),
1819
+ };
1820
+ }
1821
+ function optionalQuotaIcon(value) {
1822
+ const icon = routeText(value);
1823
+ if (icon === null)
1824
+ return {};
1825
+ if (/^file:[A-Za-z0-9][A-Za-z0-9._~-]{0,255}$/u.test(icon))
1826
+ return { icon };
1827
+ return /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u.test(icon) ? { icon } : {};
1828
+ }
1829
+ function magpieIconAssetPaths(icon) {
1830
+ if (/^file:[A-Za-z0-9][A-Za-z0-9._~-]{0,255}$/u.test(icon)) {
1831
+ return [`/api/icons/${encodeURIComponent(icon.slice('file:'.length))}`];
1832
+ }
1833
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u.test(icon))
1834
+ return [];
1835
+ return [`/icons/${icon}.svg`, `/icons/${icon}.png`];
892
1836
  }
893
1837
  function providerFromRoute(route) {
894
1838
  if (route === null)
@@ -947,6 +1891,12 @@ function cleanText(value) {
947
1891
  const text = value.trim();
948
1892
  return text === '' || text.length > 512 || /[\u0000-\u001f\u007f]/u.test(text) ? null : text;
949
1893
  }
1894
+ function cleanSessionName(value) {
1895
+ if (typeof value !== 'string')
1896
+ return null;
1897
+ const text = value.trim().replace(/[\u0000-\u001f\u007f]/gu, '').slice(0, 240);
1898
+ return text === '' ? null : text;
1899
+ }
950
1900
  /** Pi and Command Code store Magpie selections with a local `magpie/` provider prefix. */
951
1901
  function resolveCatalogModelId(agent, requestedModelId, catalog, modelName) {
952
1902
  if (catalog === null)