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.
- package/README.md +22 -3
- package/dist/adapters/claude.d.ts +2 -9
- package/dist/adapters/claude.js +40 -53
- package/dist/adapters/codex.d.ts +2 -8
- package/dist/adapters/codex.js +30 -42
- package/dist/adapters/copilot.d.ts +2 -7
- package/dist/adapters/copilot.js +32 -43
- package/dist/adapters/gemini.d.ts +2 -8
- package/dist/adapters/gemini.js +26 -38
- package/dist/adapters/parse-claude-usage.js +1 -0
- package/dist/adapters/parse-codex-usage.js +1 -0
- package/dist/adapters/parse-copilot-usage.js +2 -0
- package/dist/adapters/parse-gemini-usage.js +1 -0
- package/dist/cli.js +1 -1
- package/dist/commands/fetch-service-usage.d.ts +8 -2
- package/dist/commands/fetch-service-usage.js +76 -9
- package/dist/commands/serve-command.js +5 -2
- package/dist/commands/usage-command.d.ts +1 -0
- package/dist/commands/usage-command.js +8 -8
- package/dist/config/credential-sources.d.ts +16 -11
- package/dist/config/credential-sources.js +48 -27
- package/dist/server/routes.js +6 -2
- package/dist/services/get-instance-access-token.d.ts +20 -0
- package/dist/services/{get-service-access-token.js → get-instance-access-token.js} +31 -52
- package/dist/services/resolve-service-instances.d.ts +13 -0
- package/dist/services/resolve-service-instances.js +24 -0
- package/dist/services/service-adapter-registry.d.ts +3 -12
- package/dist/services/service-adapter-registry.js +14 -14
- package/dist/types/domain.d.ts +9 -3
- package/dist/utils/calculate-usage-rate.d.ts +1 -1
- package/dist/utils/calculate-usage-rate.js +1 -2
- package/dist/utils/format-prometheus-metrics.d.ts +2 -2
- package/dist/utils/format-prometheus-metrics.js +22 -5
- package/dist/utils/format-service-usage.d.ts +1 -1
- package/dist/utils/format-service-usage.js +26 -17
- package/package.json +6 -6
- 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 {
|
|
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
|
-
|
|
11
|
-
|
|
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":
|
|
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
|
|
19
|
-
interface
|
|
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
|
-
|
|
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":
|
|
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
|
-
/**
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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 };
|
package/dist/server/routes.js
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
2
|
+
* Instance-aware credential fetcher.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
103
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
106
|
+
return result;
|
|
131
107
|
}
|
|
132
108
|
case "auto": {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
|
|
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
|
-
|
|
117
|
+
return fetchFromVaultWithMetadata(agentId, config.name);
|
|
140
118
|
}
|
|
141
|
-
// No credential name
|
|
142
|
-
|
|
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 {
|
|
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 {
|
|
1
|
+
import type { ServiceUsageFetcher } from "../types/domain.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Get a token-based usage fetcher by service type
|
|
4
4
|
*/
|
|
5
|
-
export declare
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
|
6
|
+
* Registry of token-based usage fetchers
|
|
7
7
|
*/
|
|
8
|
-
|
|
9
|
-
claude:
|
|
10
|
-
codex:
|
|
11
|
-
copilot:
|
|
12
|
-
gemini:
|
|
8
|
+
const SERVICE_USAGE_FETCHERS = {
|
|
9
|
+
claude: claudeUsageFetcher,
|
|
10
|
+
codex: codexUsageFetcher,
|
|
11
|
+
copilot: copilotUsageFetcher,
|
|
12
|
+
gemini: geminiUsageFetcher,
|
|
13
13
|
};
|
|
14
14
|
/**
|
|
15
|
-
* Get a
|
|
15
|
+
* Get a token-based usage fetcher by service type
|
|
16
16
|
*/
|
|
17
|
-
export function
|
|
17
|
+
export function getServiceUsageFetcher(name) {
|
|
18
18
|
const key = name.toLowerCase();
|
|
19
|
-
return
|
|
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(
|
|
25
|
+
return Object.keys(SERVICE_USAGE_FETCHERS);
|
|
26
26
|
}
|
package/dist/types/domain.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
49
|
+
* Token-based usage fetcher for a service.
|
|
50
|
+
* Tokens are resolved externally via credential sources.
|
|
45
51
|
*/
|
|
46
|
-
export interface
|
|
52
|
+
export interface ServiceUsageFetcher {
|
|
47
53
|
readonly name: string;
|
|
48
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
*/
|