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
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
|
{
|
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
|
}
|
|
@@ -97,12 +97,15 @@ export async function serveCommand(options) {
|
|
|
97
97
|
process.exit(1);
|
|
98
98
|
}, 5000);
|
|
99
99
|
forceExit.unref();
|
|
100
|
-
server
|
|
100
|
+
server
|
|
101
|
+
.stop()
|
|
102
|
+
.finally(() => {
|
|
101
103
|
clearTimeout(forceExit);
|
|
104
|
+
})
|
|
105
|
+
.then(() => {
|
|
102
106
|
// eslint-disable-next-line unicorn/no-process-exit -- CLI graceful shutdown
|
|
103
107
|
process.exit(0);
|
|
104
108
|
}, (error) => {
|
|
105
|
-
clearTimeout(forceExit);
|
|
106
109
|
console.error("Error during shutdown:", error);
|
|
107
110
|
// eslint-disable-next-line unicorn/no-process-exit -- CLI graceful shutdown
|
|
108
111
|
process.exit(1);
|