axusage 3.5.0 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +22 -3
  2. package/dist/adapters/claude.d.ts +2 -9
  3. package/dist/adapters/claude.js +40 -53
  4. package/dist/adapters/codex.d.ts +2 -8
  5. package/dist/adapters/codex.js +30 -42
  6. package/dist/adapters/copilot.d.ts +2 -7
  7. package/dist/adapters/copilot.js +32 -43
  8. package/dist/adapters/gemini.d.ts +2 -8
  9. package/dist/adapters/gemini.js +26 -38
  10. package/dist/adapters/parse-claude-usage.js +1 -0
  11. package/dist/adapters/parse-codex-usage.js +1 -0
  12. package/dist/adapters/parse-copilot-usage.js +2 -0
  13. package/dist/adapters/parse-gemini-usage.js +1 -0
  14. package/dist/cli.js +1 -1
  15. package/dist/commands/fetch-service-usage.d.ts +8 -2
  16. package/dist/commands/fetch-service-usage.js +76 -9
  17. package/dist/commands/serve-command.js +5 -2
  18. package/dist/commands/usage-command.d.ts +1 -0
  19. package/dist/commands/usage-command.js +8 -8
  20. package/dist/config/credential-sources.d.ts +16 -11
  21. package/dist/config/credential-sources.js +48 -27
  22. package/dist/server/routes.js +6 -2
  23. package/dist/services/get-instance-access-token.d.ts +20 -0
  24. package/dist/services/{get-service-access-token.js → get-instance-access-token.js} +31 -52
  25. package/dist/services/resolve-service-instances.d.ts +13 -0
  26. package/dist/services/resolve-service-instances.js +24 -0
  27. package/dist/services/service-adapter-registry.d.ts +3 -12
  28. package/dist/services/service-adapter-registry.js +14 -14
  29. package/dist/types/domain.d.ts +9 -3
  30. package/dist/utils/calculate-usage-rate.d.ts +1 -1
  31. package/dist/utils/calculate-usage-rate.js +1 -2
  32. package/dist/utils/format-prometheus-metrics.d.ts +2 -2
  33. package/dist/utils/format-prometheus-metrics.js +22 -5
  34. package/dist/utils/format-service-usage.d.ts +1 -1
  35. package/dist/utils/format-service-usage.js +26 -17
  36. package/package.json +6 -6
  37. package/dist/services/get-service-access-token.d.ts +0 -28
@@ -2,6 +2,7 @@ import type { ServiceResult } from "../types/domain.js";
2
2
  import type { UsageCommandOptions } from "./fetch-service-usage.js";
3
3
  /**
4
4
  * Fetches usage for all requested services in parallel.
5
+ * Each service type may produce multiple results (multi-instance support).
5
6
  */
6
7
  export declare function fetchServicesInParallel(servicesToQuery: string[]): Promise<ServiceResult[]>;
7
8
  /**
@@ -1,16 +1,15 @@
1
1
  import { formatServiceUsageData, formatServiceUsageDataAsJson, formatServiceUsageAsTsv, toJsonObject, } from "../utils/format-service-usage.js";
2
2
  import { formatPrometheusMetrics } from "../utils/format-prometheus-metrics.js";
3
- import { fetchServiceUsage, selectServicesToQuery, } from "./fetch-service-usage.js";
3
+ import { fetchServiceInstanceUsage, selectServicesToQuery, } from "./fetch-service-usage.js";
4
4
  import { isAuthFailure } from "./run-auth-setup.js";
5
5
  import { chalk } from "../utils/color.js";
6
6
  /**
7
7
  * Fetches usage for all requested services in parallel.
8
+ * Each service type may produce multiple results (multi-instance support).
8
9
  */
9
10
  export async function fetchServicesInParallel(servicesToQuery) {
10
- return await Promise.all(servicesToQuery.map(async (serviceName) => {
11
- const result = await fetchServiceUsage(serviceName);
12
- return { service: serviceName, result };
13
- }));
11
+ const nestedResults = await Promise.all(servicesToQuery.map((serviceName) => fetchServiceInstanceUsage(serviceName)));
12
+ return nestedResults.flat();
14
13
  }
15
14
  /**
16
15
  * Executes the usage command
@@ -74,9 +73,10 @@ export async function usageCommand(options) {
74
73
  console.log(formatServiceUsageDataAsJson(singleSuccess));
75
74
  }
76
75
  else {
76
+ const now = Date.now();
77
77
  const payload = successes.length === 1 && singleSuccess
78
- ? toJsonObject(singleSuccess)
79
- : successes.map((data) => toJsonObject(data));
78
+ ? toJsonObject(singleSuccess, now)
79
+ : successes.map((data) => toJsonObject(data, now));
80
80
  const output = hasPartialFailures
81
81
  ? {
82
82
  results: payload,
@@ -94,7 +94,7 @@ export async function usageCommand(options) {
94
94
  }
95
95
  case "prometheus": {
96
96
  // Emit Prometheus text metrics using prom-client
97
- const output = await formatPrometheusMetrics(successes);
97
+ const output = await formatPrometheusMetrics(successes, Date.now());
98
98
  process.stdout.write(output);
99
99
  break;
100
100
  }
@@ -4,7 +4,9 @@
4
4
  * Supports three modes:
5
5
  * - "local": Use local credentials from axauth (default behavior)
6
6
  * - "vault": Fetch credentials from axvault server
7
- * - "auto": Try vault first if configured and credential name provided, fallback to local
7
+ * - "auto": Without a credential name, uses local credentials. With a named
8
+ * credential, requires vault (no local fallback) to prevent returning the same
9
+ * local token for multiple instances. Vault must be configured for named credentials.
8
10
  */
9
11
  import { z } from "zod";
10
12
  import type { SupportedService } from "../services/supported-service.js";
@@ -15,18 +17,12 @@ declare const CredentialSourceType: z.ZodEnum<{
15
17
  vault: "vault";
16
18
  }>;
17
19
  type CredentialSourceType = z.infer<typeof CredentialSourceType>;
18
- /** Resolved source config with normalized fields */
19
- interface ResolvedSourceConfig {
20
+ /** Resolved instance config with display name */
21
+ interface ResolvedInstanceConfig {
20
22
  source: CredentialSourceType;
21
23
  name: string | undefined;
24
+ displayName: string | undefined;
22
25
  }
23
- /**
24
- * Get the resolved source config for a specific service.
25
- *
26
- * @param service - Service ID (e.g., "claude", "codex", "gemini")
27
- * @returns Resolved config with source type and optional credential name
28
- */
29
- declare function getServiceSourceConfig(service: SupportedService): ResolvedSourceConfig;
30
26
  /**
31
27
  * Get the credential sources config file path.
32
28
  *
@@ -35,4 +31,13 @@ declare function getServiceSourceConfig(service: SupportedService): ResolvedSour
35
31
  * directory during construction).
36
32
  */
37
33
  declare function getCredentialSourcesPath(): string;
38
- export { getServiceSourceConfig, getCredentialSourcesPath };
34
+ /**
35
+ * Get all instance configs for a service, normalizing all config forms to an array.
36
+ *
37
+ * - String shorthand → single instance with that source
38
+ * - Object → single instance
39
+ * - Array → multiple instances
40
+ */
41
+ declare function getServiceInstanceConfigs(service: SupportedService): ResolvedInstanceConfig[];
42
+ export { getServiceInstanceConfigs, getCredentialSourcesPath };
43
+ export type { ResolvedInstanceConfig };
@@ -4,7 +4,9 @@
4
4
  * Supports three modes:
5
5
  * - "local": Use local credentials from axauth (default behavior)
6
6
  * - "vault": Fetch credentials from axvault server
7
- * - "auto": Try vault first if configured and credential name provided, fallback to local
7
+ * - "auto": Without a credential name, uses local credentials. With a named
8
+ * credential, requires vault (no local fallback) to prevent returning the same
9
+ * local token for multiple instances. Vault must be configured for named credentials.
8
10
  */
9
11
  import Conf from "conf";
10
12
  import envPaths from "env-paths";
@@ -12,13 +14,17 @@ import path from "node:path";
12
14
  import { z } from "zod";
13
15
  /** Credential source type */
14
16
  const CredentialSourceType = z.enum(["auto", "local", "vault"]);
15
- /** Service source config - either a string shorthand or object with name */
17
+ /** Instance source config - object form with optional name and displayName */
18
+ const InstanceSourceConfig = z.object({
19
+ source: CredentialSourceType,
20
+ name: z.string().min(1).optional(),
21
+ displayName: z.string().min(1).optional(),
22
+ });
23
+ /** Service source config - string shorthand, object, or array of objects */
16
24
  const ServiceSourceConfig = z.union([
17
25
  CredentialSourceType,
18
- z.object({
19
- source: CredentialSourceType,
20
- name: z.string().optional(),
21
- }),
26
+ InstanceSourceConfig,
27
+ z.array(InstanceSourceConfig).min(1),
22
28
  ]);
23
29
  /** Full sources config - map of service ID to source config */
24
30
  const SourcesConfig = z.record(z.string(), ServiceSourceConfig);
@@ -96,26 +102,6 @@ function getCredentialSourceConfig() {
96
102
  // Priority 3: Empty (defaults apply)
97
103
  return {};
98
104
  }
99
- /**
100
- * Get the resolved source config for a specific service.
101
- *
102
- * @param service - Service ID (e.g., "claude", "codex", "gemini")
103
- * @returns Resolved config with source type and optional credential name
104
- */
105
- function getServiceSourceConfig(service) {
106
- const config = getCredentialSourceConfig();
107
- const serviceConfig = config[service];
108
- // Default: auto mode with no credential name
109
- if (serviceConfig === undefined) {
110
- return { source: "auto", name: undefined };
111
- }
112
- // String shorthand: just the source type
113
- if (typeof serviceConfig === "string") {
114
- return { source: serviceConfig, name: undefined };
115
- }
116
- // Object: source and name
117
- return { source: serviceConfig.source, name: serviceConfig.name };
118
- }
119
105
  /**
120
106
  * Get the credential sources config file path.
121
107
  *
@@ -127,4 +113,39 @@ function getCredentialSourcesPath() {
127
113
  const configDirectory = envPaths("axusage", { suffix: "" }).config;
128
114
  return path.resolve(configDirectory, "config.json");
129
115
  }
130
- export { getServiceSourceConfig, getCredentialSourcesPath };
116
+ /**
117
+ * Get all instance configs for a service, normalizing all config forms to an array.
118
+ *
119
+ * - String shorthand → single instance with that source
120
+ * - Object → single instance
121
+ * - Array → multiple instances
122
+ */
123
+ function getServiceInstanceConfigs(service) {
124
+ const config = getCredentialSourceConfig();
125
+ const serviceConfig = config[service];
126
+ // Default: single auto instance
127
+ if (serviceConfig === undefined) {
128
+ return [{ source: "auto", name: undefined, displayName: undefined }];
129
+ }
130
+ // String shorthand: single instance with that source
131
+ if (typeof serviceConfig === "string") {
132
+ return [{ source: serviceConfig, name: undefined, displayName: undefined }];
133
+ }
134
+ // Array: multiple instances
135
+ if (Array.isArray(serviceConfig)) {
136
+ return serviceConfig.map((instance) => ({
137
+ source: instance.source,
138
+ name: instance.name,
139
+ displayName: instance.displayName,
140
+ }));
141
+ }
142
+ // Object: single instance
143
+ return [
144
+ {
145
+ source: serviceConfig.source,
146
+ name: serviceConfig.name,
147
+ displayName: serviceConfig.displayName,
148
+ },
149
+ ];
150
+ }
151
+ export { getServiceInstanceConfigs, getCredentialSourcesPath };
@@ -27,6 +27,8 @@ export function createMetricsRouter(getState) {
27
27
  // Memoize the rendered Prometheus text by the state snapshot's refreshedAt
28
28
  // timestamp. Scrapes within the same cache window reuse the same Promise,
29
29
  // avoiding recreating prom-client Registry/Gauge objects on each request.
30
+ // Rate is computed at refreshedAt so output is deterministic per snapshot,
31
+ // keeping the gauge coherent with the usage data it describes.
30
32
  // Assignments happen synchronously (before any await) so require-atomic-updates
31
33
  // is satisfied and concurrent scrapes naturally coalesce onto one render.
32
34
  let memoFor;
@@ -40,7 +42,7 @@ export function createMetricsRouter(getState) {
40
42
  }
41
43
  if (memoFor !== state.refreshedAt) {
42
44
  memoFor = state.refreshedAt;
43
- memoPromise = formatPrometheusMetrics(usage);
45
+ memoPromise = formatPrometheusMetrics(usage, state.refreshedAt.getTime());
44
46
  }
45
47
  const text = await memoPromise;
46
48
  response
@@ -60,7 +62,9 @@ export function createUsageRouter(getFreshState) {
60
62
  response.status(503).json({ error: "No data yet" });
61
63
  return;
62
64
  }
63
- response.status(200).json(usage.map((entry) => toJsonObject(entry)));
65
+ response
66
+ .status(200)
67
+ .json(usage.map((entry) => toJsonObject(entry, state.refreshedAt.getTime())));
64
68
  });
65
69
  return router;
66
70
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Instance-aware credential fetcher.
3
+ *
4
+ * Resolves an access token for a specific service instance config,
5
+ * returning vault metadata (displayName) alongside the token.
6
+ */
7
+ import type { ResolvedInstanceConfig } from "../config/credential-sources.js";
8
+ /** Result of resolving an instance token */
9
+ interface InstanceTokenResult {
10
+ token: string | undefined;
11
+ vaultDisplayName: string | undefined;
12
+ }
13
+ /**
14
+ * Get access token for a specific service instance.
15
+ *
16
+ * Returns vault metadata (displayName) alongside the token
17
+ * for multi-instance identification.
18
+ */
19
+ declare function getInstanceAccessToken(service: string, config: ResolvedInstanceConfig): Promise<InstanceTokenResult>;
20
+ export { getInstanceAccessToken };
@@ -1,13 +1,10 @@
1
1
  /**
2
- * Unified credential fetcher for services.
2
+ * Instance-aware credential fetcher.
3
3
  *
4
- * Fetches access tokens based on per-service configuration:
5
- * - "local": From local axauth credential store
6
- * - "vault": From axvault server
7
- * - "auto": Try vault first if configured, fallback to local
4
+ * Resolves an access token for a specific service instance config,
5
+ * returning vault metadata (displayName) alongside the token.
8
6
  */
9
7
  import { fetchVaultCredentials, getAgentAccessToken, isVaultConfigured, } from "axauth";
10
- import { getServiceSourceConfig } from "../config/credential-sources.js";
11
8
  import { getCopilotTokenFromCustomGhPath } from "../utils/copilot-gh-token.js";
12
9
  /**
13
10
  * Extract access token from vault credentials.
@@ -42,41 +39,32 @@ function extractAccessToken(credentials) {
42
39
  }
43
40
  return undefined;
44
41
  }
45
- /**
46
- * Fetch access token from vault.
47
- *
48
- * @returns Access token string or undefined if not available
49
- */
50
- async function fetchFromVault(agentId, credentialName) {
42
+ /** Fetch access token from vault, returning metadata alongside the token */
43
+ async function fetchFromVaultWithMetadata(agentId, credentialName) {
51
44
  try {
52
45
  const result = await fetchVaultCredentials({
53
46
  agentId,
54
47
  name: credentialName,
55
48
  });
56
49
  if (!result.ok) {
57
- // Log warning for debugging, but don't fail hard
58
50
  if (result.reason !== "not-configured" && result.reason !== "not-found") {
59
51
  console.error(`[axusage] Vault fetch failed for ${agentId}/${credentialName}: ${result.reason}`);
60
52
  }
61
- return undefined;
53
+ return { token: undefined, vaultDisplayName: undefined };
62
54
  }
63
55
  const token = extractAccessToken(result.credentials);
64
56
  if (!token) {
65
57
  console.error(`[axusage] Vault credentials for ${agentId}/${credentialName} missing access token. ` +
66
58
  `Credential type: ${result.credentials.type}`);
67
59
  }
68
- return token;
60
+ return { token, vaultDisplayName: result.displayName };
69
61
  }
70
62
  catch (error) {
71
63
  console.error(`[axusage] Vault fetch error for ${agentId}/${credentialName}: ${error instanceof Error ? error.message : String(error)}`);
72
- return undefined;
64
+ return { token: undefined, vaultDisplayName: undefined };
73
65
  }
74
66
  }
75
- /**
76
- * Fetch access token from local credential store.
77
- *
78
- * @returns Access token string or undefined if not available
79
- */
67
+ /** Fetch access token from local credential store */
80
68
  async function fetchFromLocal(agentId) {
81
69
  try {
82
70
  const token = await getAgentAccessToken(agentId);
@@ -92,55 +80,46 @@ async function fetchFromLocal(agentId) {
92
80
  return undefined;
93
81
  }
94
82
  /**
95
- * Get access token for a service.
96
- *
97
- * Uses the configured credential source for the service:
98
- * - "local": Fetch from local axauth credential store
99
- * - "vault": Fetch from axvault server (requires credential name)
100
- * - "auto": Try vault if configured and name provided, fallback to local
83
+ * Get access token for a specific service instance.
101
84
  *
102
- * @param service - Service ID (e.g., "claude", "codex", "gemini")
103
- * @returns Access token string or undefined if not available
104
- *
105
- * @example
106
- * const token = await getServiceAccessToken("claude");
107
- * if (!token) {
108
- * console.error("No credentials found for Claude");
109
- * }
85
+ * Returns vault metadata (displayName) alongside the token
86
+ * for multi-instance identification.
110
87
  */
111
- async function getServiceAccessToken(service) {
112
- const config = getServiceSourceConfig(service);
88
+ async function getInstanceAccessToken(service, config) {
113
89
  const agentId = service;
114
90
  switch (config.source) {
115
91
  case "local": {
116
- return fetchFromLocal(agentId);
92
+ const token = await fetchFromLocal(agentId);
93
+ return { token, vaultDisplayName: undefined };
117
94
  }
118
95
  case "vault": {
119
96
  if (!config.name) {
120
97
  console.error(`[axusage] Vault source requires credential name for ${service}. ` +
121
98
  `Set {"${service}": {"source": "vault", "name": "your-name"}} in config.`);
122
- return undefined;
99
+ return { token: undefined, vaultDisplayName: undefined };
123
100
  }
124
- const token = await fetchFromVault(agentId, config.name);
125
- if (!token) {
126
- // User explicitly selected vault but it failed - provide clear feedback
101
+ const result = await fetchFromVaultWithMetadata(agentId, config.name);
102
+ if (!result.token) {
127
103
  console.error(`[axusage] Vault credential fetch failed for ${service}. ` +
128
104
  `Check that vault is configured (AXVAULT env) and credential "${config.name}" exists.`);
129
105
  }
130
- return token;
106
+ return result;
131
107
  }
132
108
  case "auto": {
133
- // Auto mode: try vault first if configured and name provided
134
- if (config.name && isVaultConfigured()) {
135
- const vaultToken = await fetchFromVault(agentId, config.name);
136
- if (vaultToken) {
137
- return vaultToken;
109
+ if (config.name) {
110
+ // Named credential: vault-only to avoid silently returning
111
+ // the same local token for multiple instances
112
+ if (!isVaultConfigured()) {
113
+ console.error(`[axusage] Named credential "${config.name}" for ${service} requires vault, ` +
114
+ `but vault is not configured. Set AXVAULT env or use source "local" instead.`);
115
+ return { token: undefined, vaultDisplayName: undefined };
138
116
  }
139
- // Fallback to local if vault failed
117
+ return fetchFromVaultWithMetadata(agentId, config.name);
140
118
  }
141
- // No credential name or vault not configured: use local only
142
- return fetchFromLocal(agentId);
119
+ // No credential name: use local
120
+ const token = await fetchFromLocal(agentId);
121
+ return { token, vaultDisplayName: undefined };
143
122
  }
144
123
  }
145
124
  }
146
- export { getServiceAccessToken };
125
+ export { getInstanceAccessToken };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Pure functions for resolving display names for service instances.
3
+ *
4
+ * Priority: config displayName > vault displayName > auto-number (multi) / adapter default (single)
5
+ */
6
+ /**
7
+ * Resolve the display name for a service instance.
8
+ *
9
+ * For single-instance configs: config displayName > vault displayName > adapter default name
10
+ * For multi-instance without displayName: "Claude #1", "Claude #2"
11
+ */
12
+ declare function resolveInstanceDisplayName(configDisplayName: string | undefined, vaultDisplayName: string | undefined, defaultName: string, index: number, total: number): string;
13
+ export { resolveInstanceDisplayName };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Pure functions for resolving display names for service instances.
3
+ *
4
+ * Priority: config displayName > vault displayName > auto-number (multi) / adapter default (single)
5
+ */
6
+ /**
7
+ * Resolve the display name for a service instance.
8
+ *
9
+ * For single-instance configs: config displayName > vault displayName > adapter default name
10
+ * For multi-instance without displayName: "Claude #1", "Claude #2"
11
+ */
12
+ function resolveInstanceDisplayName(configDisplayName, vaultDisplayName, defaultName, index, total) {
13
+ // Explicit displayName always wins
14
+ if (configDisplayName)
15
+ return configDisplayName;
16
+ if (vaultDisplayName)
17
+ return vaultDisplayName;
18
+ // Single instance: use adapter default
19
+ if (total === 1)
20
+ return defaultName;
21
+ // Multi-instance without displayName: auto-number
22
+ return `${defaultName} #${String(index + 1)}`;
23
+ }
24
+ export { resolveInstanceDisplayName };
@@ -1,17 +1,8 @@
1
- import type { ServiceAdapter } from "../types/domain.js";
1
+ import type { ServiceUsageFetcher } from "../types/domain.js";
2
2
  /**
3
- * Registry of available service adapters
3
+ * Get a token-based usage fetcher by service type
4
4
  */
5
- export declare const SERVICE_ADAPTERS: {
6
- readonly claude: ServiceAdapter;
7
- readonly codex: ServiceAdapter;
8
- readonly copilot: ServiceAdapter;
9
- readonly gemini: ServiceAdapter;
10
- };
11
- /**
12
- * Get a service adapter by name
13
- */
14
- export declare function getServiceAdapter(name: string): ServiceAdapter | undefined;
5
+ export declare function getServiceUsageFetcher(name: string): ServiceUsageFetcher | undefined;
15
6
  /**
16
7
  * Get all available service names
17
8
  */
@@ -1,26 +1,26 @@
1
- import { codexAdapter } from "../adapters/codex.js";
2
- import { claudeAdapter } from "../adapters/claude.js";
3
- import { geminiAdapter } from "../adapters/gemini.js";
4
- import { copilotAdapter } from "../adapters/copilot.js";
1
+ import { codexUsageFetcher } from "../adapters/codex.js";
2
+ import { claudeUsageFetcher } from "../adapters/claude.js";
3
+ import { geminiUsageFetcher } from "../adapters/gemini.js";
4
+ import { copilotUsageFetcher } from "../adapters/copilot.js";
5
5
  /**
6
- * Registry of available service adapters
6
+ * Registry of token-based usage fetchers
7
7
  */
8
- export const SERVICE_ADAPTERS = {
9
- claude: claudeAdapter,
10
- codex: codexAdapter,
11
- copilot: copilotAdapter,
12
- gemini: geminiAdapter,
8
+ const SERVICE_USAGE_FETCHERS = {
9
+ claude: claudeUsageFetcher,
10
+ codex: codexUsageFetcher,
11
+ copilot: copilotUsageFetcher,
12
+ gemini: geminiUsageFetcher,
13
13
  };
14
14
  /**
15
- * Get a service adapter by name
15
+ * Get a token-based usage fetcher by service type
16
16
  */
17
- export function getServiceAdapter(name) {
17
+ export function getServiceUsageFetcher(name) {
18
18
  const key = name.toLowerCase();
19
- return SERVICE_ADAPTERS[key];
19
+ return SERVICE_USAGE_FETCHERS[key];
20
20
  }
21
21
  /**
22
22
  * Get all available service names
23
23
  */
24
24
  export function getAvailableServices() {
25
- return Object.keys(SERVICE_ADAPTERS);
25
+ return Object.keys(SERVICE_USAGE_FETCHERS);
26
26
  }
@@ -14,7 +14,12 @@ export type UsageWindow = {
14
14
  * Complete usage data for a service
15
15
  */
16
16
  export type ServiceUsageData = {
17
+ /** Display name (may be overridden by instance displayName) */
17
18
  readonly service: string;
19
+ /** Stable machine key (e.g., "claude", "codex") for filtering and labeling */
20
+ readonly serviceType: string;
21
+ /** Stable per-instance identifier for metrics (derived from credential name or config key) */
22
+ readonly instanceId?: string;
18
23
  readonly planType?: string;
19
24
  readonly windows: readonly UsageWindow[];
20
25
  readonly metadata?: {
@@ -41,11 +46,12 @@ export declare class ApiError extends Error {
41
46
  constructor(message: string, status?: number, body?: unknown);
42
47
  }
43
48
  /**
44
- * Service adapter interface
49
+ * Token-based usage fetcher for a service.
50
+ * Tokens are resolved externally via credential sources.
45
51
  */
46
- export interface ServiceAdapter {
52
+ export interface ServiceUsageFetcher {
47
53
  readonly name: string;
48
- fetchUsage(): Promise<Result<ServiceUsageData, ApiError>>;
54
+ fetchUsageWithToken(accessToken: string): Promise<Result<ServiceUsageData, ApiError>>;
49
55
  }
50
56
  /**
51
57
  * Result of fetching usage for a single service.
@@ -6,4 +6,4 @@
6
6
  * Rate is shown after either {@link MIN_ELAPSED_FRACTION} of the period OR
7
7
  * {@link MIN_ELAPSED_TIME_MS} has passed (whichever comes first).
8
8
  */
9
- export declare function calculateUsageRate(utilization: number, resetsAt: Date | undefined, periodDurationMs: number): number | undefined;
9
+ export declare function calculateUsageRate(utilization: number, resetsAt: Date | undefined, periodDurationMs: number, now: number): number | undefined;
@@ -10,12 +10,11 @@ const MIN_ELAPSED_TIME_MS = 2 * 60 * 60 * 1000;
10
10
  * Rate is shown after either {@link MIN_ELAPSED_FRACTION} of the period OR
11
11
  * {@link MIN_ELAPSED_TIME_MS} has passed (whichever comes first).
12
12
  */
13
- export function calculateUsageRate(utilization, resetsAt, periodDurationMs) {
13
+ export function calculateUsageRate(utilization, resetsAt, periodDurationMs, now) {
14
14
  if (!resetsAt)
15
15
  return undefined;
16
16
  if (periodDurationMs <= 0)
17
17
  return 0;
18
- const now = Date.now();
19
18
  const resetTime = resetsAt.getTime();
20
19
  const periodStart = resetTime - periodDurationMs;
21
20
  const elapsedTime = now - periodStart;
@@ -1,6 +1,6 @@
1
1
  import type { ServiceUsageData } from "../types/domain.js";
2
2
  /**
3
3
  * Formats service usage data as Prometheus text exposition using prom-client.
4
- * Emits a gauge `axusage_utilization_percent{service,window}` per window.
4
+ * Emits gauges `axusage_utilization_percent` and `axusage_usage_rate` per window.
5
5
  */
6
- export declare function formatPrometheusMetrics(data: readonly ServiceUsageData[]): Promise<string>;
6
+ export declare function formatPrometheusMetrics(data: readonly ServiceUsageData[], now: number): Promise<string>;
@@ -1,19 +1,36 @@
1
1
  import { Gauge, Registry } from "prom-client";
2
+ import { calculateUsageRate } from "./calculate-usage-rate.js";
2
3
  /**
3
4
  * Formats service usage data as Prometheus text exposition using prom-client.
4
- * Emits a gauge `axusage_utilization_percent{service,window}` per window.
5
+ * Emits gauges `axusage_utilization_percent` and `axusage_usage_rate` per window.
5
6
  */
6
- export async function formatPrometheusMetrics(data) {
7
+ export async function formatPrometheusMetrics(data, now) {
7
8
  const registry = new Registry();
8
- const gauge = new Gauge({
9
+ const utilizationGauge = new Gauge({
9
10
  name: "axusage_utilization_percent",
10
11
  help: "Current utilization percentage by service/window",
11
- labelNames: ["service", "window"],
12
+ labelNames: ["service", "service_type", "instance_id", "window"],
13
+ registers: [registry],
14
+ });
15
+ const rateGauge = new Gauge({
16
+ name: "axusage_usage_rate",
17
+ help: "Usage rate (utilization / elapsed fraction of period); >1 means over budget",
18
+ labelNames: ["service", "service_type", "instance_id", "window"],
12
19
  registers: [registry],
13
20
  });
14
21
  for (const entry of data) {
15
22
  for (const w of entry.windows) {
16
- gauge.set({ service: entry.service, window: w.name }, w.utilization);
23
+ const labels = {
24
+ service: entry.service,
25
+ service_type: entry.serviceType,
26
+ instance_id: entry.instanceId ?? entry.serviceType,
27
+ window: w.name,
28
+ };
29
+ utilizationGauge.set(labels, w.utilization);
30
+ const rate = calculateUsageRate(w.utilization, w.resetsAt, w.periodDurationMs, now);
31
+ if (rate !== undefined) {
32
+ rateGauge.set(labels, rate);
33
+ }
17
34
  }
18
35
  }
19
36
  return registry.metrics();
@@ -6,7 +6,7 @@ export declare function formatServiceUsageData(data: ServiceUsageData): string;
6
6
  /**
7
7
  * Converts service usage data to a plain JSON-serializable object
8
8
  */
9
- export declare function toJsonObject(data: ServiceUsageData): unknown;
9
+ export declare function toJsonObject(data: ServiceUsageData, now: number): unknown;
10
10
  /**
11
11
  * Formats service usage data as JSON string
12
12
  */