@gnahz77/opencode-copilot-multi-auth 0.1.0 → 0.1.2
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 +56 -7
- package/dist/auth.d.ts +18 -0
- package/dist/auth.js +195 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +28 -0
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +6 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +309 -0
- package/dist/models.d.ts +193 -0
- package/dist/models.js +150 -0
- package/dist/pool.d.ts +8 -0
- package/dist/pool.js +157 -0
- package/dist/routing.d.ts +8 -0
- package/dist/routing.js +12 -0
- package/dist/tui.d.ts +5 -0
- package/dist/tui.js +44 -0
- package/dist/types.d.ts +133 -0
- package/dist/types.js +1 -0
- package/dist/usage.d.ts +11 -0
- package/dist/usage.js +113 -0
- package/dist/utils.d.ts +28 -0
- package/dist/utils.js +216 -0
- package/index.mjs +12 -1211
- package/package.json +28 -3
package/dist/pool.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { dirname } from "path";
|
|
4
|
+
import { ACCOUNT_POOL_SCHEMA_VERSION } from "./constants.js";
|
|
5
|
+
import { matchesAnyModelIdPattern, normalizeDomain, normalizeIdSource, normalizeList, normalizePriority, preserveStringOrDefault, } from "./utils.js";
|
|
6
|
+
export function getPoolPath() {
|
|
7
|
+
return `${homedir()}/.local/share/opencode/copilot-auth.json`;
|
|
8
|
+
}
|
|
9
|
+
export function validatePoolSchema(pool, context) {
|
|
10
|
+
if (!pool
|
|
11
|
+
|| typeof pool !== "object"
|
|
12
|
+
|| pool.version !== ACCOUNT_POOL_SCHEMA_VERSION
|
|
13
|
+
|| !Array.isArray(pool.accounts)) {
|
|
14
|
+
throw new Error(`[opencode-copilot-cli-auth] Invalid ${context}: expected { version: ${ACCOUNT_POOL_SCHEMA_VERSION}, accounts: [] } schema.`);
|
|
15
|
+
}
|
|
16
|
+
return pool;
|
|
17
|
+
}
|
|
18
|
+
export function readPool() {
|
|
19
|
+
const poolPath = getPoolPath();
|
|
20
|
+
if (!existsSync(poolPath)) {
|
|
21
|
+
const defaultPool = {
|
|
22
|
+
version: ACCOUNT_POOL_SCHEMA_VERSION,
|
|
23
|
+
accounts: [],
|
|
24
|
+
};
|
|
25
|
+
writePool(defaultPool);
|
|
26
|
+
return defaultPool;
|
|
27
|
+
}
|
|
28
|
+
const raw = readFileSync(poolPath, "utf8");
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = JSON.parse(raw);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
throw new Error(`[opencode-copilot-cli-auth] Malformed JSON in account pool file at ${poolPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
35
|
+
}
|
|
36
|
+
return validatePoolSchema(parsed, `account pool file at ${poolPath}`);
|
|
37
|
+
}
|
|
38
|
+
export function writePool(pool) {
|
|
39
|
+
const validatedPool = validatePoolSchema(pool, "account pool payload");
|
|
40
|
+
const poolPath = getPoolPath();
|
|
41
|
+
const dirPath = dirname(poolPath);
|
|
42
|
+
const tmpPath = `${poolPath}.tmp`;
|
|
43
|
+
mkdirSync(dirPath, { recursive: true });
|
|
44
|
+
writeFileSync(tmpPath, `${JSON.stringify(validatedPool, null, 2)}\n`, "utf8");
|
|
45
|
+
renameSync(tmpPath, poolPath);
|
|
46
|
+
chmodSync(poolPath, 0o600);
|
|
47
|
+
}
|
|
48
|
+
function deriveDefaultAccountId(accounts, key, identity) {
|
|
49
|
+
const userIdText = String(identity.userId);
|
|
50
|
+
const baseId = normalizeIdSource(identity.login || userIdText) || userIdText;
|
|
51
|
+
const idTakenByDifferentKey = accounts.some((account) => account?.id === baseId && account?.key !== key);
|
|
52
|
+
if (!idTakenByDifferentKey) {
|
|
53
|
+
return baseId;
|
|
54
|
+
}
|
|
55
|
+
return `${baseId}-${userIdText.slice(-6)}`;
|
|
56
|
+
}
|
|
57
|
+
export function deriveAccountKey(deployment, userId) {
|
|
58
|
+
return `${deployment}:${userId}`;
|
|
59
|
+
}
|
|
60
|
+
export function upsertAccount(pool, accountData) {
|
|
61
|
+
const validatedPool = validatePoolSchema(pool, "account pool payload");
|
|
62
|
+
const now = new Date().toISOString();
|
|
63
|
+
const { key, deployment, domain, identity, enterpriseUrl, baseUrl, auth, authResult, } = accountData ?? {};
|
|
64
|
+
if (!key || typeof key !== "string") {
|
|
65
|
+
throw new Error("[opencode-copilot-cli-auth] Cannot upsert account: missing key.");
|
|
66
|
+
}
|
|
67
|
+
const userId = Number(identity?.userId);
|
|
68
|
+
if (!Number.isFinite(userId)) {
|
|
69
|
+
throw new Error("[opencode-copilot-cli-auth] Cannot upsert account: missing numeric identity.userId.");
|
|
70
|
+
}
|
|
71
|
+
const login = typeof identity?.login === "string" ? identity.login : "";
|
|
72
|
+
const normalizedDeployment = normalizeDomain(deployment || domain || enterpriseUrl || "github.com");
|
|
73
|
+
const normalizedDomain = normalizeDomain(domain || normalizedDeployment);
|
|
74
|
+
const normalizedEnterpriseUrl = normalizedDeployment === "github.com"
|
|
75
|
+
? null
|
|
76
|
+
: normalizeDomain(enterpriseUrl || normalizedDeployment);
|
|
77
|
+
const normalizedIdentity = {
|
|
78
|
+
login,
|
|
79
|
+
userId,
|
|
80
|
+
};
|
|
81
|
+
const defaultId = deriveDefaultAccountId(validatedPool.accounts, key, normalizedIdentity);
|
|
82
|
+
const defaultName = login || String(userId);
|
|
83
|
+
const mergedAuth = (auth ?? authResult ?? {});
|
|
84
|
+
const nextBaseUrl = baseUrl ?? authResult?.baseUrl ?? null;
|
|
85
|
+
const existingIndex = validatedPool.accounts.findIndex((account) => account?.key === key);
|
|
86
|
+
if (existingIndex === -1) {
|
|
87
|
+
return {
|
|
88
|
+
...validatedPool,
|
|
89
|
+
accounts: [
|
|
90
|
+
...validatedPool.accounts,
|
|
91
|
+
{
|
|
92
|
+
key,
|
|
93
|
+
id: defaultId,
|
|
94
|
+
name: defaultName,
|
|
95
|
+
enabled: true,
|
|
96
|
+
priority: 0,
|
|
97
|
+
deployment: normalizedDeployment,
|
|
98
|
+
domain: normalizedDomain,
|
|
99
|
+
identity: normalizedIdentity,
|
|
100
|
+
enterpriseUrl: normalizedEnterpriseUrl,
|
|
101
|
+
baseUrl: nextBaseUrl,
|
|
102
|
+
allowlist: [],
|
|
103
|
+
blocklist: [],
|
|
104
|
+
auth: mergedAuth,
|
|
105
|
+
createdAt: now,
|
|
106
|
+
updatedAt: now,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const existing = validatedPool.accounts[existingIndex];
|
|
112
|
+
const updatedAccount = {
|
|
113
|
+
...existing,
|
|
114
|
+
key,
|
|
115
|
+
deployment: normalizedDeployment,
|
|
116
|
+
domain: normalizedDomain,
|
|
117
|
+
identity: normalizedIdentity,
|
|
118
|
+
enterpriseUrl: normalizedEnterpriseUrl,
|
|
119
|
+
auth: mergedAuth,
|
|
120
|
+
baseUrl: nextBaseUrl ?? existing.baseUrl ?? null,
|
|
121
|
+
id: preserveStringOrDefault(existing.id, defaultId),
|
|
122
|
+
name: preserveStringOrDefault(existing.name, defaultName),
|
|
123
|
+
enabled: typeof existing.enabled === "boolean" ? existing.enabled : true,
|
|
124
|
+
priority: Number.isFinite(existing.priority) ? existing.priority : 0,
|
|
125
|
+
allowlist: Array.isArray(existing.allowlist) ? existing.allowlist : [],
|
|
126
|
+
blocklist: Array.isArray(existing.blocklist) ? existing.blocklist : [],
|
|
127
|
+
createdAt: existing.createdAt ?? now,
|
|
128
|
+
updatedAt: now,
|
|
129
|
+
};
|
|
130
|
+
const nextAccounts = [...validatedPool.accounts];
|
|
131
|
+
nextAccounts[existingIndex] = updatedAccount;
|
|
132
|
+
return {
|
|
133
|
+
...validatedPool,
|
|
134
|
+
accounts: nextAccounts,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
export function resolveWinnerAccount(rawModelId, pool) {
|
|
138
|
+
const canAccountServeModel = (account) => {
|
|
139
|
+
const allowlist = normalizeList(account?.allowlist);
|
|
140
|
+
if (allowlist.length > 0 && !matchesAnyModelIdPattern(allowlist, rawModelId)) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
const blocklist = normalizeList(account?.blocklist);
|
|
144
|
+
return !matchesAnyModelIdPattern(blocklist, rawModelId);
|
|
145
|
+
};
|
|
146
|
+
const candidates = (Array.isArray(pool?.accounts) ? pool.accounts : [])
|
|
147
|
+
.filter((account) => account?.enabled !== false)
|
|
148
|
+
.filter(canAccountServeModel)
|
|
149
|
+
.sort((left, right) => {
|
|
150
|
+
const priorityDelta = normalizePriority(left?.priority) - normalizePriority(right?.priority);
|
|
151
|
+
if (priorityDelta !== 0) {
|
|
152
|
+
return priorityDelta;
|
|
153
|
+
}
|
|
154
|
+
return String(left?.key ?? "").localeCompare(String(right?.key ?? ""));
|
|
155
|
+
});
|
|
156
|
+
return candidates[0] ?? null;
|
|
157
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { HeaderObject } from "./types.js";
|
|
2
|
+
export declare function injectRoutingHeaders(headers: HeaderObject, accountKey: string): {
|
|
3
|
+
"x-opencode-copilot-account-key": string;
|
|
4
|
+
"x-opencode-copilot-route-source": string;
|
|
5
|
+
};
|
|
6
|
+
export declare function stripRoutingHeaders(headers: HeaderObject): {
|
|
7
|
+
[k: string]: string;
|
|
8
|
+
};
|
package/dist/routing.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { INTERNAL_ROUTING_HEADERS, ROUTING_ACCOUNT_KEY_HEADER, ROUTING_SOURCE_HEADER } from "./constants.js";
|
|
2
|
+
import { normalizeHeaderObject } from "./utils.js";
|
|
3
|
+
export function injectRoutingHeaders(headers, accountKey) {
|
|
4
|
+
return {
|
|
5
|
+
...normalizeHeaderObject(headers),
|
|
6
|
+
[ROUTING_ACCOUNT_KEY_HEADER]: accountKey,
|
|
7
|
+
[ROUTING_SOURCE_HEADER]: "model-resolution",
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function stripRoutingHeaders(headers) {
|
|
11
|
+
return Object.fromEntries(Object.entries(normalizeHeaderObject(headers)).filter(([key]) => !INTERNAL_ROUTING_HEADERS.has(key.toLowerCase())));
|
|
12
|
+
}
|
package/dist/tui.d.ts
ADDED
package/dist/tui.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { getCopilotUsageDialogMessage } from "./usage.js";
|
|
2
|
+
const id = "@gnahz77/opencode-copilot-multi-auth";
|
|
3
|
+
function showErrorToast(api, error) {
|
|
4
|
+
api.ui.toast({
|
|
5
|
+
title: "Copilot Usage",
|
|
6
|
+
message: error instanceof Error ? error.message : String(error),
|
|
7
|
+
variant: "error",
|
|
8
|
+
duration: 7000,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
async function showCopilotUsageDialog(api) {
|
|
12
|
+
try {
|
|
13
|
+
const message = await getCopilotUsageDialogMessage();
|
|
14
|
+
api.ui.dialog.setSize("xlarge");
|
|
15
|
+
api.ui.dialog.replace(() => api.ui.DialogAlert({
|
|
16
|
+
title: "Copilot Usage",
|
|
17
|
+
message,
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
showErrorToast(api, error);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const tui = async (api) => {
|
|
25
|
+
api.command.register(() => [
|
|
26
|
+
{
|
|
27
|
+
title: "Copilot Usage",
|
|
28
|
+
value: "copilot-usage",
|
|
29
|
+
description: "Show Copilot usage for all accounts in the local account pool.",
|
|
30
|
+
category: "GitHub Copilot",
|
|
31
|
+
slash: {
|
|
32
|
+
name: "copilot-usage",
|
|
33
|
+
},
|
|
34
|
+
onSelect() {
|
|
35
|
+
void showCopilotUsageDialog(api);
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
]);
|
|
39
|
+
};
|
|
40
|
+
const pluginModule = {
|
|
41
|
+
id,
|
|
42
|
+
tui,
|
|
43
|
+
};
|
|
44
|
+
export default pluginModule;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
export interface PoolIdentity {
|
|
2
|
+
login: string;
|
|
3
|
+
userId: number;
|
|
4
|
+
}
|
|
5
|
+
export interface OAuthAuth {
|
|
6
|
+
type: "oauth";
|
|
7
|
+
refresh: string;
|
|
8
|
+
access?: string;
|
|
9
|
+
expires?: number;
|
|
10
|
+
baseUrl?: string | null;
|
|
11
|
+
provider?: string;
|
|
12
|
+
enterpriseUrl?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface PoolAccount {
|
|
15
|
+
key: string;
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
priority: number;
|
|
20
|
+
deployment: string;
|
|
21
|
+
domain: string;
|
|
22
|
+
identity: PoolIdentity;
|
|
23
|
+
enterpriseUrl: string | null;
|
|
24
|
+
baseUrl: string | null;
|
|
25
|
+
allowlist: string[];
|
|
26
|
+
blocklist: string[];
|
|
27
|
+
auth: OAuthAuth;
|
|
28
|
+
createdAt: string;
|
|
29
|
+
updatedAt: string;
|
|
30
|
+
}
|
|
31
|
+
export interface UpsertAccountData {
|
|
32
|
+
key: string;
|
|
33
|
+
deployment?: string;
|
|
34
|
+
domain?: string;
|
|
35
|
+
identity?: Partial<PoolIdentity>;
|
|
36
|
+
enterpriseUrl?: string | null;
|
|
37
|
+
baseUrl?: string | null;
|
|
38
|
+
auth?: OAuthAuth;
|
|
39
|
+
authResult?: OAuthAuth;
|
|
40
|
+
}
|
|
41
|
+
export interface AccountPool {
|
|
42
|
+
version: number;
|
|
43
|
+
accounts: PoolAccount[];
|
|
44
|
+
}
|
|
45
|
+
export interface LiveModel {
|
|
46
|
+
id: string;
|
|
47
|
+
name?: string;
|
|
48
|
+
version?: string;
|
|
49
|
+
model_picker_enabled?: boolean;
|
|
50
|
+
capabilities?: {
|
|
51
|
+
type?: string;
|
|
52
|
+
family?: string;
|
|
53
|
+
limits?: {
|
|
54
|
+
max_context_window_tokens?: number;
|
|
55
|
+
max_prompt_tokens?: number;
|
|
56
|
+
max_output_tokens?: number;
|
|
57
|
+
max_non_streaming_output_tokens?: number;
|
|
58
|
+
vision?: boolean;
|
|
59
|
+
};
|
|
60
|
+
supports?: {
|
|
61
|
+
vision?: boolean;
|
|
62
|
+
tool_calls?: boolean;
|
|
63
|
+
adaptive_thinking?: boolean;
|
|
64
|
+
max_thinking_budget?: number;
|
|
65
|
+
reasoning_effort?: string[];
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export interface ProviderModel {
|
|
70
|
+
id: string;
|
|
71
|
+
api?: {
|
|
72
|
+
id?: string;
|
|
73
|
+
url?: string;
|
|
74
|
+
npm?: string;
|
|
75
|
+
};
|
|
76
|
+
name?: string;
|
|
77
|
+
family?: string;
|
|
78
|
+
cost?: unknown;
|
|
79
|
+
limit?: {
|
|
80
|
+
context?: number;
|
|
81
|
+
input?: number;
|
|
82
|
+
output?: number;
|
|
83
|
+
};
|
|
84
|
+
capabilities?: {
|
|
85
|
+
temperature?: boolean;
|
|
86
|
+
reasoning?: boolean;
|
|
87
|
+
attachment?: boolean;
|
|
88
|
+
toolcall?: boolean;
|
|
89
|
+
interleaved?: boolean;
|
|
90
|
+
input?: {
|
|
91
|
+
text?: boolean;
|
|
92
|
+
audio?: boolean;
|
|
93
|
+
image?: boolean;
|
|
94
|
+
video?: boolean;
|
|
95
|
+
pdf?: boolean;
|
|
96
|
+
};
|
|
97
|
+
output?: {
|
|
98
|
+
text?: boolean;
|
|
99
|
+
audio?: boolean;
|
|
100
|
+
image?: boolean;
|
|
101
|
+
video?: boolean;
|
|
102
|
+
pdf?: boolean;
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
options?: Record<string, unknown>;
|
|
106
|
+
headers?: Record<string, unknown>;
|
|
107
|
+
release_date?: string;
|
|
108
|
+
variants?: Record<string, unknown>;
|
|
109
|
+
status?: string;
|
|
110
|
+
}
|
|
111
|
+
export type HeaderObject = Headers | Array<[string, string]> | Record<string, string> | undefined;
|
|
112
|
+
export interface AuthInput {
|
|
113
|
+
refresh: string;
|
|
114
|
+
enterpriseUrl?: string | null;
|
|
115
|
+
baseUrl?: string | null;
|
|
116
|
+
type?: string;
|
|
117
|
+
}
|
|
118
|
+
export interface DeviceFlowAuthorizeResult {
|
|
119
|
+
type: "success";
|
|
120
|
+
refresh: string;
|
|
121
|
+
access: string;
|
|
122
|
+
expires: number;
|
|
123
|
+
baseUrl?: string;
|
|
124
|
+
provider?: string;
|
|
125
|
+
enterpriseUrl?: string;
|
|
126
|
+
}
|
|
127
|
+
export interface ConversationMetadata {
|
|
128
|
+
isVision: boolean;
|
|
129
|
+
isAgent: boolean;
|
|
130
|
+
}
|
|
131
|
+
export interface ChatHeadersMessagePart {
|
|
132
|
+
type?: string;
|
|
133
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/usage.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface QuotaDetail {
|
|
2
|
+
entitlement: number;
|
|
3
|
+
overage_count: number;
|
|
4
|
+
overage_permitted: boolean;
|
|
5
|
+
percent_remaining: number;
|
|
6
|
+
quota_id: string;
|
|
7
|
+
quota_remaining: number;
|
|
8
|
+
remaining: number;
|
|
9
|
+
unlimited: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function getCopilotUsageDialogMessage(): Promise<string>;
|
package/dist/usage.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { fetchEntitlement } from "./auth.js";
|
|
2
|
+
import { readPool } from "./pool.js";
|
|
3
|
+
const USAGE_SNAPSHOT_PREFERENCE = ["premium_interactions", "chat", "completions"];
|
|
4
|
+
function numberOrZero(value) {
|
|
5
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
6
|
+
}
|
|
7
|
+
function clamp(value, min, max) {
|
|
8
|
+
return Math.min(Math.max(value, min), max);
|
|
9
|
+
}
|
|
10
|
+
function roundToTwoDecimals(value) {
|
|
11
|
+
return Math.round(value * 100) / 100;
|
|
12
|
+
}
|
|
13
|
+
function buildProgressBar(percentUsed, width = 16) {
|
|
14
|
+
const normalizedWidth = Math.max(8, width);
|
|
15
|
+
const filled = Math.round((clamp(percentUsed, 0, 100) / 100) * normalizedWidth);
|
|
16
|
+
return `[${"#".repeat(filled)}${"-".repeat(normalizedWidth - filled)}]`;
|
|
17
|
+
}
|
|
18
|
+
function resolveUsageDetail(payload) {
|
|
19
|
+
const snapshots = payload?.quota_snapshots;
|
|
20
|
+
if (!snapshots || typeof snapshots !== "object")
|
|
21
|
+
return null;
|
|
22
|
+
for (const key of USAGE_SNAPSHOT_PREFERENCE) {
|
|
23
|
+
const candidate = snapshots[key];
|
|
24
|
+
if (!candidate || typeof candidate !== "object")
|
|
25
|
+
continue;
|
|
26
|
+
if (candidate.unlimited)
|
|
27
|
+
continue;
|
|
28
|
+
const total = numberOrZero(candidate.entitlement);
|
|
29
|
+
if (total > 0) {
|
|
30
|
+
return candidate;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (const key of USAGE_SNAPSHOT_PREFERENCE) {
|
|
34
|
+
const candidate = snapshots[key];
|
|
35
|
+
if (!candidate || typeof candidate !== "object")
|
|
36
|
+
continue;
|
|
37
|
+
return candidate;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
function formatUsageNumbers(detail) {
|
|
42
|
+
if (detail.unlimited) {
|
|
43
|
+
return {
|
|
44
|
+
used: 0,
|
|
45
|
+
total: 0,
|
|
46
|
+
percentUsed: 0,
|
|
47
|
+
unlimited: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const total = Math.max(0, numberOrZero(detail.entitlement));
|
|
51
|
+
const remaining = Math.max(0, numberOrZero(detail.remaining));
|
|
52
|
+
const used = clamp(total - remaining, 0, total);
|
|
53
|
+
const percentRemaining = clamp(numberOrZero(detail.percent_remaining), 0, 100);
|
|
54
|
+
const percentUsed = total > 0 ? roundToTwoDecimals(clamp(100 - percentRemaining, 0, 100)) : 0;
|
|
55
|
+
return {
|
|
56
|
+
used,
|
|
57
|
+
total,
|
|
58
|
+
percentUsed,
|
|
59
|
+
unlimited: false,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function getAccountLabel(account) {
|
|
63
|
+
const base = account.name || account.identity?.login || account.id || account.key;
|
|
64
|
+
const login = account.identity?.login?.trim();
|
|
65
|
+
if (login && login !== base) {
|
|
66
|
+
return `${base} (@${login})`;
|
|
67
|
+
}
|
|
68
|
+
return base;
|
|
69
|
+
}
|
|
70
|
+
async function getCopilotUsageSummary(account) {
|
|
71
|
+
const accountLabel = getAccountLabel(account);
|
|
72
|
+
const statusSuffix = account.enabled === false ? " [disabled]" : "";
|
|
73
|
+
if (account.auth?.type !== "oauth"
|
|
74
|
+
|| typeof account.auth.refresh !== "string"
|
|
75
|
+
|| !account.auth.refresh.trim()) {
|
|
76
|
+
return [`${accountLabel}${statusSuffix}`, "OAuth token missing"].join("\n");
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const payload = (await fetchEntitlement({
|
|
80
|
+
refresh: account.auth.refresh,
|
|
81
|
+
enterpriseUrl: account.enterpriseUrl,
|
|
82
|
+
baseUrl: account.baseUrl,
|
|
83
|
+
}));
|
|
84
|
+
const detail = resolveUsageDetail(payload);
|
|
85
|
+
if (!detail) {
|
|
86
|
+
return [`${accountLabel}${statusSuffix}`, "Usage unavailable"].join("\n");
|
|
87
|
+
}
|
|
88
|
+
const usage = formatUsageNumbers(detail);
|
|
89
|
+
if (usage.unlimited) {
|
|
90
|
+
return [`${accountLabel}${statusSuffix}`, "Usage: Unlimited", "Used: N/A"].join("\n");
|
|
91
|
+
}
|
|
92
|
+
return [
|
|
93
|
+
`${accountLabel}${statusSuffix}`,
|
|
94
|
+
`${buildProgressBar(usage.percentUsed)} ${usage.used}/${usage.total}`,
|
|
95
|
+
`Used: ${usage.percentUsed}%`,
|
|
96
|
+
].join("\n");
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return [
|
|
100
|
+
`${accountLabel}${statusSuffix}`,
|
|
101
|
+
error instanceof Error ? error.message : String(error),
|
|
102
|
+
].join("\n");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export async function getCopilotUsageDialogMessage() {
|
|
106
|
+
const pool = readPool();
|
|
107
|
+
const accounts = [...pool.accounts].sort((left, right) => left.key.localeCompare(right.key));
|
|
108
|
+
if (accounts.length === 0) {
|
|
109
|
+
throw new Error("No Copilot accounts found in the account pool. Log in again to populate ~/.local/share/opencode/copilot-auth.json.");
|
|
110
|
+
}
|
|
111
|
+
const sections = await Promise.all(accounts.map((account) => getCopilotUsageSummary(account)));
|
|
112
|
+
return sections.join("\n\n");
|
|
113
|
+
}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ConversationMetadata, HeaderObject } from "./types.js";
|
|
2
|
+
export declare function normalizeHeaderObject(headers: HeaderObject): Record<string, string>;
|
|
3
|
+
export declare function normalizeList(value: unknown): string[];
|
|
4
|
+
export declare function matchesModelIdPattern(pattern: unknown, rawModelId: string): boolean;
|
|
5
|
+
export declare function matchesAnyModelIdPattern(patterns: unknown, rawModelId: string): boolean;
|
|
6
|
+
export declare function normalizePriority(value: unknown): number;
|
|
7
|
+
export declare function normalizeDomain(urlOrDomain: unknown): string;
|
|
8
|
+
export declare function normalizeIdSource(value: unknown): string;
|
|
9
|
+
export declare function preserveStringOrDefault(value: unknown, fallback: string): string;
|
|
10
|
+
export declare function zeroCost(): {
|
|
11
|
+
input: number;
|
|
12
|
+
output: number;
|
|
13
|
+
cache: {
|
|
14
|
+
read: number;
|
|
15
|
+
write: number;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export declare function isLiveChatModel(model: unknown): boolean;
|
|
19
|
+
export declare function isPickerModel(model: unknown): boolean;
|
|
20
|
+
export declare function getReleaseDate(id: string, version: unknown, fallback?: string): string;
|
|
21
|
+
export declare function getHeader(headers: HeaderObject, name: string): string | undefined;
|
|
22
|
+
export declare function getConversationMetadata(init: RequestInit | undefined): ConversationMetadata;
|
|
23
|
+
export declare function getRequestedRawModelId(init: RequestInit | undefined): string | undefined;
|
|
24
|
+
export declare function applyBaseURLToRequestInput(input: RequestInfo | URL, baseURL?: string | null): URL | RequestInfo;
|
|
25
|
+
export declare function isValidBaseURL(value: unknown): value is string;
|
|
26
|
+
export declare function resolveClaudeThinkingBudget(model: {
|
|
27
|
+
id?: string;
|
|
28
|
+
} | undefined, variant: unknown): number | undefined;
|