@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.
- package/README.en.md +6 -4
- package/README.md +6 -6
- package/data/build/dist/bootstrap/dsh-connection-adapter.d.ts +1 -1
- package/data/build/dist/bootstrap/index.d.ts +1 -1
- package/data/build/dist/client/bundle.js +5224 -290
- package/data/build/dist/client/bundle.js.map +1 -1
- package/data/build/dist/client/client.magpie-model-tag.js +155 -5
- package/data/build/dist/client/client.magpie-model-tag.js.map +1 -1
- package/data/build/dist/client/client.native-workspace-store.js +30 -4
- package/data/build/dist/client/client.native-workspace-store.js.map +1 -1
- package/data/build/dist/client/client.transport.js +747 -48
- package/data/build/dist/client/client.transport.js.map +1 -1
- package/data/build/dist/client/desktop-assistant.js +953 -185
- package/data/build/dist/client/feature-requirement.d.ts +43 -0
- package/data/build/dist/client/feature-requirement.d.ts.map +1 -0
- package/data/build/dist/client/feature-requirement.js +105 -0
- package/data/build/dist/client/feature-requirement.js.map +1 -0
- package/data/build/dist/client/features/index.d.ts +1 -0
- package/data/build/dist/client/features/index.d.ts.map +1 -1
- package/data/build/dist/client/features/index.js +3 -0
- package/data/build/dist/client/features/index.js.map +1 -1
- package/data/build/dist/client/features/magpie-management-controls.d.ts +46 -0
- package/data/build/dist/client/features/magpie-management-controls.d.ts.map +1 -0
- package/data/build/dist/client/features/magpie-management-controls.js +794 -0
- package/data/build/dist/client/features/magpie-management-controls.js.map +1 -0
- package/data/build/dist/client/features/magpie-management-page.d.ts +129 -0
- package/data/build/dist/client/features/magpie-management-page.d.ts.map +1 -0
- package/data/build/dist/client/features/magpie-management-page.js +1279 -0
- package/data/build/dist/client/features/magpie-management-page.js.map +1 -0
- package/data/build/dist/client/features/magpie-management.d.ts +7 -0
- package/data/build/dist/client/features/magpie-management.d.ts.map +1 -0
- package/data/build/dist/client/features/magpie-management.js +154 -0
- package/data/build/dist/client/features/magpie-management.js.map +1 -0
- package/data/build/dist/client/features/magpie-mark.d.ts +10 -0
- package/data/build/dist/client/features/magpie-mark.d.ts.map +1 -0
- package/data/build/dist/client/features/magpie-mark.js +21 -0
- package/data/build/dist/client/features/magpie-mark.js.map +1 -0
- package/data/build/dist/client/features/magpie-provider-logo.d.ts +11 -0
- package/data/build/dist/client/features/magpie-provider-logo.d.ts.map +1 -0
- package/data/build/dist/client/features/magpie-provider-logo.js +35 -0
- package/data/build/dist/client/features/magpie-provider-logo.js.map +1 -0
- package/data/build/dist/client/features/subscription-quota-groups.d.ts +193 -0
- package/data/build/dist/client/features/subscription-quota-groups.d.ts.map +1 -0
- package/data/build/dist/client/features/subscription-quota-groups.js +548 -0
- package/data/build/dist/client/features/subscription-quota-groups.js.map +1 -0
- package/data/build/dist/client/git-management.d.ts.map +1 -1
- package/data/build/dist/client/git-management.js +11 -9
- package/data/build/dist/client/git-management.js.map +1 -1
- package/data/build/dist/client/git-panel-styles.js +1 -0
- package/data/build/dist/client/git-panel-styles.js.map +1 -1
- package/data/build/dist/client/index.d.ts +1 -1
- package/data/build/dist/client/index.d.ts.map +1 -1
- package/data/build/dist/client/index.js +1 -1
- package/data/build/dist/client/index.js.map +1 -1
- package/data/build/dist/client/locale.d.ts.map +1 -1
- package/data/build/dist/client/locale.js +7 -0
- package/data/build/dist/client/locale.js.map +1 -1
- package/data/build/dist/client/locales/magpie.d.ts +4 -0
- package/data/build/dist/client/locales/magpie.d.ts.map +1 -0
- package/data/build/dist/client/locales/magpie.js +644 -0
- package/data/build/dist/client/locales/magpie.js.map +1 -0
- package/data/build/dist/client/locales/usage.d.ts.map +1 -1
- package/data/build/dist/client/locales/usage.js +52 -0
- package/data/build/dist/client/locales/usage.js.map +1 -1
- package/data/build/dist/client/magpie-model-tag.d.ts +9 -1
- package/data/build/dist/client/magpie-model-tag.d.ts.map +1 -1
- package/data/build/dist/client/magpie-model-tag.js +15 -3
- package/data/build/dist/client/magpie-model-tag.js.map +1 -1
- package/data/build/dist/client/provider-icons.d.ts +8 -0
- package/data/build/dist/client/provider-icons.d.ts.map +1 -1
- package/data/build/dist/client/provider-icons.js +122 -2
- package/data/build/dist/client/provider-icons.js.map +1 -1
- package/data/build/dist/client/settings-bridge.d.ts +1 -0
- package/data/build/dist/client/settings-bridge.d.ts.map +1 -1
- package/data/build/dist/client/settings-bridge.js +29 -10
- package/data/build/dist/client/settings-bridge.js.map +1 -1
- package/data/build/dist/client/settings-section.d.ts.map +1 -1
- package/data/build/dist/client/settings-section.js +21 -3
- package/data/build/dist/client/settings-section.js.map +1 -1
- package/data/build/dist/client/style.css +224 -221
- package/data/build/dist/client/subscription-slot.d.ts +22 -1
- package/data/build/dist/client/subscription-slot.d.ts.map +1 -1
- package/data/build/dist/client/subscription-slot.js +374 -25
- package/data/build/dist/client/subscription-slot.js.map +1 -1
- package/data/build/dist/dsh-capabilities/client/global-panel-adapter.d.ts +27 -0
- package/data/build/dist/dsh-capabilities/client/global-panel-adapter.d.ts.map +1 -0
- package/data/build/dist/dsh-capabilities/client/global-panel-adapter.js +109 -0
- package/data/build/dist/dsh-capabilities/client/global-panel-adapter.js.map +1 -0
- package/data/build/dist/dsh-capabilities/host/session-title-adapter.d.ts +2 -1
- package/data/build/dist/dsh-capabilities/host/session-title-adapter.d.ts.map +1 -1
- package/data/build/dist/dsh-capabilities/host/session-title-adapter.js +32 -2
- package/data/build/dist/dsh-capabilities/host/session-title-adapter.js.map +1 -1
- package/data/build/dist/dsh-capabilities/matrix.d.ts.map +1 -1
- package/data/build/dist/dsh-capabilities/matrix.js +12 -11
- package/data/build/dist/dsh-capabilities/matrix.js.map +1 -1
- package/data/build/dist/dsh-capabilities/routes.d.ts.map +1 -1
- package/data/build/dist/dsh-capabilities/routes.js +12 -9
- package/data/build/dist/dsh-capabilities/routes.js.map +1 -1
- package/data/build/dist/dsh-capabilities/types.d.ts +1 -1
- package/data/build/dist/dsh-capabilities/types.d.ts.map +1 -1
- package/data/build/dist/dsh-capabilities/types.js.map +1 -1
- package/data/build/dist/host/cli-adapters/acp-cli-driver.d.ts +27 -0
- package/data/build/dist/host/cli-adapters/acp-cli-driver.d.ts.map +1 -1
- package/data/build/dist/host/cli-adapters/acp-cli-driver.js +88 -13
- package/data/build/dist/host/cli-adapters/acp-cli-driver.js.map +1 -1
- package/data/build/dist/host/cli-adapters/codex-driver.d.ts +10 -0
- package/data/build/dist/host/cli-adapters/codex-driver.d.ts.map +1 -1
- package/data/build/dist/host/cli-adapters/codex-driver.js +32 -1
- package/data/build/dist/host/cli-adapters/codex-driver.js.map +1 -1
- package/data/build/dist/host/cli-adapters/command-code-acp-loader.d.ts +6 -5
- package/data/build/dist/host/cli-adapters/command-code-acp-loader.d.ts.map +1 -1
- package/data/build/dist/host/cli-adapters/command-code-acp-loader.js +72 -12
- package/data/build/dist/host/cli-adapters/command-code-acp-loader.js.map +1 -1
- package/data/build/dist/host/cli-adapters/command-code-driver.d.ts.map +1 -1
- package/data/build/dist/host/cli-adapters/command-code-driver.js +26 -1
- package/data/build/dist/host/cli-adapters/command-code-driver.js.map +1 -1
- package/data/build/dist/host/cli-adapters/dsh-message-projector.d.ts.map +1 -1
- package/data/build/dist/host/cli-adapters/dsh-message-projector.js +15 -2
- package/data/build/dist/host/cli-adapters/dsh-message-projector.js.map +1 -1
- package/data/build/dist/host/cli-adapters/feature.d.ts +13 -0
- package/data/build/dist/host/cli-adapters/feature.d.ts.map +1 -1
- package/data/build/dist/host/cli-adapters/feature.js +438 -6
- package/data/build/dist/host/cli-adapters/feature.js.map +1 -1
- package/data/build/dist/host/cli-adapters/grok-driver.js +5 -0
- package/data/build/dist/host/cli-adapters/grok-driver.js.map +1 -1
- package/data/build/dist/host/cli-adapters/magpie-detection.d.ts +72 -0
- package/data/build/dist/host/cli-adapters/magpie-detection.d.ts.map +1 -0
- package/data/build/dist/host/cli-adapters/magpie-detection.js +312 -0
- package/data/build/dist/host/cli-adapters/magpie-detection.js.map +1 -0
- package/data/build/dist/host/cli-adapters/magpie-management.d.ts +50 -0
- package/data/build/dist/host/cli-adapters/magpie-management.d.ts.map +1 -0
- package/data/build/dist/host/cli-adapters/magpie-management.js +1343 -0
- package/data/build/dist/host/cli-adapters/magpie-management.js.map +1 -0
- package/data/build/dist/host/cli-adapters/magpie-update.d.ts +59 -0
- package/data/build/dist/host/cli-adapters/magpie-update.d.ts.map +1 -0
- package/data/build/dist/host/cli-adapters/magpie-update.js +249 -0
- package/data/build/dist/host/cli-adapters/magpie-update.js.map +1 -0
- package/data/build/dist/host/cli-adapters/magpie-usage.d.ts +70 -8
- package/data/build/dist/host/cli-adapters/magpie-usage.d.ts.map +1 -1
- package/data/build/dist/host/cli-adapters/magpie-usage.js +1075 -125
- package/data/build/dist/host/cli-adapters/magpie-usage.js.map +1 -1
- package/data/build/dist/host/cli-adapters/magpie-web.d.ts +69 -0
- package/data/build/dist/host/cli-adapters/magpie-web.d.ts.map +1 -0
- package/data/build/dist/host/cli-adapters/magpie-web.js +485 -0
- package/data/build/dist/host/cli-adapters/magpie-web.js.map +1 -0
- package/data/build/dist/host/cli-adapters/process-utils.d.ts +2 -0
- package/data/build/dist/host/cli-adapters/process-utils.d.ts.map +1 -1
- package/data/build/dist/host/cli-adapters/process-utils.js +8 -0
- package/data/build/dist/host/cli-adapters/process-utils.js.map +1 -1
- package/data/build/dist/host/cli-adapters/session-performance-projection.d.ts +6 -0
- package/data/build/dist/host/cli-adapters/session-performance-projection.d.ts.map +1 -1
- package/data/build/dist/host/cli-adapters/session-performance-projection.js +25 -0
- package/data/build/dist/host/cli-adapters/session-performance-projection.js.map +1 -1
- package/data/build/dist/host/dsh-gateway-feature.d.ts.map +1 -1
- package/data/build/dist/host/dsh-gateway-feature.js +26 -3
- package/data/build/dist/host/dsh-gateway-feature.js.map +1 -1
- package/data/build/dist/host/features/host-status.d.ts +25 -0
- package/data/build/dist/host/features/host-status.d.ts.map +1 -1
- package/data/build/dist/host/features/host-status.js +60 -6
- package/data/build/dist/host/features/host-status.js.map +1 -1
- package/data/build/dist/host/features/lan-access-dsh.d.ts.map +1 -1
- package/data/build/dist/host/features/lan-access-dsh.js +61 -10
- package/data/build/dist/host/features/lan-access-dsh.js.map +1 -1
- package/data/build/dist/host/features/session-translation-protection.d.ts.map +1 -1
- package/data/build/dist/host/features/session-translation-protection.js +2 -1
- package/data/build/dist/host/features/session-translation-protection.js.map +1 -1
- package/data/build/dist/host/lan-access-dsh.d.ts.map +1 -1
- package/data/build/dist/host/lan-access-dsh.js +6 -2
- package/data/build/dist/host/lan-access-dsh.js.map +1 -1
- package/data/build/dist/host/modules/pwa/pwa-session-notifications.js +2 -1
- package/data/build/dist/host/modules/pwa/pwa-session-notifications.js.map +1 -1
- package/data/build/dist/host/rpc.d.ts +15 -2
- package/data/build/dist/host/rpc.d.ts.map +1 -1
- package/data/build/dist/host/rpc.js +47 -6
- package/data/build/dist/host/rpc.js.map +1 -1
- package/data/build/dist/host/settings.d.ts.map +1 -1
- package/data/build/dist/host/settings.js +5 -0
- package/data/build/dist/host/settings.js.map +1 -1
- package/data/build/dist/shared/contracts/config.d.ts +18 -0
- package/data/build/dist/shared/contracts/config.d.ts.map +1 -1
- package/data/build/dist/shared/contracts/config.js +8 -0
- package/data/build/dist/shared/contracts/config.js.map +1 -1
- package/data/build/dist/shared/contracts/errors.d.ts +2 -2
- package/data/build/dist/shared/contracts/feature.d.ts +7 -0
- package/data/build/dist/shared/contracts/feature.d.ts.map +1 -1
- package/data/build/dist/shared/contracts/magpie-management.d.ts +414 -0
- package/data/build/dist/shared/contracts/magpie-management.d.ts.map +1 -0
- package/data/build/dist/shared/contracts/magpie-management.js +2 -0
- package/data/build/dist/shared/contracts/magpie-management.js.map +1 -0
- package/data/build/dist/shared/contracts/subscription.d.ts +77 -1
- package/data/build/dist/shared/contracts/subscription.d.ts.map +1 -1
- package/data/build/dist/shared/contracts/subscription.js.map +1 -1
- package/data/build/dist/shared/contracts/version.d.ts +4 -4
- package/data/build/dist/shared/contracts/version.js +3 -3
- package/data/build/dist/shared/index.d.ts +3 -1
- package/data/build/dist/shared/index.d.ts.map +1 -1
- package/data/build/dist/shared/index.js +2 -1
- package/data/build/dist/shared/index.js.map +1 -1
- package/data/build/dist/shared/magpie-provider-id.d.ts +12 -0
- package/data/build/dist/shared/magpie-provider-id.d.ts.map +1 -0
- package/data/build/dist/shared/magpie-provider-id.js +16 -0
- package/data/build/dist/shared/magpie-provider-id.js.map +1 -0
- package/data/build/dist/shared/session-title.d.ts.map +1 -1
- package/data/build/dist/shared/session-title.js +10 -4
- package/data/build/dist/shared/session-title.js.map +1 -1
- package/package.json +29 -29
- package/profile/README.md +2 -2
- package/profile/package.json +3 -3
- package/profile/version.json +3 -3
- 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
|
-
|
|
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
|
-
|
|
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.
|
|
183
|
-
this.
|
|
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
|
|
187
|
-
? readSession(usageResult.value, agent,
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
?
|
|
202
|
-
:
|
|
203
|
-
|
|
204
|
-
|
|
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 =
|
|
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
|
-
:
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
324
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
*
|
|
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)
|
|
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
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
|
|
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:
|
|
884
|
-
remainingPercent:
|
|
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)
|