axusage 3.6.0 → 3.7.1
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 +2 -1
- 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/usage-command.d.ts +1 -0
- package/dist/commands/usage-command.js +4 -5
- package/dist/config/credential-sources.d.ts +16 -11
- package/dist/config/credential-sources.js +48 -27
- 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/format-prometheus-metrics.js +8 -3
- package/dist/utils/format-service-usage.js +4 -1
- package/package.json +6 -6
- package/dist/services/get-service-access-token.d.ts +0 -28
package/README.md
CHANGED
|
@@ -112,12 +112,31 @@ Credential source config is read from:
|
|
|
112
112
|
- Config file path shown in `axusage --help`
|
|
113
113
|
- `AXUSAGE_SOURCES` environment variable (JSON), which overrides file config
|
|
114
114
|
|
|
115
|
+
### Multi-Instance Configuration
|
|
116
|
+
|
|
117
|
+
To monitor multiple accounts for the same service, use an array of instance configs:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"claude": [
|
|
122
|
+
{ "source": "vault", "name": "work", "displayName": "Claude (Work)" },
|
|
123
|
+
{
|
|
124
|
+
"source": "vault",
|
|
125
|
+
"name": "personal",
|
|
126
|
+
"displayName": "Claude (Personal)"
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Each instance resolves credentials independently. Named credentials require vault to be configured (`AXVAULT` env). Single-instance configs can use string shorthand (`"auto"`, `"local"`, `"vault"`) or object form.
|
|
133
|
+
|
|
115
134
|
## Examples
|
|
116
135
|
|
|
117
136
|
### Extract service and utilization (TSV + awk)
|
|
118
137
|
|
|
119
138
|
```bash
|
|
120
|
-
axusage --format tsv | tail -n +2 | awk -F'\t' '{print $1, $
|
|
139
|
+
axusage --format tsv | tail -n +2 | awk -F'\t' '{print $1, $5"%"}'
|
|
121
140
|
```
|
|
122
141
|
|
|
123
142
|
### Count windows by service (TSV + cut/sort/uniq)
|
|
@@ -129,7 +148,7 @@ axusage --format tsv | tail -n +2 | cut -f1 | sort | uniq -c
|
|
|
129
148
|
### Filter by utilization threshold (TSV + awk)
|
|
130
149
|
|
|
131
150
|
```bash
|
|
132
|
-
axusage --format tsv | tail -n +2 | awk -F'\t' '$
|
|
151
|
+
axusage --format tsv | tail -n +2 | awk -F'\t' '$5 > 50 {print $1, $4, $5"%"}'
|
|
133
152
|
```
|
|
134
153
|
|
|
135
154
|
### Extract utilization as JSON (JSON + jq)
|
|
@@ -180,7 +199,7 @@ AXUSAGE_PORT=9090 AXUSAGE_INTERVAL=60 axusage serve
|
|
|
180
199
|
### Endpoints
|
|
181
200
|
|
|
182
201
|
- `GET /metrics` — Prometheus text exposition (`text/plain; version=0.0.4`). Serves cached data immediately; triggers a background refresh when stale. Returns 503 when all services are currently failing.
|
|
183
|
-
- `GET /usage` — JSON array of usage objects (one per service). Waits for a fresh snapshot when stale. Returns 503 if no data is available. Date fields (e.g. `resetsAt`) are serialized as ISO 8601 strings.
|
|
202
|
+
- `GET /usage` — JSON array of usage objects (one per service instance; multi-instance configs produce multiple entries per service type). Waits for a fresh snapshot when stale. Returns 503 if no data is available. Date fields (e.g. `resetsAt`) are serialized as ISO 8601 strings.
|
|
184
203
|
- `GET /health` — JSON health status with version, last refresh time, tracked services, and errors. Always responds immediately from cached state without triggering a refresh.
|
|
185
204
|
|
|
186
205
|
### Container Deployment
|
|
@@ -1,9 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
* Claude service adapter using direct API access.
|
|
4
|
-
*
|
|
5
|
-
* This adapter uses the OAuth token from Claude Code's credential store
|
|
6
|
-
* (Keychain on macOS, credentials file elsewhere) to make direct API calls
|
|
7
|
-
* to the Anthropic usage endpoint.
|
|
8
|
-
*/
|
|
9
|
-
export declare const claudeAdapter: ServiceAdapter;
|
|
1
|
+
import type { ServiceUsageFetcher } from "../types/domain.js";
|
|
2
|
+
export declare const claudeUsageFetcher: ServiceUsageFetcher;
|
package/dist/adapters/claude.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { ApiError } from "../types/domain.js";
|
|
3
|
-
import { getServiceAccessToken } from "../services/get-service-access-token.js";
|
|
4
3
|
import { UsageResponse as UsageResponseSchema } from "../types/usage.js";
|
|
5
4
|
import { coalesceClaudeUsageResponse } from "./coalesce-claude-usage-response.js";
|
|
6
5
|
import { toServiceUsageData } from "./parse-claude-usage.js";
|
|
@@ -33,64 +32,52 @@ async function fetchPlanType(accessToken) {
|
|
|
33
32
|
return undefined;
|
|
34
33
|
}
|
|
35
34
|
}
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
35
|
+
/** Fetch Claude usage data using a pre-resolved access token */
|
|
36
|
+
async function fetchClaudeUsageWithToken(accessToken) {
|
|
37
|
+
try {
|
|
38
|
+
const [response, planType] = await Promise.all([
|
|
39
|
+
fetch(USAGE_API_URL, {
|
|
40
|
+
headers: {
|
|
41
|
+
Authorization: `Bearer ${accessToken}`,
|
|
42
|
+
"anthropic-beta": ANTHROPIC_BETA_HEADER,
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
fetchPlanType(accessToken),
|
|
46
|
+
]);
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const errorText = await response.text().catch(() => "");
|
|
48
49
|
return {
|
|
49
50
|
ok: false,
|
|
50
|
-
error: new ApiError(
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
try {
|
|
54
|
-
const [response, planType] = await Promise.all([
|
|
55
|
-
fetch(USAGE_API_URL, {
|
|
56
|
-
headers: {
|
|
57
|
-
Authorization: `Bearer ${accessToken}`,
|
|
58
|
-
"anthropic-beta": ANTHROPIC_BETA_HEADER,
|
|
59
|
-
},
|
|
60
|
-
}),
|
|
61
|
-
fetchPlanType(accessToken),
|
|
62
|
-
]);
|
|
63
|
-
if (!response.ok) {
|
|
64
|
-
const errorText = await response.text().catch(() => "");
|
|
65
|
-
return {
|
|
66
|
-
ok: false,
|
|
67
|
-
error: new ApiError(`Claude API request failed: ${String(response.status)} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`, response.status),
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
const data = await response.json();
|
|
71
|
-
const parseResult = UsageResponseSchema.safeParse(coalesceClaudeUsageResponse(data) ?? data);
|
|
72
|
-
if (!parseResult.success) {
|
|
73
|
-
/* eslint-disable unicorn/no-null -- JSON.stringify requires null for no replacer */
|
|
74
|
-
console.error("Raw API response:", JSON.stringify(data, null, 2));
|
|
75
|
-
console.error("Validation errors:", JSON.stringify(z.treeifyError(parseResult.error), null, 2));
|
|
76
|
-
/* eslint-enable unicorn/no-null */
|
|
77
|
-
return {
|
|
78
|
-
ok: false,
|
|
79
|
-
error: new ApiError(`Invalid response format: ${parseResult.error.message}`, undefined, data),
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
const usageData = toServiceUsageData(parseResult.data);
|
|
83
|
-
return {
|
|
84
|
-
ok: true,
|
|
85
|
-
value: planType ? { ...usageData, planType } : usageData,
|
|
51
|
+
error: new ApiError(`Claude API request failed: ${String(response.status)} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`, response.status),
|
|
86
52
|
};
|
|
87
53
|
}
|
|
88
|
-
|
|
89
|
-
|
|
54
|
+
const data = await response.json();
|
|
55
|
+
const parseResult = UsageResponseSchema.safeParse(coalesceClaudeUsageResponse(data) ?? data);
|
|
56
|
+
if (!parseResult.success) {
|
|
57
|
+
/* eslint-disable unicorn/no-null -- JSON.stringify requires null for no replacer */
|
|
58
|
+
console.error("Raw API response:", JSON.stringify(data, null, 2));
|
|
59
|
+
console.error("Validation errors:", JSON.stringify(z.treeifyError(parseResult.error), null, 2));
|
|
60
|
+
/* eslint-enable unicorn/no-null */
|
|
90
61
|
return {
|
|
91
62
|
ok: false,
|
|
92
|
-
error: new ApiError(`
|
|
63
|
+
error: new ApiError(`Invalid response format: ${parseResult.error.message}`, undefined, data),
|
|
93
64
|
};
|
|
94
65
|
}
|
|
95
|
-
|
|
66
|
+
const usageData = toServiceUsageData(parseResult.data);
|
|
67
|
+
return {
|
|
68
|
+
ok: true,
|
|
69
|
+
value: planType ? { ...usageData, planType } : usageData,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
error: new ApiError(`Failed to fetch Claude usage: ${message}`),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export const claudeUsageFetcher = {
|
|
81
|
+
name: "Claude",
|
|
82
|
+
fetchUsageWithToken: fetchClaudeUsageWithToken,
|
|
96
83
|
};
|
package/dist/adapters/codex.d.ts
CHANGED
|
@@ -1,8 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
* ChatGPT service adapter using direct API access.
|
|
4
|
-
*
|
|
5
|
-
* Uses the OAuth token from Codex CLI's credential store (~/.codex/auth.json)
|
|
6
|
-
* to make direct API calls to ChatGPT's usage endpoint.
|
|
7
|
-
*/
|
|
8
|
-
export declare const codexAdapter: ServiceAdapter;
|
|
1
|
+
import type { ServiceUsageFetcher } from "../types/domain.js";
|
|
2
|
+
export declare const codexUsageFetcher: ServiceUsageFetcher;
|
package/dist/adapters/codex.js
CHANGED
|
@@ -1,55 +1,43 @@
|
|
|
1
1
|
import { ApiError } from "../types/domain.js";
|
|
2
|
-
import { getServiceAccessToken } from "../services/get-service-access-token.js";
|
|
3
2
|
import { CodexUsageResponse as CodexUsageResponseSchema } from "../types/codex.js";
|
|
4
3
|
import { toServiceUsageData } from "./parse-codex-usage.js";
|
|
5
4
|
const API_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (!accessToken) {
|
|
5
|
+
/** Fetch ChatGPT usage data using a pre-resolved access token */
|
|
6
|
+
async function fetchCodexUsageWithToken(accessToken) {
|
|
7
|
+
try {
|
|
8
|
+
const response = await fetch(API_URL, {
|
|
9
|
+
headers: {
|
|
10
|
+
Authorization: `Bearer ${accessToken}`,
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
const errorText = await response.text().catch(() => "");
|
|
17
15
|
return {
|
|
18
16
|
ok: false,
|
|
19
|
-
error: new ApiError(
|
|
17
|
+
error: new ApiError(`ChatGPT API request failed: ${String(response.status)} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`, response.status),
|
|
20
18
|
};
|
|
21
19
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
Authorization: `Bearer ${accessToken}`,
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
if (!response.ok) {
|
|
29
|
-
const errorText = await response.text().catch(() => "");
|
|
30
|
-
return {
|
|
31
|
-
ok: false,
|
|
32
|
-
error: new ApiError(`ChatGPT API request failed: ${String(response.status)} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`, response.status),
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
const data = await response.json();
|
|
36
|
-
const parseResult = CodexUsageResponseSchema.safeParse(data);
|
|
37
|
-
if (!parseResult.success) {
|
|
38
|
-
return {
|
|
39
|
-
ok: false,
|
|
40
|
-
error: new ApiError(`Invalid response format: ${parseResult.error.message}`, undefined, data),
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
return {
|
|
44
|
-
ok: true,
|
|
45
|
-
value: toServiceUsageData(parseResult.data),
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
20
|
+
const data = await response.json();
|
|
21
|
+
const parseResult = CodexUsageResponseSchema.safeParse(data);
|
|
22
|
+
if (!parseResult.success) {
|
|
49
23
|
return {
|
|
50
24
|
ok: false,
|
|
51
|
-
error: new ApiError(`
|
|
25
|
+
error: new ApiError(`Invalid response format: ${parseResult.error.message}`, undefined, data),
|
|
52
26
|
};
|
|
53
27
|
}
|
|
54
|
-
|
|
28
|
+
return {
|
|
29
|
+
ok: true,
|
|
30
|
+
value: toServiceUsageData(parseResult.data),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
error: new ApiError(`Failed to fetch ChatGPT usage: ${error instanceof Error ? error.message : String(error)}`),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export const codexUsageFetcher = {
|
|
41
|
+
name: "ChatGPT",
|
|
42
|
+
fetchUsageWithToken: fetchCodexUsageWithToken,
|
|
55
43
|
};
|
|
@@ -1,7 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
* GitHub Copilot service adapter using token-based API access.
|
|
4
|
-
*
|
|
5
|
-
* Credentials resolved via getServiceAccessToken (vault, local axauth, gh CLI).
|
|
6
|
-
*/
|
|
7
|
-
export declare const copilotAdapter: ServiceAdapter;
|
|
1
|
+
import type { ServiceUsageFetcher } from "../types/domain.js";
|
|
2
|
+
export declare const copilotUsageFetcher: ServiceUsageFetcher;
|
package/dist/adapters/copilot.js
CHANGED
|
@@ -1,58 +1,47 @@
|
|
|
1
1
|
import { ApiError } from "../types/domain.js";
|
|
2
2
|
import { CopilotUsageResponse as CopilotUsageResponseSchema } from "../types/copilot.js";
|
|
3
3
|
import { toServiceUsageData } from "./parse-copilot-usage.js";
|
|
4
|
-
import { getServiceAccessToken } from "../services/get-service-access-token.js";
|
|
5
4
|
// Internal/undocumented GitHub API used by VS Code, JetBrains, and other
|
|
6
5
|
// first-party Copilot integrations. May change without notice.
|
|
7
6
|
const API_URL = "https://api.github.com/copilot_internal/user";
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (!
|
|
7
|
+
/** Fetch GitHub Copilot usage data using a pre-resolved access token */
|
|
8
|
+
async function fetchCopilotUsageWithToken(accessToken) {
|
|
9
|
+
try {
|
|
10
|
+
const response = await fetch(API_URL, {
|
|
11
|
+
headers: {
|
|
12
|
+
Authorization: `Bearer ${accessToken}`,
|
|
13
|
+
Accept: "application/json",
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
const errorText = await response.text().catch(() => "");
|
|
18
18
|
return {
|
|
19
19
|
ok: false,
|
|
20
|
-
error: new ApiError(
|
|
20
|
+
error: new ApiError(`GitHub Copilot API request failed: ${String(response.status)} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`, response.status),
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
Authorization: `Bearer ${accessToken}`,
|
|
27
|
-
Accept: "application/json",
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
if (!response.ok) {
|
|
31
|
-
const errorText = await response.text().catch(() => "");
|
|
32
|
-
return {
|
|
33
|
-
ok: false,
|
|
34
|
-
error: new ApiError(`GitHub Copilot API request failed: ${String(response.status)} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`, response.status),
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
const data = await response.json();
|
|
38
|
-
const parseResult = CopilotUsageResponseSchema.safeParse(data);
|
|
39
|
-
if (!parseResult.success) {
|
|
40
|
-
return {
|
|
41
|
-
ok: false,
|
|
42
|
-
error: new ApiError(`Invalid response format: ${parseResult.error.message}`, undefined, data),
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
return {
|
|
46
|
-
ok: true,
|
|
47
|
-
value: toServiceUsageData(parseResult.data),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
23
|
+
const data = await response.json();
|
|
24
|
+
const parseResult = CopilotUsageResponseSchema.safeParse(data);
|
|
25
|
+
if (!parseResult.success) {
|
|
52
26
|
return {
|
|
53
27
|
ok: false,
|
|
54
|
-
error: new ApiError(`
|
|
28
|
+
error: new ApiError(`Invalid response format: ${parseResult.error.message}`, undefined, data),
|
|
55
29
|
};
|
|
56
30
|
}
|
|
57
|
-
|
|
31
|
+
return {
|
|
32
|
+
ok: true,
|
|
33
|
+
value: toServiceUsageData(parseResult.data),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
error: new ApiError(`Failed to fetch GitHub Copilot usage: ${message}`),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export const copilotUsageFetcher = {
|
|
45
|
+
name: "GitHub Copilot",
|
|
46
|
+
fetchUsageWithToken: fetchCopilotUsageWithToken,
|
|
58
47
|
};
|
|
@@ -1,8 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
* Gemini service adapter using direct API access.
|
|
4
|
-
*
|
|
5
|
-
* Uses the OAuth token from Gemini CLI's credential store (~/.gemini/oauth_creds.json)
|
|
6
|
-
* to make direct API calls to Google's quota endpoint.
|
|
7
|
-
*/
|
|
8
|
-
export declare const geminiAdapter: ServiceAdapter;
|
|
1
|
+
import type { ServiceUsageFetcher } from "../types/domain.js";
|
|
2
|
+
export declare const geminiUsageFetcher: ServiceUsageFetcher;
|
package/dist/adapters/gemini.js
CHANGED
|
@@ -1,43 +1,31 @@
|
|
|
1
1
|
import { ApiError } from "../types/domain.js";
|
|
2
2
|
import { fetchGeminiQuota, fetchGeminiProject, } from "../services/gemini-api.js";
|
|
3
|
-
import { getServiceAccessToken } from "../services/get-service-access-token.js";
|
|
4
3
|
import { toServiceUsageData } from "./parse-gemini-usage.js";
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const accessToken = await getServiceAccessToken("gemini");
|
|
15
|
-
if (!accessToken) {
|
|
16
|
-
return {
|
|
17
|
-
ok: false,
|
|
18
|
-
error: new ApiError("No Gemini credentials found. Run 'gemini' to authenticate."),
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
try {
|
|
22
|
-
// Discover project ID for more accurate quotas (best effort)
|
|
23
|
-
const projectId = await fetchGeminiProject(accessToken);
|
|
24
|
-
// Fetch quota data
|
|
25
|
-
const quotaResult = await fetchGeminiQuota(accessToken, projectId);
|
|
26
|
-
if (!quotaResult.ok) {
|
|
27
|
-
return quotaResult;
|
|
28
|
-
}
|
|
29
|
-
return {
|
|
30
|
-
ok: true,
|
|
31
|
-
value: toServiceUsageData(quotaResult.value),
|
|
32
|
-
};
|
|
4
|
+
/** Fetch Gemini usage data using a pre-resolved access token */
|
|
5
|
+
async function fetchGeminiUsageWithToken(accessToken) {
|
|
6
|
+
try {
|
|
7
|
+
// Discover project ID for more accurate quotas (best effort)
|
|
8
|
+
const projectId = await fetchGeminiProject(accessToken);
|
|
9
|
+
// Fetch quota data
|
|
10
|
+
const quotaResult = await fetchGeminiQuota(accessToken, projectId);
|
|
11
|
+
if (!quotaResult.ok) {
|
|
12
|
+
return quotaResult;
|
|
33
13
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
14
|
+
return {
|
|
15
|
+
ok: true,
|
|
16
|
+
value: toServiceUsageData(quotaResult.value),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
return {
|
|
21
|
+
ok: false,
|
|
22
|
+
error: error instanceof ApiError
|
|
23
|
+
? error
|
|
24
|
+
: new ApiError(`Failed to fetch Gemini usage: ${error instanceof Error ? error.message : String(error)}`),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export const geminiUsageFetcher = {
|
|
29
|
+
name: "Gemini",
|
|
30
|
+
fetchUsageWithToken: fetchGeminiUsageWithToken,
|
|
43
31
|
};
|
|
@@ -15,6 +15,7 @@ export function toUsageWindow(name, window) {
|
|
|
15
15
|
export function toServiceUsageData(response) {
|
|
16
16
|
return {
|
|
17
17
|
service: "ChatGPT",
|
|
18
|
+
serviceType: "codex",
|
|
18
19
|
planType: response.plan_type,
|
|
19
20
|
windows: [
|
|
20
21
|
toUsageWindow("Primary Window (~5 hours)", response.rate_limit.primary_window),
|
|
@@ -29,6 +29,7 @@ export function toServiceUsageData(response) {
|
|
|
29
29
|
if (premium_interactions.unlimited) {
|
|
30
30
|
return {
|
|
31
31
|
service: "GitHub Copilot",
|
|
32
|
+
serviceType: "copilot",
|
|
32
33
|
planType: response.copilot_plan,
|
|
33
34
|
windows: [
|
|
34
35
|
{
|
|
@@ -48,6 +49,7 @@ export function toServiceUsageData(response) {
|
|
|
48
49
|
: (used / premium_interactions.entitlement) * 100;
|
|
49
50
|
return {
|
|
50
51
|
service: "GitHub Copilot",
|
|
52
|
+
serviceType: "copilot",
|
|
51
53
|
planType: response.copilot_plan,
|
|
52
54
|
windows: [
|
|
53
55
|
{
|
|
@@ -104,7 +104,7 @@ export function groupByQuotaPool(modelQuotas) {
|
|
|
104
104
|
}
|
|
105
105
|
// Sort model IDs within each pool for consistent ordering
|
|
106
106
|
for (const pool of poolMap.values()) {
|
|
107
|
-
pool.modelIds.
|
|
107
|
+
pool.modelIds = pool.modelIds.toSorted();
|
|
108
108
|
}
|
|
109
109
|
return [...poolMap.values()];
|
|
110
110
|
}
|
|
@@ -145,6 +145,7 @@ export function toServiceUsageData(response, planType) {
|
|
|
145
145
|
const windows = quotaPools.map((pool) => poolToUsageWindow(pool));
|
|
146
146
|
return {
|
|
147
147
|
service: "Gemini",
|
|
148
|
+
serviceType: "gemini",
|
|
148
149
|
planType,
|
|
149
150
|
windows,
|
|
150
151
|
};
|
package/dist/cli.js
CHANGED
|
@@ -28,7 +28,7 @@ const program = new Command()
|
|
|
28
28
|
.default("text"))
|
|
29
29
|
.option("--auth-setup <service>", "set up authentication for a service (directs to appropriate CLI)")
|
|
30
30
|
.option("--auth-status [service]", "check authentication status for services")
|
|
31
|
-
.addHelpText("after", () => `\nExamples:\n # Fetch usage for all services\n ${packageJson.name}\n\n # JSON output for a single service\n ${packageJson.name} --service claude --format json\n\n # TSV output for piping to cut, awk, sort\n ${packageJson.name} --format tsv | tail -n +2 | awk -F'\\t' '{print $1, $
|
|
31
|
+
.addHelpText("after", () => `\nExamples:\n # Fetch usage for all services\n ${packageJson.name}\n\n # JSON output for a single service\n ${packageJson.name} --service claude --format json\n\n # TSV output for piping to cut, awk, sort\n ${packageJson.name} --format tsv | tail -n +2 | awk -F'\\t' '{print $1, $5"%"}'\n\n # Filter Prometheus metrics with standard tools\n ${packageJson.name} --format prometheus | grep axusage_utilization_percent\n\n # Check authentication status for all services\n ${packageJson.name} --auth-status\n\nSources config file: ${getCredentialSourcesPath()}\n(or set AXUSAGE_SOURCES to JSON to bypass file)\n\n${formatRequiresHelpText()}\nOverride CLI paths: AXUSAGE_CLAUDE_PATH, AXUSAGE_CODEX_PATH, AXUSAGE_GEMINI_PATH, AXUSAGE_GH_PATH\n`);
|
|
32
32
|
program
|
|
33
33
|
.command("serve")
|
|
34
34
|
.description("Start HTTP server exposing Prometheus metrics at /metrics and usage JSON at /usage")
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ServiceResult } from "../types/domain.js";
|
|
2
2
|
export type UsageCommandOptions = {
|
|
3
3
|
readonly service?: string;
|
|
4
4
|
readonly format?: "text" | "tsv" | "json" | "prometheus";
|
|
5
5
|
};
|
|
6
6
|
export declare function selectServicesToQuery(service?: string): string[];
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Fetch usage for all instances of a service type.
|
|
9
|
+
*
|
|
10
|
+
* Resolves tokens and fetches usage for each configured instance in parallel.
|
|
11
|
+
* Produces N results with resolved display names and stable instance IDs.
|
|
12
|
+
*/
|
|
13
|
+
export declare function fetchServiceInstanceUsage(serviceType: string): Promise<ServiceResult[]>;
|
|
@@ -1,20 +1,87 @@
|
|
|
1
1
|
import { ApiError as ApiErrorClass } from "../types/domain.js";
|
|
2
|
-
import { getAvailableServices,
|
|
2
|
+
import { getAvailableServices, getServiceUsageFetcher, } from "../services/service-adapter-registry.js";
|
|
3
|
+
import { getServiceInstanceConfigs } from "../config/credential-sources.js";
|
|
4
|
+
import { getInstanceAccessToken } from "../services/get-instance-access-token.js";
|
|
5
|
+
import { resolveInstanceDisplayName } from "../services/resolve-service-instances.js";
|
|
3
6
|
export function selectServicesToQuery(service) {
|
|
4
7
|
const normalized = service?.toLowerCase();
|
|
5
8
|
if (!service || normalized === "all")
|
|
6
9
|
return getAvailableServices();
|
|
7
10
|
return [service];
|
|
8
11
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Derive a stable instance identifier for metrics labeling.
|
|
14
|
+
* Uses credential name when available, otherwise falls back to service type.
|
|
15
|
+
*/
|
|
16
|
+
function deriveInstanceId(serviceType, credentialName, index, total) {
|
|
17
|
+
if (credentialName)
|
|
18
|
+
return credentialName;
|
|
19
|
+
if (total === 1)
|
|
20
|
+
return serviceType;
|
|
21
|
+
return `${serviceType}-${String(index + 1)}`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Fetch usage for all instances of a service type.
|
|
25
|
+
*
|
|
26
|
+
* Resolves tokens and fetches usage for each configured instance in parallel.
|
|
27
|
+
* Produces N results with resolved display names and stable instance IDs.
|
|
28
|
+
*/
|
|
29
|
+
export async function fetchServiceInstanceUsage(serviceType) {
|
|
30
|
+
const normalized = serviceType.toLowerCase();
|
|
31
|
+
const fetcher = getServiceUsageFetcher(normalized);
|
|
32
|
+
if (!fetcher) {
|
|
12
33
|
const available = getAvailableServices().join(", ");
|
|
34
|
+
return [
|
|
35
|
+
{
|
|
36
|
+
service: serviceType,
|
|
37
|
+
result: {
|
|
38
|
+
ok: false,
|
|
39
|
+
error: new ApiErrorClass(`Unknown service "${serviceType}". Supported services: ${available}. ` +
|
|
40
|
+
"Run 'axusage --help' for usage."),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
const instanceConfigs = getServiceInstanceConfigs(normalized);
|
|
46
|
+
const results = await Promise.all(instanceConfigs.map(async (config, index) => {
|
|
47
|
+
const tokenResult = await getInstanceAccessToken(normalized, config);
|
|
48
|
+
if (!tokenResult.token) {
|
|
49
|
+
const label = config.name ?? normalized;
|
|
50
|
+
const isVaultPath = config.source === "vault" ||
|
|
51
|
+
(config.source === "auto" && config.name !== undefined);
|
|
52
|
+
return {
|
|
53
|
+
service: normalized,
|
|
54
|
+
result: {
|
|
55
|
+
ok: false,
|
|
56
|
+
error: new ApiErrorClass(`No credentials found for ${label}. ` +
|
|
57
|
+
(isVaultPath
|
|
58
|
+
? config.name
|
|
59
|
+
? `Check that vault is configured and credential "${config.name}" exists.`
|
|
60
|
+
: `Check that vault is configured. Set a credential name in config.`
|
|
61
|
+
: `Run the agent CLI to authenticate.`)),
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const usageResult = await fetcher.fetchUsageWithToken(tokenResult.token);
|
|
66
|
+
if (usageResult.ok) {
|
|
67
|
+
const displayName = resolveInstanceDisplayName(config.displayName, tokenResult.vaultDisplayName, fetcher.name, index, instanceConfigs.length);
|
|
68
|
+
const instanceId = deriveInstanceId(normalized, config.name, index, instanceConfigs.length);
|
|
69
|
+
return {
|
|
70
|
+
service: normalized,
|
|
71
|
+
result: {
|
|
72
|
+
ok: true,
|
|
73
|
+
value: {
|
|
74
|
+
...usageResult.value,
|
|
75
|
+
service: displayName,
|
|
76
|
+
instanceId,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
13
81
|
return {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"Run 'axusage --help' for usage."),
|
|
82
|
+
service: normalized,
|
|
83
|
+
result: usageResult,
|
|
17
84
|
};
|
|
18
|
-
}
|
|
19
|
-
return
|
|
85
|
+
}));
|
|
86
|
+
return results;
|
|
20
87
|
}
|
|
@@ -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
|
|
@@ -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 };
|
|
@@ -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.
|
|
@@ -9,18 +9,23 @@ export async function formatPrometheusMetrics(data, now) {
|
|
|
9
9
|
const utilizationGauge = new Gauge({
|
|
10
10
|
name: "axusage_utilization_percent",
|
|
11
11
|
help: "Current utilization percentage by service/window",
|
|
12
|
-
labelNames: ["service", "window"],
|
|
12
|
+
labelNames: ["service", "service_type", "instance_id", "window"],
|
|
13
13
|
registers: [registry],
|
|
14
14
|
});
|
|
15
15
|
const rateGauge = new Gauge({
|
|
16
16
|
name: "axusage_usage_rate",
|
|
17
17
|
help: "Usage rate (utilization / elapsed fraction of period); >1 means over budget",
|
|
18
|
-
labelNames: ["service", "window"],
|
|
18
|
+
labelNames: ["service", "service_type", "instance_id", "window"],
|
|
19
19
|
registers: [registry],
|
|
20
20
|
});
|
|
21
21
|
for (const entry of data) {
|
|
22
22
|
for (const w of entry.windows) {
|
|
23
|
-
const labels = {
|
|
23
|
+
const labels = {
|
|
24
|
+
service: entry.service,
|
|
25
|
+
service_type: entry.serviceType,
|
|
26
|
+
instance_id: entry.instanceId ?? entry.serviceType,
|
|
27
|
+
window: w.name,
|
|
28
|
+
};
|
|
24
29
|
utilizationGauge.set(labels, w.utilization);
|
|
25
30
|
const rate = calculateUsageRate(w.utilization, w.resetsAt, w.periodDurationMs, now);
|
|
26
31
|
if (rate !== undefined) {
|
|
@@ -71,6 +71,8 @@ export function formatServiceUsageData(data) {
|
|
|
71
71
|
export function toJsonObject(data, now) {
|
|
72
72
|
return {
|
|
73
73
|
service: data.service,
|
|
74
|
+
serviceType: data.serviceType,
|
|
75
|
+
...(data.instanceId !== undefined && { instanceId: data.instanceId }),
|
|
74
76
|
planType: data.planType,
|
|
75
77
|
windows: data.windows.map((w) => {
|
|
76
78
|
const rate = calculateUsageRate(w.utilization, w.resetsAt, w.periodDurationMs, now);
|
|
@@ -92,7 +94,7 @@ export function formatServiceUsageDataAsJson(data) {
|
|
|
92
94
|
// eslint-disable-next-line unicorn/no-null -- JSON.stringify requires null for no replacer
|
|
93
95
|
return JSON.stringify(toJsonObject(data, Date.now()), null, 2);
|
|
94
96
|
}
|
|
95
|
-
const TSV_HEADER = "SERVICE\tPLAN\tWINDOW\tUTILIZATION\tRATE\tRESETS_AT";
|
|
97
|
+
const TSV_HEADER = "SERVICE\tSERVICE_TYPE\tPLAN\tWINDOW\tUTILIZATION\tRATE\tRESETS_AT";
|
|
96
98
|
/**
|
|
97
99
|
* Sanitizes a string for TSV output by replacing tabs and newlines with spaces.
|
|
98
100
|
*/
|
|
@@ -108,6 +110,7 @@ function formatServiceUsageRowsAsTsv(data) {
|
|
|
108
110
|
const rate = calculateUsageRate(w.utilization, w.resetsAt, w.periodDurationMs, Date.now());
|
|
109
111
|
return [
|
|
110
112
|
sanitizeForTsv(data.service),
|
|
113
|
+
sanitizeForTsv(data.serviceType),
|
|
111
114
|
sanitizeForTsv(data.planType ?? "-"),
|
|
112
115
|
sanitizeForTsv(w.name),
|
|
113
116
|
w.utilization.toFixed(2),
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "axusage",
|
|
3
3
|
"author": "Łukasz Jerciński",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "3.
|
|
5
|
+
"version": "3.7.1",
|
|
6
6
|
"description": "Monitor API usage across Claude, ChatGPT, GitHub Copilot, and Gemini from a single CLI",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -47,13 +47,13 @@
|
|
|
47
47
|
"llm",
|
|
48
48
|
"monitoring"
|
|
49
49
|
],
|
|
50
|
-
"packageManager": "pnpm@10.30.
|
|
50
|
+
"packageManager": "pnpm@10.30.2",
|
|
51
51
|
"engines": {
|
|
52
52
|
"node": ">=22.14.0"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@commander-js/extra-typings": "^14.0.0",
|
|
56
|
-
"axauth": "^3.
|
|
56
|
+
"axauth": "^3.2.0",
|
|
57
57
|
"chalk": "^5.6.2",
|
|
58
58
|
"commander": "^14.0.3",
|
|
59
59
|
"conf": "^15.1.0",
|
|
@@ -65,10 +65,10 @@
|
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@total-typescript/ts-reset": "^0.6.1",
|
|
67
67
|
"@types/express": "^5.0.6",
|
|
68
|
-
"@types/node": "^25.3.
|
|
68
|
+
"@types/node": "^25.3.1",
|
|
69
69
|
"@vitest/coverage-v8": "^4.0.18",
|
|
70
|
-
"eslint": "^10.0.
|
|
71
|
-
"eslint-config-axkit": "^1.
|
|
70
|
+
"eslint": "^10.0.2",
|
|
71
|
+
"eslint-config-axkit": "^1.3.0",
|
|
72
72
|
"fta-check": "^1.5.1",
|
|
73
73
|
"fta-cli": "^3.0.0",
|
|
74
74
|
"knip": "^5.85.0",
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified credential fetcher for services.
|
|
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
|
|
8
|
-
*/
|
|
9
|
-
import type { SupportedService } from "./supported-service.js";
|
|
10
|
-
/**
|
|
11
|
-
* Get access token for a service.
|
|
12
|
-
*
|
|
13
|
-
* Uses the configured credential source for the service:
|
|
14
|
-
* - "local": Fetch from local axauth credential store
|
|
15
|
-
* - "vault": Fetch from axvault server (requires credential name)
|
|
16
|
-
* - "auto": Try vault if configured and name provided, fallback to local
|
|
17
|
-
*
|
|
18
|
-
* @param service - Service ID (e.g., "claude", "codex", "gemini")
|
|
19
|
-
* @returns Access token string or undefined if not available
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* const token = await getServiceAccessToken("claude");
|
|
23
|
-
* if (!token) {
|
|
24
|
-
* console.error("No credentials found for Claude");
|
|
25
|
-
* }
|
|
26
|
-
*/
|
|
27
|
-
declare function getServiceAccessToken(service: SupportedService): Promise<string | undefined>;
|
|
28
|
-
export { getServiceAccessToken };
|