@gnahz77/opencode-copilot-multi-auth 0.1.0 → 0.1.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 +52 -5
- 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 +5 -0
- package/dist/index.js +307 -0
- package/dist/models.d.ts +134 -0
- package/dist/models.js +147 -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 +26 -0
- package/dist/utils.js +203 -0
- package/index.mjs +10 -1211
- package/package.json +28 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { CLIENT_ID, OAUTH_POLLING_SAFETY_MARGIN_MS, OAUTH_SCOPES, ROUTING_ACCOUNT_KEY_HEADER, ROUTING_SOURCE_HEADER, } from "./constants.js";
|
|
2
|
+
import { buildHeaders, fetchEntitlement, fetchWithSelectedAccount, getBaseURL, getUrls, lookupGitHubIdentity, resolveSelectedPoolAccount, } from "./auth.js";
|
|
3
|
+
import { buildPoolBackedModels, normalizeExistingModels, resolveProviderModels } from "./models.js";
|
|
4
|
+
import { deriveAccountKey, getPoolPath, readPool, resolveWinnerAccount, upsertAccount, writePool, } from "./pool.js";
|
|
5
|
+
import { getHeader, getConversationMetadata, getRequestedRawModelId, normalizeDomain, resolveClaudeThinkingBudget, } from "./utils.js";
|
|
6
|
+
export { getPoolPath, readPool, writePool, deriveAccountKey, lookupGitHubIdentity, upsertAccount, resolveWinnerAccount };
|
|
7
|
+
export const CopilotAuthPlugin = async (input) => {
|
|
8
|
+
return {
|
|
9
|
+
provider: {
|
|
10
|
+
id: "github-copilot",
|
|
11
|
+
models: async (provider, ctx) => {
|
|
12
|
+
try {
|
|
13
|
+
const pool = readPool();
|
|
14
|
+
if (pool.accounts.length > 0) {
|
|
15
|
+
return await buildPoolBackedModels(provider.models, pool);
|
|
16
|
+
}
|
|
17
|
+
const oauthAuth = ctx.auth && ctx.auth.type === "oauth" ? ctx.auth : undefined;
|
|
18
|
+
return await resolveProviderModels(provider.models, oauthAuth);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.warn("[opencode-copilot-cli-auth] Failed to sync live Copilot models.", error instanceof Error ? error.message : String(error));
|
|
22
|
+
return normalizeExistingModels(provider.models);
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
auth: {
|
|
27
|
+
provider: "github-copilot",
|
|
28
|
+
loader: async (getAuth) => {
|
|
29
|
+
const info = await getAuth();
|
|
30
|
+
let poolFirstEnabled = null;
|
|
31
|
+
try {
|
|
32
|
+
const currentPool = readPool();
|
|
33
|
+
poolFirstEnabled = currentPool.accounts.find((account) => account?.enabled !== false) ?? null;
|
|
34
|
+
if (currentPool.accounts.length === 0 && info && info.type === "oauth") {
|
|
35
|
+
// Best-effort migration bridge for legacy single-account auth.
|
|
36
|
+
// We intentionally avoid identity lookup here because loader runs on hot paths,
|
|
37
|
+
// and we do not have a stable userId without making a network request.
|
|
38
|
+
// The next successful OAuth authorize callback performs canonical persistence.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// intentional: pool read/parse errors fall back to legacy singleton auth
|
|
43
|
+
}
|
|
44
|
+
const baseSource = poolFirstEnabled?.auth?.type === "oauth"
|
|
45
|
+
? poolFirstEnabled.auth
|
|
46
|
+
: info && info.type === "oauth"
|
|
47
|
+
? info
|
|
48
|
+
: null;
|
|
49
|
+
if (!baseSource)
|
|
50
|
+
return {};
|
|
51
|
+
const baseURL = poolFirstEnabled?.baseUrl ?? await getBaseURL(baseSource);
|
|
52
|
+
return {
|
|
53
|
+
...(baseURL && { baseURL }),
|
|
54
|
+
apiKey: "",
|
|
55
|
+
async fetch(inputRequest, init) {
|
|
56
|
+
const pool = readPool();
|
|
57
|
+
const accountKey = getHeader(init?.headers, ROUTING_ACCOUNT_KEY_HEADER);
|
|
58
|
+
const requestedRawModelId = getRequestedRawModelId(init);
|
|
59
|
+
if (pool.accounts.length > 0) {
|
|
60
|
+
const selectedAccount = resolveSelectedPoolAccount(pool, accountKey, requestedRawModelId);
|
|
61
|
+
if (selectedAccount?.auth?.type === "oauth") {
|
|
62
|
+
return fetchWithSelectedAccount(inputRequest, init, selectedAccount);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const auth = await getAuth();
|
|
66
|
+
if (!auth || auth.type !== "oauth") {
|
|
67
|
+
return fetch(inputRequest, init);
|
|
68
|
+
}
|
|
69
|
+
const { isVision, isAgent } = getConversationMetadata(init);
|
|
70
|
+
const headers = buildHeaders(init, auth, isVision, isAgent);
|
|
71
|
+
return fetch(inputRequest, {
|
|
72
|
+
...init,
|
|
73
|
+
headers,
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
methods: [
|
|
79
|
+
{
|
|
80
|
+
type: "oauth",
|
|
81
|
+
label: "Login with GitHub Copilot CLI",
|
|
82
|
+
prompts: [
|
|
83
|
+
{
|
|
84
|
+
type: "select",
|
|
85
|
+
key: "deploymentType",
|
|
86
|
+
message: "Select GitHub deployment type",
|
|
87
|
+
options: [
|
|
88
|
+
{
|
|
89
|
+
label: "GitHub.com (Add)",
|
|
90
|
+
value: "github.com",
|
|
91
|
+
hint: "Public",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
label: "GitHub Enterprise (Add)",
|
|
95
|
+
value: "enterprise",
|
|
96
|
+
hint: "Data residency or self-hosted",
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
type: "text",
|
|
102
|
+
key: "enterpriseUrl",
|
|
103
|
+
message: "Enter your GitHub Enterprise URL or domain",
|
|
104
|
+
placeholder: "github.com or https://github.com (default: github.com)",
|
|
105
|
+
condition: (inputs) => inputs.deploymentType === "enterprise",
|
|
106
|
+
validate: (value) => {
|
|
107
|
+
if (!value || !String(value).trim()) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const url = value.includes("://")
|
|
112
|
+
? new URL(value)
|
|
113
|
+
: new URL(`https://${value}`);
|
|
114
|
+
if (!url.hostname) {
|
|
115
|
+
return "Please enter a valid URL or domain";
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return "Please enter a valid URL";
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
async authorize(inputs = {}) {
|
|
126
|
+
const deploymentType = inputs.deploymentType || "github.com";
|
|
127
|
+
let domain = "github.com";
|
|
128
|
+
let actualProvider = "github-copilot";
|
|
129
|
+
if (deploymentType === "enterprise") {
|
|
130
|
+
domain = normalizeDomain(inputs.enterpriseUrl);
|
|
131
|
+
actualProvider = "github-copilot-enterprise";
|
|
132
|
+
}
|
|
133
|
+
const urls = getUrls(domain);
|
|
134
|
+
const deviceResponse = await fetch(urls.DEVICE_CODE_URL, {
|
|
135
|
+
method: "POST",
|
|
136
|
+
headers: {
|
|
137
|
+
Accept: "application/json",
|
|
138
|
+
"Content-Type": "application/json",
|
|
139
|
+
"User-Agent": "opencode-copilot-cli-auth/0.0.16",
|
|
140
|
+
},
|
|
141
|
+
body: JSON.stringify({
|
|
142
|
+
client_id: CLIENT_ID,
|
|
143
|
+
scope: OAUTH_SCOPES,
|
|
144
|
+
}),
|
|
145
|
+
});
|
|
146
|
+
if (!deviceResponse.ok) {
|
|
147
|
+
throw new Error("[opencode-copilot-cli-auth] Failed to initiate device authorization");
|
|
148
|
+
}
|
|
149
|
+
const deviceData = await deviceResponse.json();
|
|
150
|
+
return {
|
|
151
|
+
url: deviceData.verification_uri,
|
|
152
|
+
instructions: `Enter code: ${deviceData.user_code}`,
|
|
153
|
+
method: "auto",
|
|
154
|
+
callback: async () => {
|
|
155
|
+
while (true) {
|
|
156
|
+
const response = await fetch(urls.ACCESS_TOKEN_URL, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: {
|
|
159
|
+
Accept: "application/json",
|
|
160
|
+
"Content-Type": "application/json",
|
|
161
|
+
"User-Agent": "opencode-copilot-cli-auth/0.0.16",
|
|
162
|
+
},
|
|
163
|
+
body: JSON.stringify({
|
|
164
|
+
client_id: CLIENT_ID,
|
|
165
|
+
device_code: deviceData.device_code,
|
|
166
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
167
|
+
}),
|
|
168
|
+
});
|
|
169
|
+
if (!response.ok)
|
|
170
|
+
return { type: "failed" };
|
|
171
|
+
const data = await response.json();
|
|
172
|
+
if (data.access_token) {
|
|
173
|
+
const entitlement = await fetchEntitlement({
|
|
174
|
+
refresh: data.access_token,
|
|
175
|
+
enterpriseUrl: actualProvider === "github-copilot-enterprise"
|
|
176
|
+
? domain
|
|
177
|
+
: undefined,
|
|
178
|
+
});
|
|
179
|
+
const result = {
|
|
180
|
+
type: "success",
|
|
181
|
+
refresh: data.access_token,
|
|
182
|
+
access: data.access_token,
|
|
183
|
+
expires: 0,
|
|
184
|
+
baseUrl: entitlement?.endpoints?.api,
|
|
185
|
+
};
|
|
186
|
+
if (actualProvider === "github-copilot-enterprise") {
|
|
187
|
+
result.provider = "github-copilot-enterprise";
|
|
188
|
+
result.enterpriseUrl = domain;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const identity = await lookupGitHubIdentity(data.access_token, actualProvider === "github-copilot-enterprise" ? domain : undefined);
|
|
192
|
+
const deployment = actualProvider === "github-copilot-enterprise" ? domain : "github.com";
|
|
193
|
+
const key = deriveAccountKey(deployment, identity.userId);
|
|
194
|
+
const pool = readPool();
|
|
195
|
+
const updatedPool = upsertAccount(pool, {
|
|
196
|
+
key,
|
|
197
|
+
deployment,
|
|
198
|
+
domain: deployment,
|
|
199
|
+
identity,
|
|
200
|
+
enterpriseUrl: actualProvider === "github-copilot-enterprise" ? domain : null,
|
|
201
|
+
baseUrl: result.baseUrl ?? null,
|
|
202
|
+
auth: {
|
|
203
|
+
type: "oauth",
|
|
204
|
+
refresh: data.access_token,
|
|
205
|
+
access: data.access_token,
|
|
206
|
+
expires: 0,
|
|
207
|
+
baseUrl: result.baseUrl ?? null,
|
|
208
|
+
...(result.provider && { provider: result.provider }),
|
|
209
|
+
...(result.enterpriseUrl && { enterpriseUrl: result.enterpriseUrl }),
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
writePool(updatedPool);
|
|
213
|
+
}
|
|
214
|
+
catch (persistError) {
|
|
215
|
+
console.warn("[opencode-copilot-cli-auth] Failed to persist account to pool:", persistError instanceof Error ? persistError.message : String(persistError));
|
|
216
|
+
}
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
if (data.error === "authorization_pending") {
|
|
220
|
+
await new Promise((resolve) => setTimeout(resolve, deviceData.interval * 1000
|
|
221
|
+
+ OAUTH_POLLING_SAFETY_MARGIN_MS));
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (data.error === "slow_down") {
|
|
225
|
+
const nextInterval = (typeof data.interval === "number" && data.interval > 0
|
|
226
|
+
? data.interval
|
|
227
|
+
: deviceData.interval + 5) * 1000;
|
|
228
|
+
await new Promise((resolve) => setTimeout(resolve, nextInterval + OAUTH_POLLING_SAFETY_MARGIN_MS));
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (data.error)
|
|
232
|
+
return { type: "failed" };
|
|
233
|
+
await new Promise((resolve) => setTimeout(resolve, deviceData.interval * 1000
|
|
234
|
+
+ OAUTH_POLLING_SAFETY_MARGIN_MS));
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
"chat.params": async (input, output) => {
|
|
243
|
+
if (input.model.providerID !== "github-copilot")
|
|
244
|
+
return;
|
|
245
|
+
if (input.model.api?.npm !== "@ai-sdk/github-copilot")
|
|
246
|
+
return;
|
|
247
|
+
if (!input.model.id.includes("claude"))
|
|
248
|
+
return;
|
|
249
|
+
const messageWithVariant = input.message;
|
|
250
|
+
const thinkingBudget = resolveClaudeThinkingBudget(input.model, messageWithVariant.variant);
|
|
251
|
+
if (thinkingBudget === undefined)
|
|
252
|
+
return;
|
|
253
|
+
output.options.thinking_budget = thinkingBudget;
|
|
254
|
+
},
|
|
255
|
+
"chat.headers": async (incoming, output) => {
|
|
256
|
+
if (!incoming.model.providerID.includes("github-copilot"))
|
|
257
|
+
return;
|
|
258
|
+
const sdk = input.client;
|
|
259
|
+
if (sdk?.session?.message && sdk?.session?.get) {
|
|
260
|
+
const parts = await sdk.session
|
|
261
|
+
.message({
|
|
262
|
+
path: {
|
|
263
|
+
id: incoming.message.sessionID,
|
|
264
|
+
messageID: incoming.message.id,
|
|
265
|
+
},
|
|
266
|
+
query: {
|
|
267
|
+
directory: input.directory,
|
|
268
|
+
},
|
|
269
|
+
throwOnError: true,
|
|
270
|
+
})
|
|
271
|
+
.catch(() => undefined);
|
|
272
|
+
if (parts?.data?.parts?.some((part) => part.type === "compaction")) {
|
|
273
|
+
output.headers["x-initiator"] = "agent";
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
const session = await sdk.session
|
|
277
|
+
.get({
|
|
278
|
+
path: {
|
|
279
|
+
id: incoming.sessionID,
|
|
280
|
+
},
|
|
281
|
+
query: {
|
|
282
|
+
directory: input.directory,
|
|
283
|
+
},
|
|
284
|
+
throwOnError: true,
|
|
285
|
+
})
|
|
286
|
+
.catch(() => undefined);
|
|
287
|
+
if (session?.data?.parentID) {
|
|
288
|
+
output.headers["x-initiator"] = "agent";
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
const pool = readPool();
|
|
294
|
+
if (pool.accounts.length > 0) {
|
|
295
|
+
const winner = resolveWinnerAccount(incoming.model.id, pool);
|
|
296
|
+
if (winner) {
|
|
297
|
+
output.headers[ROUTING_ACCOUNT_KEY_HEADER] = winner.key;
|
|
298
|
+
output.headers[ROUTING_SOURCE_HEADER] = "model-resolution";
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// intentional: routing header injection is best-effort; missing header falls back to request-time model resolution
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
};
|
package/dist/models.d.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { Model as SDKModel } from "@opencode-ai/sdk/v2";
|
|
2
|
+
import type { AccountPool, AuthInput, LiveModel } from "./types.js";
|
|
3
|
+
export declare function fetchModels(info: AuthInput, baseURL: string): Promise<LiveModel[]>;
|
|
4
|
+
export declare function createProviderModel(existing: SDKModel | undefined, live: LiveModel, baseURL: string): SDKModel;
|
|
5
|
+
export declare function buildProviderModels(existingModels: Record<string, SDKModel> | undefined, liveModels: LiveModel[], baseURL: string): {
|
|
6
|
+
[k: string]: SDKModel;
|
|
7
|
+
};
|
|
8
|
+
export declare function normalizeExistingModels(existingModels: Record<string, SDKModel> | undefined, baseURL?: string): {
|
|
9
|
+
[k: string]: {
|
|
10
|
+
cost: {
|
|
11
|
+
input: number;
|
|
12
|
+
output: number;
|
|
13
|
+
cache: {
|
|
14
|
+
read: number;
|
|
15
|
+
write: number;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
api: {
|
|
19
|
+
url: string;
|
|
20
|
+
npm: string;
|
|
21
|
+
id: string;
|
|
22
|
+
};
|
|
23
|
+
id: string;
|
|
24
|
+
providerID: string;
|
|
25
|
+
name: string;
|
|
26
|
+
family?: string;
|
|
27
|
+
capabilities: {
|
|
28
|
+
temperature: boolean;
|
|
29
|
+
reasoning: boolean;
|
|
30
|
+
attachment: boolean;
|
|
31
|
+
toolcall: boolean;
|
|
32
|
+
input: {
|
|
33
|
+
text: boolean;
|
|
34
|
+
audio: boolean;
|
|
35
|
+
image: boolean;
|
|
36
|
+
video: boolean;
|
|
37
|
+
pdf: boolean;
|
|
38
|
+
};
|
|
39
|
+
output: {
|
|
40
|
+
text: boolean;
|
|
41
|
+
audio: boolean;
|
|
42
|
+
image: boolean;
|
|
43
|
+
video: boolean;
|
|
44
|
+
pdf: boolean;
|
|
45
|
+
};
|
|
46
|
+
interleaved: boolean | {
|
|
47
|
+
field: "reasoning_content" | "reasoning_details";
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
limit: {
|
|
51
|
+
context: number;
|
|
52
|
+
input?: number;
|
|
53
|
+
output: number;
|
|
54
|
+
};
|
|
55
|
+
status: "alpha" | "beta" | "deprecated" | "active";
|
|
56
|
+
options: {
|
|
57
|
+
[key: string]: unknown;
|
|
58
|
+
};
|
|
59
|
+
headers: {
|
|
60
|
+
[key: string]: string;
|
|
61
|
+
};
|
|
62
|
+
release_date: string;
|
|
63
|
+
variants?: {
|
|
64
|
+
[key: string]: {
|
|
65
|
+
[key: string]: unknown;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
export declare function resolveProviderModels(existingModels: Record<string, SDKModel> | undefined, auth: AuthInput | null | undefined): Promise<{
|
|
71
|
+
[k: string]: {
|
|
72
|
+
cost: {
|
|
73
|
+
input: number;
|
|
74
|
+
output: number;
|
|
75
|
+
cache: {
|
|
76
|
+
read: number;
|
|
77
|
+
write: number;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
api: {
|
|
81
|
+
url: string;
|
|
82
|
+
npm: string;
|
|
83
|
+
id: string;
|
|
84
|
+
};
|
|
85
|
+
id: string;
|
|
86
|
+
providerID: string;
|
|
87
|
+
name: string;
|
|
88
|
+
family?: string;
|
|
89
|
+
capabilities: {
|
|
90
|
+
temperature: boolean;
|
|
91
|
+
reasoning: boolean;
|
|
92
|
+
attachment: boolean;
|
|
93
|
+
toolcall: boolean;
|
|
94
|
+
input: {
|
|
95
|
+
text: boolean;
|
|
96
|
+
audio: boolean;
|
|
97
|
+
image: boolean;
|
|
98
|
+
video: boolean;
|
|
99
|
+
pdf: boolean;
|
|
100
|
+
};
|
|
101
|
+
output: {
|
|
102
|
+
text: boolean;
|
|
103
|
+
audio: boolean;
|
|
104
|
+
image: boolean;
|
|
105
|
+
video: boolean;
|
|
106
|
+
pdf: boolean;
|
|
107
|
+
};
|
|
108
|
+
interleaved: boolean | {
|
|
109
|
+
field: "reasoning_content" | "reasoning_details";
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
limit: {
|
|
113
|
+
context: number;
|
|
114
|
+
input?: number;
|
|
115
|
+
output: number;
|
|
116
|
+
};
|
|
117
|
+
status: "alpha" | "beta" | "deprecated" | "active";
|
|
118
|
+
options: {
|
|
119
|
+
[key: string]: unknown;
|
|
120
|
+
};
|
|
121
|
+
headers: {
|
|
122
|
+
[key: string]: string;
|
|
123
|
+
};
|
|
124
|
+
release_date: string;
|
|
125
|
+
variants?: {
|
|
126
|
+
[key: string]: {
|
|
127
|
+
[key: string]: unknown;
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
}>;
|
|
132
|
+
export declare function buildPoolBackedModels(existingModels: Record<string, SDKModel> | undefined, pool: AccountPool): Promise<{
|
|
133
|
+
[k: string]: SDKModel;
|
|
134
|
+
}>;
|
package/dist/models.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { getBaseURL } from "./auth.js";
|
|
2
|
+
import { resolveWinnerAccount } from "./pool.js";
|
|
3
|
+
import { getReleaseDate, isPickerModel, zeroCost } from "./utils.js";
|
|
4
|
+
export async function fetchModels(info, baseURL) {
|
|
5
|
+
const response = await fetch(`${baseURL}/models`, {
|
|
6
|
+
headers: {
|
|
7
|
+
Authorization: `Bearer ${info.refresh}`,
|
|
8
|
+
"Copilot-Integration-Id": "copilot-developer-cli",
|
|
9
|
+
"Openai-Intent": "model-access",
|
|
10
|
+
"User-Agent": "opencode-copilot-cli-auth/0.0.16",
|
|
11
|
+
"X-GitHub-Api-Version": "2025-05-01",
|
|
12
|
+
"X-Interaction-Type": "model-access",
|
|
13
|
+
"X-Request-Id": crypto.randomUUID(),
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
throw new Error(`[opencode-copilot-cli-auth] Model fetch failed: ${response.status}`);
|
|
18
|
+
}
|
|
19
|
+
const data = await response.json();
|
|
20
|
+
return Array.isArray(data?.data) ? data.data : [];
|
|
21
|
+
}
|
|
22
|
+
export function createProviderModel(existing, live, baseURL) {
|
|
23
|
+
const limits = live.capabilities?.limits ?? {};
|
|
24
|
+
const supports = live.capabilities?.supports ?? {};
|
|
25
|
+
const vision = !!supports.vision || !!limits.vision;
|
|
26
|
+
const reasoning = existing?.capabilities?.reasoning
|
|
27
|
+
?? (!!supports.adaptive_thinking
|
|
28
|
+
|| typeof supports.max_thinking_budget === "number"
|
|
29
|
+
|| Array.isArray(supports.reasoning_effort));
|
|
30
|
+
return {
|
|
31
|
+
...structuredClone(existing ?? {}),
|
|
32
|
+
id: live.id,
|
|
33
|
+
providerID: existing?.providerID ?? "github-copilot",
|
|
34
|
+
api: {
|
|
35
|
+
...(existing?.api ?? {}),
|
|
36
|
+
id: live.id,
|
|
37
|
+
url: baseURL,
|
|
38
|
+
npm: "@ai-sdk/github-copilot",
|
|
39
|
+
},
|
|
40
|
+
name: live.name ?? existing?.name ?? live.id,
|
|
41
|
+
family: live.capabilities?.family ?? existing?.family ?? "",
|
|
42
|
+
cost: zeroCost(),
|
|
43
|
+
limit: {
|
|
44
|
+
context: limits.max_context_window_tokens
|
|
45
|
+
?? existing?.limit?.context
|
|
46
|
+
?? 0,
|
|
47
|
+
input: limits.max_prompt_tokens
|
|
48
|
+
?? existing?.limit?.input
|
|
49
|
+
?? limits.max_context_window_tokens,
|
|
50
|
+
output: limits.max_output_tokens
|
|
51
|
+
?? limits.max_non_streaming_output_tokens
|
|
52
|
+
?? existing?.limit?.output
|
|
53
|
+
?? 0,
|
|
54
|
+
},
|
|
55
|
+
capabilities: {
|
|
56
|
+
temperature: existing?.capabilities?.temperature ?? true,
|
|
57
|
+
reasoning,
|
|
58
|
+
attachment: existing?.capabilities?.attachment ?? vision,
|
|
59
|
+
toolcall: !!supports.tool_calls,
|
|
60
|
+
input: {
|
|
61
|
+
text: existing?.capabilities?.input?.text ?? true,
|
|
62
|
+
audio: existing?.capabilities?.input?.audio ?? false,
|
|
63
|
+
image: existing?.capabilities?.input?.image ?? vision,
|
|
64
|
+
video: existing?.capabilities?.input?.video ?? false,
|
|
65
|
+
pdf: existing?.capabilities?.input?.pdf ?? false,
|
|
66
|
+
},
|
|
67
|
+
output: {
|
|
68
|
+
text: existing?.capabilities?.output?.text ?? true,
|
|
69
|
+
audio: existing?.capabilities?.output?.audio ?? false,
|
|
70
|
+
image: existing?.capabilities?.output?.image ?? false,
|
|
71
|
+
video: existing?.capabilities?.output?.video ?? false,
|
|
72
|
+
pdf: existing?.capabilities?.output?.pdf ?? false,
|
|
73
|
+
},
|
|
74
|
+
interleaved: existing?.capabilities?.interleaved ?? false,
|
|
75
|
+
},
|
|
76
|
+
options: existing?.options ?? {},
|
|
77
|
+
headers: existing?.headers ?? {},
|
|
78
|
+
release_date: getReleaseDate(live.id, live.version, existing?.release_date ?? ""),
|
|
79
|
+
variants: existing?.variants ?? {},
|
|
80
|
+
status: "active",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function buildProviderModels(existingModels, liveModels, baseURL) {
|
|
84
|
+
const existingById = new Map(Object.values(existingModels ?? {}).map((model) => [model?.api?.id ?? model?.id, model]));
|
|
85
|
+
return Object.fromEntries(liveModels
|
|
86
|
+
.filter(isPickerModel)
|
|
87
|
+
.map((model) => [
|
|
88
|
+
model.id,
|
|
89
|
+
createProviderModel(existingById.get(model.id), model, baseURL),
|
|
90
|
+
]));
|
|
91
|
+
}
|
|
92
|
+
export function normalizeExistingModels(existingModels, baseURL) {
|
|
93
|
+
return Object.fromEntries(Object.entries(existingModels ?? {}).map(([id, model]) => [
|
|
94
|
+
id,
|
|
95
|
+
{
|
|
96
|
+
...structuredClone(model),
|
|
97
|
+
cost: zeroCost(),
|
|
98
|
+
api: {
|
|
99
|
+
...model.api,
|
|
100
|
+
url: baseURL ?? model.api?.url,
|
|
101
|
+
npm: "@ai-sdk/github-copilot",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
]));
|
|
105
|
+
}
|
|
106
|
+
export async function resolveProviderModels(existingModels, auth) {
|
|
107
|
+
const baseURL = auth ? await getBaseURL(auth) : undefined;
|
|
108
|
+
if (!auth || auth.type !== "oauth" || !baseURL) {
|
|
109
|
+
return normalizeExistingModels(existingModels, baseURL);
|
|
110
|
+
}
|
|
111
|
+
const liveModels = await fetchModels(auth, baseURL);
|
|
112
|
+
return buildProviderModels(existingModels, liveModels, baseURL);
|
|
113
|
+
}
|
|
114
|
+
export async function buildPoolBackedModels(existingModels, pool) {
|
|
115
|
+
const enabledAccounts = (Array.isArray(pool?.accounts) ? pool.accounts : [])
|
|
116
|
+
.filter((account) => account?.enabled !== false);
|
|
117
|
+
if (enabledAccounts.length === 0) {
|
|
118
|
+
return {};
|
|
119
|
+
}
|
|
120
|
+
const candidatesByModel = new Map();
|
|
121
|
+
for (const account of enabledAccounts) {
|
|
122
|
+
try {
|
|
123
|
+
const baseURL = account.baseUrl ?? await getBaseURL(account.auth);
|
|
124
|
+
const liveModels = await fetchModels(account.auth, baseURL);
|
|
125
|
+
for (const liveModel of liveModels.filter(isPickerModel)) {
|
|
126
|
+
const winner = resolveWinnerAccount(liveModel.id, pool);
|
|
127
|
+
if (winner?.key === account.key) {
|
|
128
|
+
candidatesByModel.set(liveModel.id, {
|
|
129
|
+
account,
|
|
130
|
+
liveModel,
|
|
131
|
+
baseURL,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
console.warn(`[opencode-copilot-cli-auth] Skipping account ${account?.key ?? "unknown"} during model sync:`, error instanceof Error ? error.message : String(error));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const existingById = new Map(Object.values(existingModels ?? {}).map((model) => [model?.api?.id ?? model?.id, model]));
|
|
141
|
+
return Object.fromEntries([...candidatesByModel.entries()]
|
|
142
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
143
|
+
.map(([rawModelId, { liveModel, baseURL }]) => [
|
|
144
|
+
rawModelId,
|
|
145
|
+
createProviderModel(existingById.get(rawModelId), liveModel, baseURL),
|
|
146
|
+
]));
|
|
147
|
+
}
|
package/dist/pool.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AccountPool, PoolAccount, UpsertAccountData } from "./types.js";
|
|
2
|
+
export declare function getPoolPath(): string;
|
|
3
|
+
export declare function validatePoolSchema(pool: unknown, context: string): AccountPool;
|
|
4
|
+
export declare function readPool(): AccountPool;
|
|
5
|
+
export declare function writePool(pool: AccountPool): void;
|
|
6
|
+
export declare function deriveAccountKey(deployment: string, userId: number): string;
|
|
7
|
+
export declare function upsertAccount(pool: AccountPool, accountData: UpsertAccountData): AccountPool;
|
|
8
|
+
export declare function resolveWinnerAccount(rawModelId: string, pool: AccountPool): PoolAccount;
|