@jingyi0605/codingns4dsh 0.2.1-rc.3 → 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 (68) hide show
  1. package/README.en.md +3 -3
  2. package/README.md +3 -3
  3. package/data/build/dist/client/bundle.js +168 -47
  4. package/data/build/dist/client/bundle.js.map +1 -1
  5. package/data/build/dist/client/client.native-workspace-store.js +1 -1
  6. package/data/build/dist/client/client.native-workspace-store.js.map +1 -1
  7. package/data/build/dist/client/client.transport.js +32 -10
  8. package/data/build/dist/client/client.transport.js.map +1 -1
  9. package/data/build/dist/client/desktop-assistant.js +32 -10
  10. package/data/build/dist/client/features/magpie-management-controls.d.ts.map +1 -1
  11. package/data/build/dist/client/features/magpie-management-controls.js +45 -10
  12. package/data/build/dist/client/features/magpie-management-controls.js.map +1 -1
  13. package/data/build/dist/client/features/magpie-management-page.d.ts +6 -1
  14. package/data/build/dist/client/features/magpie-management-page.d.ts.map +1 -1
  15. package/data/build/dist/client/features/magpie-management-page.js +29 -6
  16. package/data/build/dist/client/features/magpie-management-page.js.map +1 -1
  17. package/data/build/dist/client/locales/magpie.d.ts.map +1 -1
  18. package/data/build/dist/client/locales/magpie.js +32 -10
  19. package/data/build/dist/client/locales/magpie.js.map +1 -1
  20. package/data/build/dist/client/subscription-slot.d.ts +3 -2
  21. package/data/build/dist/client/subscription-slot.d.ts.map +1 -1
  22. package/data/build/dist/client/subscription-slot.js +68 -22
  23. package/data/build/dist/client/subscription-slot.js.map +1 -1
  24. package/data/build/dist/host/cli-adapters/feature.d.ts.map +1 -1
  25. package/data/build/dist/host/cli-adapters/feature.js +56 -8
  26. package/data/build/dist/host/cli-adapters/feature.js.map +1 -1
  27. package/data/build/dist/host/cli-adapters/magpie-detection.d.ts +3 -3
  28. package/data/build/dist/host/cli-adapters/magpie-detection.d.ts.map +1 -1
  29. package/data/build/dist/host/cli-adapters/magpie-detection.js +18 -4
  30. package/data/build/dist/host/cli-adapters/magpie-detection.js.map +1 -1
  31. package/data/build/dist/host/cli-adapters/magpie-management.d.ts.map +1 -1
  32. package/data/build/dist/host/cli-adapters/magpie-management.js +95 -20
  33. package/data/build/dist/host/cli-adapters/magpie-management.js.map +1 -1
  34. package/data/build/dist/host/cli-adapters/magpie-usage.d.ts +5 -1
  35. package/data/build/dist/host/cli-adapters/magpie-usage.d.ts.map +1 -1
  36. package/data/build/dist/host/cli-adapters/magpie-usage.js +285 -59
  37. package/data/build/dist/host/cli-adapters/magpie-usage.js.map +1 -1
  38. package/data/build/dist/host/cli-adapters/magpie-web.d.ts +15 -0
  39. package/data/build/dist/host/cli-adapters/magpie-web.d.ts.map +1 -1
  40. package/data/build/dist/host/cli-adapters/magpie-web.js +100 -22
  41. package/data/build/dist/host/cli-adapters/magpie-web.js.map +1 -1
  42. package/data/build/dist/host/cli-adapters/process-utils.d.ts.map +1 -1
  43. package/data/build/dist/host/cli-adapters/process-utils.js +4 -0
  44. package/data/build/dist/host/cli-adapters/process-utils.js.map +1 -1
  45. package/data/build/dist/host/cli-adapters/session-performance-projection.d.ts +6 -0
  46. package/data/build/dist/host/cli-adapters/session-performance-projection.d.ts.map +1 -1
  47. package/data/build/dist/host/cli-adapters/session-performance-projection.js +25 -0
  48. package/data/build/dist/host/cli-adapters/session-performance-projection.js.map +1 -1
  49. package/data/build/dist/shared/contracts/magpie-management.d.ts +19 -0
  50. package/data/build/dist/shared/contracts/magpie-management.d.ts.map +1 -1
  51. package/data/build/dist/shared/contracts/subscription.d.ts +2 -0
  52. package/data/build/dist/shared/contracts/subscription.d.ts.map +1 -1
  53. package/data/build/dist/shared/contracts/subscription.js.map +1 -1
  54. package/data/build/dist/shared/contracts/version.d.ts +1 -1
  55. package/data/build/dist/shared/contracts/version.js +1 -1
  56. package/data/build/dist/shared/index.d.ts +2 -1
  57. package/data/build/dist/shared/index.d.ts.map +1 -1
  58. package/data/build/dist/shared/index.js +1 -0
  59. package/data/build/dist/shared/index.js.map +1 -1
  60. package/data/build/dist/shared/magpie-provider-id.d.ts +12 -0
  61. package/data/build/dist/shared/magpie-provider-id.d.ts.map +1 -0
  62. package/data/build/dist/shared/magpie-provider-id.js +16 -0
  63. package/data/build/dist/shared/magpie-provider-id.js.map +1 -0
  64. package/package.json +1 -1
  65. package/profile/README.md +2 -2
  66. package/profile/package.json +2 -2
  67. package/profile/version.json +1 -1
  68. package/version.json +1 -1
@@ -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;
@@ -8,10 +9,13 @@ 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;
10
11
  const MAGPIE_DASHBOARD_CACHE_TTL_MS = 15_000;
12
+ const MAGPIE_PROVIDER_CATALOG_TTL_MS = 15_000;
11
13
  const MAX_MAGPIE_CLI_OUTPUT_BYTES = 512 * 1024;
12
14
  const MAX_MAGPIE_RESPONSE_BYTES = 2 * 1024 * 1024;
13
15
  const MAX_MAGPIE_ICON_FETCHES = 64;
14
16
  const MAX_MAGPIE_MODELS = 10_000;
17
+ const MAX_MAGPIE_SESSION_SCAN_ROWS = 10_000;
18
+ const MAX_MAGPIE_SESSION_IDENTITIES = 16;
15
19
  const MAX_MODEL_TOKEN_LIMIT = 1_000_000_000;
16
20
  const MAGPIE_MODEL_ID_PATTERN = '[A-Za-z0-9][A-Za-z0-9._-]*(?:/[A-Za-z0-9][A-Za-z0-9._:/-]*)+';
17
21
  const MAGPIE_AGENT_BY_ADAPTER = {
@@ -62,6 +66,7 @@ export class MagpieUsageService {
62
66
  dashboardCache = new Map();
63
67
  dashboardRefreshes = new Map();
64
68
  quotaLogoCache = new Map();
69
+ providerCatalog;
65
70
  constructor(options = {}) {
66
71
  this.readModels = options.readModels ?? readMagpieModels;
67
72
  // An injected catalog reader represents the whole fake CLI boundary in
@@ -305,45 +310,119 @@ export class MagpieUsageService {
305
310
  || !Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > 100)
306
311
  return null;
307
312
  const sessionId = options.sessionId === undefined ? null : cleanText(options.sessionId);
313
+ const sessionIds = options.sessionIds === undefined
314
+ ? []
315
+ : options.sessionIds.map(cleanText);
308
316
  const sessionAgent = options.sessionAgent === undefined ? null : cleanText(options.sessionAgent);
309
317
  if ((options.sessionId !== undefined && sessionId === null)
318
+ || sessionIds.some((value) => value === null)
319
+ || sessionIds.length > MAX_MAGPIE_SESSION_IDENTITIES
310
320
  || (options.sessionAgent !== undefined && (sessionAgent === null || !isMagpieAgentId(sessionAgent)))
311
- || ((sessionId === null) !== (sessionAgent === null)))
312
- return null;
313
- const query = new URLSearchParams({ period: magpieWebPeriod(options.period), limit: String(options.limit), offset: String(options.offset) });
314
- if (sessionId !== null)
315
- query.set('q', sessionId);
316
- const response = await this.readWebJson(`/api/usage/requests?${query.toString()}`, signal);
317
- if (!response.ok)
321
+ || ((sessionId === null) !== (sessionAgent === null))
322
+ || (sessionIds.length > 0 && sessionId === null))
318
323
  return null;
319
- const root = asRecord(response.value);
320
- const rawRows = root?.rows;
321
- if (!Array.isArray(rawRows) || rawRows.length > 500)
324
+ const requestedSessionIds = sessionId === null
325
+ ? []
326
+ : [...new Set([sessionId, ...sessionIds.filter((value) => value !== null)])];
327
+ if (requestedSessionIds.length > MAX_MAGPIE_SESSION_IDENTITIES)
322
328
  return null;
323
- const candidates = rawRows.flatMap((row) => {
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) => {
324
378
  const record = asRecord(row);
325
379
  if (record === null)
326
- return [];
327
- const candidateSession = cleanText(record.session);
328
- const nativeSession = cleanText(record.native_session);
329
- const agent = cleanText(record.agent);
330
- const acceptedAgents = sessionAgent === null ? [] : MAGPIE_USAGE_AGENT_ALIASES[sessionAgent] ?? [sessionAgent];
331
- if (sessionId !== null
332
- && (candidateSession !== sessionId && nativeSession !== sessionId || agent === null || !acceptedAgents.includes(agent)))
333
- return [];
334
- const item = parseMagpieRequest(record);
335
- return item === null ? [] : [sessionId === null ? item : { ...item, sessionId }];
336
- });
337
- const total = sessionId === null ? safeCount(root?.total) : 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;
338
417
  return {
339
418
  period: options.period,
340
419
  offset: options.offset,
341
420
  limit: options.limit,
342
421
  total,
343
- hasMore: total === null ? rawRows.length >= options.limit : options.offset + rawRows.length < total,
422
+ hasMore: !fullyScanned || matching.length > requestedEnd,
344
423
  sessionId,
345
424
  sessionAgent,
346
- items: candidates.slice(0, options.limit),
425
+ items: matching.slice(options.offset, requestedEnd),
347
426
  };
348
427
  }
349
428
  async readFromWeb(agent, modelId, sessionId, magpieSessionId, fallbackSessionId, signal, includeRoute) {
@@ -369,8 +448,12 @@ export class MagpieUsageService {
369
448
  : { ok: false };
370
449
  const routeRows = routeHistoryResult.ok ? asRecord(routeHistoryResult.value)?.rows : null;
371
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
+ : [];
372
455
  const routeHistory = routeHistoryResult.ok && routeHistoryValid && resolvedSessionId !== null
373
- ? readSessionRouteHistory(routeHistoryResult.value, agent, resolvedSessionId)
456
+ ? readSessionRouteHistory(routeHistoryResult.value, agent, resolvedSessionId, providerRecords)
374
457
  : [];
375
458
  const route = routeHistory[0]?.route ?? null;
376
459
  const routeStatus = route !== null
@@ -407,6 +490,7 @@ export class MagpieUsageService {
407
490
  usageStatus: aggregate === null ? 'unavailable' : 'available',
408
491
  aggregate,
409
492
  sessionId,
493
+ ...(resolvedSessionId === null ? {} : { requestSessionId: resolvedSessionId }),
410
494
  session: sessionUsage,
411
495
  routeStatus,
412
496
  route,
@@ -591,6 +675,18 @@ export class MagpieUsageService {
591
675
  return { ok: false };
592
676
  }
593
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
+ }
594
690
  }
595
691
  function awaitWithSignal(task, signal) {
596
692
  if (signal === undefined)
@@ -864,53 +960,124 @@ export function magpieUsageAgentAliases(adapterId) {
864
960
  * native DSH use different identities across Magpie versions.
865
961
  */
866
962
  export function attachMagpieSessionNames(dashboard, bindings) {
867
- if (!Array.isArray(dashboard.sessions) || bindings.length === 0)
963
+ if (!Array.isArray(dashboard.sessions))
868
964
  return dashboard;
869
- const names = new Map();
965
+ const owners = new Map();
870
966
  const ambiguous = new Set();
967
+ const ownerByGroup = new Map();
871
968
  for (const binding of bindings) {
872
- const name = cleanSessionName(binding.name);
873
969
  const dshSessionId = cleanText(binding.dshSessionId);
874
- if (name === null || dshSessionId === null)
970
+ const name = cleanSessionName(binding.name);
971
+ if (dshSessionId === null)
875
972
  continue;
876
- // Match the identity strategy used by compact usage: native DSH uses its
877
- // DSH ID, Command Code may use either injected DSH or ACP ID, and other
878
- // adapters use only their Provider session ID.
973
+ // Magpie versions may record either the provider conversation ID or the
974
+ // DSH ID; both exact identities represent the same DSH session.
879
975
  const providerSessionId = cleanText(binding.providerSessionId);
880
- const ids = new Set((binding.adapterId === 'dsh'
881
- ? [dshSessionId]
882
- : binding.adapterId === 'command-code'
883
- ? [dshSessionId, providerSessionId]
884
- : [providerSessionId]).filter((value) => value !== null));
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);
885
996
  for (const agent of magpieUsageAgentAliases(binding.adapterId)) {
886
997
  for (const id of ids) {
887
998
  const key = `${agent}\u0000${id}`;
888
999
  if (ambiguous.has(key))
889
1000
  continue;
890
- const previous = names.get(key);
891
- if (previous !== undefined && previous !== name) {
892
- names.delete(key);
1001
+ const previous = owners.get(key);
1002
+ if (previous !== undefined && previous.groupKey !== groupKey) {
1003
+ owners.delete(key);
893
1004
  ambiguous.add(key);
894
1005
  }
895
1006
  else {
896
- names.set(key, name);
1007
+ owners.set(key, currentOwner);
897
1008
  }
898
1009
  }
899
1010
  }
900
1011
  }
901
- if (names.size === 0)
902
- return dashboard;
903
- let changed = false;
904
- const sessions = dashboard.sessions.map((session) => {
905
- if (session.dshSessionName !== undefined)
906
- return session;
907
- const name = names.get(`${session.agent}\u0000${session.id}`);
908
- if (name === undefined)
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)
909
1045
  return session;
910
- changed = true;
911
- return { ...session, dshSessionName: name };
1046
+ return { ...session, sessionIds: [...ids] };
912
1047
  });
913
- return changed ? { ...dashboard, sessions } : dashboard;
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);
914
1081
  }
915
1082
  function isMagpieAgentId(value) {
916
1083
  return value.length <= 64 && /^[a-z0-9][a-z0-9-]*$/u.test(value);
@@ -999,7 +1166,7 @@ function readSession(value, agent, sessionId) {
999
1166
  return record !== null && usageAgent !== null && totals !== null ? { id: sessionId, agent: usageAgent, ...totals } : null;
1000
1167
  }
1001
1168
  /** Read the durable request ledger when Magpie's short in-memory trace has expired. */
1002
- function readSessionRouteHistory(value, agent, sessionId) {
1169
+ function readSessionRouteHistory(value, agent, sessionId, providerRecords = []) {
1003
1170
  const rows = asRecord(value)?.rows;
1004
1171
  if (!Array.isArray(rows) || rows.length > 500)
1005
1172
  return [];
@@ -1009,9 +1176,11 @@ function readSessionRouteHistory(value, agent, sessionId) {
1009
1176
  const rowAgent = cleanText(record?.agent);
1010
1177
  const session = cleanText(record?.session);
1011
1178
  const nativeSession = cleanText(record?.native_session);
1012
- const provider = routeText(record?.provider);
1013
1179
  if (record === null || rowAgent === null || !usageAgents.includes(rowAgent)
1014
- || (session !== sessionId && nativeSession !== sessionId) || provider === null)
1180
+ || (session !== sessionId && nativeSession !== sessionId))
1181
+ return [];
1182
+ const provider = normalizeMagpieRequestProvider(record, providerRecords);
1183
+ if (provider === null)
1015
1184
  return [];
1016
1185
  const requested = routeText(record.req);
1017
1186
  const model = routeText(record.model);
@@ -1393,9 +1562,32 @@ function readMagpieUsageSessions(value) {
1393
1562
  return record === null || id === null || agent === null ? [] : [{ id, agent, ...readMagpieUsageMetrics(record) }];
1394
1563
  });
1395
1564
  }
1396
- function parseMagpieRequest(record) {
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 = []) {
1397
1589
  const agent = routeText(record.agent);
1398
- const provider = routeText(record.provider);
1590
+ const provider = normalizeMagpieRequestProvider(record, providerRecords);
1399
1591
  const requestedModel = routeText(record.req);
1400
1592
  const servedModel = routeText(record.served);
1401
1593
  const group = requestedModel?.startsWith('group/') === true ? requestedModel.slice('group/'.length) : null;
@@ -1404,7 +1596,7 @@ function parseMagpieRequest(record) {
1404
1596
  time: routeText(record.t),
1405
1597
  agent,
1406
1598
  provider,
1407
- account: routeText(record.providerAccount),
1599
+ account: normalizeMagpieRequestAccount(record, provider, providerRecords),
1408
1600
  requestedModel,
1409
1601
  servedModel,
1410
1602
  group,
@@ -1425,6 +1617,40 @@ function parseMagpieRequest(record) {
1425
1617
  sessionId: cleanText(record.session) ?? cleanText(record.native_session),
1426
1618
  };
1427
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
+ }
1428
1654
  function firstRouteText(record, keys) {
1429
1655
  for (const key of keys) {
1430
1656
  const value = routeText(record[key]);