pi-provider-codebuddy 0.0.0-stage → 0.1.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/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +98 -2
- package/dist/index.js +1324 -0
- package/package.json +65 -5
package/dist/index.js
ADDED
|
@@ -0,0 +1,1324 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
3
|
+
import { join as join2 } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
getAgentDir as getAgentDir2
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
|
|
8
|
+
// src/product.ts
|
|
9
|
+
var CLI_VERSION = "2.171.0";
|
|
10
|
+
var PLATFORM = "CLI";
|
|
11
|
+
var PROVIDERS = [
|
|
12
|
+
{
|
|
13
|
+
id: "codebuddy-cn",
|
|
14
|
+
label: "CodeBuddy (CN)",
|
|
15
|
+
endpoint: "https://copilot.tencent.com"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "codebuddy-intl",
|
|
19
|
+
label: "CodeBuddy (International)",
|
|
20
|
+
endpoint: "https://www.codebuddy.ai"
|
|
21
|
+
},
|
|
22
|
+
// Same endpoint as the Chinese site; iOA is told apart by the `ioa=1` auth param
|
|
23
|
+
// and the default headers below, never by the domain.
|
|
24
|
+
{
|
|
25
|
+
id: "codebuddy-ioa",
|
|
26
|
+
label: "CodeBuddy iOA",
|
|
27
|
+
endpoint: "https://copilot.tencent.com",
|
|
28
|
+
environment: "iOA"
|
|
29
|
+
},
|
|
30
|
+
{ id: "codebuddy-enterprise", label: "CodeBuddy Enterprise" }
|
|
31
|
+
];
|
|
32
|
+
function providerById(id) {
|
|
33
|
+
return PROVIDERS.find((provider) => provider.id === id);
|
|
34
|
+
}
|
|
35
|
+
function isCodeBuddyProvider(provider) {
|
|
36
|
+
return /^codebuddy(-|$)/.test(provider ?? "");
|
|
37
|
+
}
|
|
38
|
+
var IOA_DEFAULT_DOMAIN = "tencent.sso.copilot.tencent.com";
|
|
39
|
+
var IOA_DEFAULT_ENTERPRISE_ID = "etahzsqej0n4";
|
|
40
|
+
var INTERNAL_DOMAINS = [
|
|
41
|
+
"copilot.tencent.com",
|
|
42
|
+
"staging-copilot.tencent.com",
|
|
43
|
+
"www.codebuddy.cn",
|
|
44
|
+
"staging.codebuddy.cn",
|
|
45
|
+
"www.workbuddy.cn",
|
|
46
|
+
"staging.workbuddy.cn"
|
|
47
|
+
];
|
|
48
|
+
var EXTERNAL_DOMAINS = ["www.codebuddy.ai", "staging-codebuddy.tencent.com"];
|
|
49
|
+
var IOA_DOMAINS = [
|
|
50
|
+
"tencent.sso.copilot.tencent.com",
|
|
51
|
+
"tencent.sso.copilot-staging.tencent.com",
|
|
52
|
+
"tencent.sso.codebuddy.cn",
|
|
53
|
+
"tencent.staging-sso.codebuddy.cn"
|
|
54
|
+
];
|
|
55
|
+
var CLOUD_HOSTED_DOMAINS = [
|
|
56
|
+
"*.sso.copilot.tencent.com",
|
|
57
|
+
"*.sso.copilot-staging.tencent.com",
|
|
58
|
+
"*.copilot.qq.com",
|
|
59
|
+
"*.copilot-staging.qq.com",
|
|
60
|
+
"*.sso.codebuddy.cn",
|
|
61
|
+
"*.staging-sso.codebuddy.cn"
|
|
62
|
+
];
|
|
63
|
+
function wildcardPattern(pattern, capture) {
|
|
64
|
+
const body = pattern.replace(/\./g, "\\.").replace(/\*/g, capture ? "([^.]*)" : "[^.]*");
|
|
65
|
+
return new RegExp(`^${body}$`);
|
|
66
|
+
}
|
|
67
|
+
function matchesDomain(patterns, host) {
|
|
68
|
+
return patterns.some(
|
|
69
|
+
(pattern) => pattern === host || pattern.includes("*") && wildcardPattern(pattern, false).test(host)
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
function isIOADomain(host) {
|
|
73
|
+
return matchesDomain(IOA_DOMAINS, host.toLowerCase().replace(/:\d+$/, ""));
|
|
74
|
+
}
|
|
75
|
+
function normalizeEndpoint(input) {
|
|
76
|
+
const trimmed = input.trim();
|
|
77
|
+
if (!trimmed) throw new Error("CodeBuddy endpoint is empty");
|
|
78
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
79
|
+
let url;
|
|
80
|
+
try {
|
|
81
|
+
url = new URL(withScheme);
|
|
82
|
+
} catch {
|
|
83
|
+
throw new Error(`Invalid CodeBuddy endpoint: ${input}`);
|
|
84
|
+
}
|
|
85
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
86
|
+
throw new Error(`Unsupported CodeBuddy endpoint protocol: ${url.protocol}`);
|
|
87
|
+
}
|
|
88
|
+
return url.origin;
|
|
89
|
+
}
|
|
90
|
+
function hostOf(endpoint) {
|
|
91
|
+
return new URL(endpoint).hostname;
|
|
92
|
+
}
|
|
93
|
+
function networkEnvironment(domain) {
|
|
94
|
+
const host = domain.replace(/:\d+$/, "").toLowerCase();
|
|
95
|
+
if (matchesDomain(INTERNAL_DOMAINS, host)) return "internal";
|
|
96
|
+
if (matchesDomain(IOA_DOMAINS, host)) return "iOA";
|
|
97
|
+
if (matchesDomain(CLOUD_HOSTED_DOMAINS, host)) return "cloudhosted";
|
|
98
|
+
if (matchesDomain(EXTERNAL_DOMAINS, host)) return "external";
|
|
99
|
+
return "selfhosted";
|
|
100
|
+
}
|
|
101
|
+
function deploymentType(endpoint, environment) {
|
|
102
|
+
if (matchesDomain(CLOUD_HOSTED_DOMAINS, hostOf(endpoint)))
|
|
103
|
+
return "Cloud-Hosted";
|
|
104
|
+
if (environment === "cloudhosted") return "Cloud-Hosted";
|
|
105
|
+
if (environment === "selfhosted") return "Self-Hosted";
|
|
106
|
+
return "SaaS";
|
|
107
|
+
}
|
|
108
|
+
function enterpriseIdFromEndpoint(endpoint) {
|
|
109
|
+
const host = hostOf(endpoint);
|
|
110
|
+
for (const pattern of CLOUD_HOSTED_DOMAINS) {
|
|
111
|
+
const match = wildcardPattern(pattern, true).exec(host);
|
|
112
|
+
if (match?.[1]) return match[1];
|
|
113
|
+
}
|
|
114
|
+
return void 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/http.ts
|
|
118
|
+
var HttpError = class extends Error {
|
|
119
|
+
status;
|
|
120
|
+
constructor(message, status) {
|
|
121
|
+
super(message);
|
|
122
|
+
this.status = status;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
async function requestEnvelope(url, init, signal) {
|
|
126
|
+
const response = await fetch(url, { ...init, signal });
|
|
127
|
+
const text = await response.text();
|
|
128
|
+
const path = new URL(url).pathname;
|
|
129
|
+
let body;
|
|
130
|
+
try {
|
|
131
|
+
body = JSON.parse(text);
|
|
132
|
+
} catch {
|
|
133
|
+
throw new HttpError(
|
|
134
|
+
`CodeBuddy ${path} returned HTTP ${response.status}: ${text.slice(0, 200)}`,
|
|
135
|
+
response.status
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
throw new HttpError(
|
|
140
|
+
`CodeBuddy ${path} returned HTTP ${response.status}: ${body.msg ?? text.slice(0, 200)}`,
|
|
141
|
+
response.status
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return body;
|
|
145
|
+
}
|
|
146
|
+
function sessionDomain(session) {
|
|
147
|
+
if (session.domain) return session.domain;
|
|
148
|
+
if (session.environment === "iOA") return IOA_DEFAULT_DOMAIN;
|
|
149
|
+
return hostOf(session.endpoint);
|
|
150
|
+
}
|
|
151
|
+
function sessionEnterpriseId(session) {
|
|
152
|
+
return session.enterpriseId || enterpriseIdFromEndpoint(session.endpoint);
|
|
153
|
+
}
|
|
154
|
+
function identityHeaders(session) {
|
|
155
|
+
const domain = sessionDomain(session);
|
|
156
|
+
const environment = session.environment ?? networkEnvironment(domain);
|
|
157
|
+
const headers = {
|
|
158
|
+
Authorization: `Bearer ${session.accessToken}`,
|
|
159
|
+
"X-Domain": domain,
|
|
160
|
+
"X-Product": deploymentType(session.endpoint, networkEnvironment(domain)),
|
|
161
|
+
"X-IDE-Type": PLATFORM,
|
|
162
|
+
"X-IDE-Name": PLATFORM,
|
|
163
|
+
"X-IDE-Version": CLI_VERSION,
|
|
164
|
+
"User-Agent": `${PLATFORM}/${CLI_VERSION} CodeBuddy/${CLI_VERSION}`
|
|
165
|
+
};
|
|
166
|
+
if (session.uid) headers["X-User-Id"] = session.uid;
|
|
167
|
+
const enterpriseId = sessionEnterpriseId(session);
|
|
168
|
+
if (enterpriseId) {
|
|
169
|
+
headers["X-Enterprise-Id"] = enterpriseId;
|
|
170
|
+
headers["X-Tenant-Id"] = enterpriseId;
|
|
171
|
+
} else if (environment === "iOA") {
|
|
172
|
+
headers["X-Enterprise-Id"] = IOA_DEFAULT_ENTERPRISE_ID;
|
|
173
|
+
headers["X-Tenant-Id"] = IOA_DEFAULT_ENTERPRISE_ID;
|
|
174
|
+
}
|
|
175
|
+
return headers;
|
|
176
|
+
}
|
|
177
|
+
async function getData(session, path, signal) {
|
|
178
|
+
const body = await requestEnvelope(
|
|
179
|
+
`${session.endpoint}${path}`,
|
|
180
|
+
{ headers: identityHeaders(session) },
|
|
181
|
+
signal
|
|
182
|
+
);
|
|
183
|
+
if (body.code !== 0)
|
|
184
|
+
throw new Error(
|
|
185
|
+
`CodeBuddy ${path} failed (code ${body.code}): ${body.msg ?? "unknown error"}`
|
|
186
|
+
);
|
|
187
|
+
return body.data;
|
|
188
|
+
}
|
|
189
|
+
function uuidv7Hex() {
|
|
190
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
191
|
+
const ts = BigInt(Date.now());
|
|
192
|
+
for (let i = 0; i < 6; i++)
|
|
193
|
+
bytes[i] = Number(ts >> BigInt(8 * (5 - i)) & 0xffn);
|
|
194
|
+
bytes[6] = bytes[6] & 15 | 112;
|
|
195
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
196
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
197
|
+
}
|
|
198
|
+
var sessionsByToken = /* @__PURE__ */ new Map();
|
|
199
|
+
function rememberSession(session) {
|
|
200
|
+
sessionsByToken.set(session.accessToken, session);
|
|
201
|
+
}
|
|
202
|
+
function lookupSession(accessToken) {
|
|
203
|
+
if (!accessToken) return void 0;
|
|
204
|
+
const known = sessionsByToken.get(accessToken);
|
|
205
|
+
if (known) return known;
|
|
206
|
+
const recovered = sessionFromJwt(accessToken);
|
|
207
|
+
if (recovered) sessionsByToken.set(accessToken, recovered);
|
|
208
|
+
return recovered;
|
|
209
|
+
}
|
|
210
|
+
function sessionFromJwt(accessToken) {
|
|
211
|
+
try {
|
|
212
|
+
const claims = JSON.parse(
|
|
213
|
+
Buffer.from(accessToken.split(".")[1] ?? "", "base64url").toString(
|
|
214
|
+
"utf8"
|
|
215
|
+
)
|
|
216
|
+
);
|
|
217
|
+
if (!claims.iss) return void 0;
|
|
218
|
+
const issuer = new URL(claims.iss);
|
|
219
|
+
return {
|
|
220
|
+
endpoint: issuer.origin,
|
|
221
|
+
accessToken,
|
|
222
|
+
uid: claims.sub,
|
|
223
|
+
enterpriseId: claims.enterprise_id || void 0,
|
|
224
|
+
// iOA tokens issued from an SSO domain carry no other marker (see test/http.test.ts).
|
|
225
|
+
environment: isIOADomain(issuer.hostname) ? "iOA" : void 0
|
|
226
|
+
};
|
|
227
|
+
} catch {
|
|
228
|
+
return void 0;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/auth.ts
|
|
233
|
+
var LOGIN_TOKEN_PENDING_CODE = 11217;
|
|
234
|
+
var LOGIN_ACCOUNT_PENDING_CODE = 12151;
|
|
235
|
+
var POLL_INTERVAL_MS = 1e3;
|
|
236
|
+
var LOGIN_TIMEOUT_MS = 3e5;
|
|
237
|
+
var EXPIRY_SKEW_MS = 5 * 6e4;
|
|
238
|
+
var UNAUTHENTICATED_HEADERS = {
|
|
239
|
+
"X-No-Authorization": "true",
|
|
240
|
+
"X-No-User-Id": "true",
|
|
241
|
+
"X-No-Enterprise-Id": "true",
|
|
242
|
+
"X-No-Department-Info": "true"
|
|
243
|
+
};
|
|
244
|
+
function sleep(ms, signal) {
|
|
245
|
+
return new Promise((resolve, reject) => {
|
|
246
|
+
if (signal?.aborted) return reject(new Error("Login cancelled"));
|
|
247
|
+
const timer = setTimeout(() => {
|
|
248
|
+
signal?.removeEventListener("abort", onAbort);
|
|
249
|
+
resolve();
|
|
250
|
+
}, ms);
|
|
251
|
+
const onAbort = () => {
|
|
252
|
+
clearTimeout(timer);
|
|
253
|
+
reject(new Error("Login cancelled"));
|
|
254
|
+
};
|
|
255
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
async function promptEnterpriseEndpoint(callbacks) {
|
|
259
|
+
const answer = await callbacks.onPrompt({
|
|
260
|
+
message: "Enterprise domain",
|
|
261
|
+
placeholder: "https://<enterprise>.copilot.qq.com"
|
|
262
|
+
});
|
|
263
|
+
return normalizeEndpoint(answer);
|
|
264
|
+
}
|
|
265
|
+
function reloginHint(credentials) {
|
|
266
|
+
const provider = providerById(credentials.providerId);
|
|
267
|
+
return `${provider?.label ?? "CodeBuddy"} login has expired; run /login ${credentials.providerId}`;
|
|
268
|
+
}
|
|
269
|
+
var CredentialsInvalidError = class extends Error {
|
|
270
|
+
};
|
|
271
|
+
function toCredentials(providerId, endpoint, token, account, environment, previous) {
|
|
272
|
+
const now = Date.now();
|
|
273
|
+
const expiresIn = (token.expiresIn ?? 3600) * 1e3;
|
|
274
|
+
return {
|
|
275
|
+
providerId: providerId || previous?.providerId || "codebuddy",
|
|
276
|
+
refresh: token.refreshToken ?? previous?.refresh ?? "",
|
|
277
|
+
access: token.accessToken,
|
|
278
|
+
expires: now + Math.max(expiresIn - EXPIRY_SKEW_MS, expiresIn / 2),
|
|
279
|
+
refreshExpires: token.refreshExpiresIn ? now + token.refreshExpiresIn * 1e3 : previous?.refreshExpires,
|
|
280
|
+
endpoint,
|
|
281
|
+
// The CLI's applyIOADefaultHeaders: the server's value wins, iOA falls back to its SSO domain.
|
|
282
|
+
domain: token.domain || previous?.domain || (environment === "iOA" ? IOA_DEFAULT_DOMAIN : hostOf(endpoint)),
|
|
283
|
+
environment: environment ?? previous?.environment,
|
|
284
|
+
uid: account?.uid ?? previous?.uid,
|
|
285
|
+
enterpriseId: account?.enterpriseId || previous?.enterpriseId || void 0,
|
|
286
|
+
nickname: account?.nickname ?? previous?.nickname,
|
|
287
|
+
enterpriseName: account?.enterpriseName ?? previous?.enterpriseName
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function sessionFromCredentials(credentials) {
|
|
291
|
+
return {
|
|
292
|
+
endpoint: credentials.endpoint,
|
|
293
|
+
accessToken: credentials.access,
|
|
294
|
+
domain: credentials.domain,
|
|
295
|
+
environment: credentials.environment,
|
|
296
|
+
uid: credentials.uid,
|
|
297
|
+
enterpriseId: credentials.enterpriseId
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
async function pollToken(endpoint, state, signal) {
|
|
301
|
+
const deadline = Date.now() + LOGIN_TIMEOUT_MS;
|
|
302
|
+
const url = `${endpoint}/v2/plugin/auth/token?state=${encodeURIComponent(state)}`;
|
|
303
|
+
while (Date.now() < deadline) {
|
|
304
|
+
const body = await requestEnvelope(
|
|
305
|
+
url,
|
|
306
|
+
{ headers: UNAUTHENTICATED_HEADERS },
|
|
307
|
+
signal
|
|
308
|
+
);
|
|
309
|
+
if (body.data?.accessToken) return body.data;
|
|
310
|
+
if (body.code !== LOGIN_TOKEN_PENDING_CODE && body.code !== 0) {
|
|
311
|
+
throw new Error(
|
|
312
|
+
`CodeBuddy login failed (code ${body.code}): ${body.msg ?? "unknown error"}`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
await sleep(POLL_INTERVAL_MS, signal);
|
|
316
|
+
}
|
|
317
|
+
throw new Error("CodeBuddy login timed out after 5 minutes");
|
|
318
|
+
}
|
|
319
|
+
async function pollAccount(endpoint, state, token, environment, signal) {
|
|
320
|
+
const deadline = Date.now() + LOGIN_TIMEOUT_MS;
|
|
321
|
+
const url = `${endpoint}/v2/plugin/login/account?state=${encodeURIComponent(state)}`;
|
|
322
|
+
const headers = {
|
|
323
|
+
Authorization: `Bearer ${token.accessToken}`,
|
|
324
|
+
"X-Domain": token.domain || (environment === "iOA" ? IOA_DEFAULT_DOMAIN : hostOf(endpoint)),
|
|
325
|
+
"X-No-User-Id": "true",
|
|
326
|
+
"X-No-Enterprise-Id": "true",
|
|
327
|
+
"X-No-Department-Info": "true"
|
|
328
|
+
};
|
|
329
|
+
let authRetried = false;
|
|
330
|
+
while (Date.now() < deadline) {
|
|
331
|
+
let body;
|
|
332
|
+
try {
|
|
333
|
+
body = await requestEnvelope(url, { headers }, signal);
|
|
334
|
+
} catch (error) {
|
|
335
|
+
if (error instanceof HttpError && (error.status === 401 || error.status === 403) && !authRetried) {
|
|
336
|
+
authRetried = true;
|
|
337
|
+
await sleep(POLL_INTERVAL_MS, signal);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
if (body.code === 0 && body.data) return body.data;
|
|
343
|
+
if (body.code !== LOGIN_ACCOUNT_PENDING_CODE) {
|
|
344
|
+
throw new Error(
|
|
345
|
+
`CodeBuddy account lookup failed (code ${body.code}): ${body.msg ?? "unknown error"}`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
await sleep(POLL_INTERVAL_MS, signal);
|
|
349
|
+
}
|
|
350
|
+
throw new Error("CodeBuddy account lookup timed out");
|
|
351
|
+
}
|
|
352
|
+
async function loginCodeBuddy(provider, callbacks) {
|
|
353
|
+
const signal = callbacks.signal;
|
|
354
|
+
const endpoint = provider.endpoint ?? await promptEnterpriseEndpoint(callbacks);
|
|
355
|
+
const environment = provider.environment;
|
|
356
|
+
const stateBody = await requestEnvelope(
|
|
357
|
+
`${endpoint}/v2/plugin/auth/state?platform=${PLATFORM}`,
|
|
358
|
+
{
|
|
359
|
+
method: "POST",
|
|
360
|
+
headers: {
|
|
361
|
+
"Content-Type": "application/json",
|
|
362
|
+
"X-Domain": hostOf(endpoint),
|
|
363
|
+
...UNAUTHENTICATED_HEADERS
|
|
364
|
+
},
|
|
365
|
+
body: "{}"
|
|
366
|
+
},
|
|
367
|
+
signal
|
|
368
|
+
);
|
|
369
|
+
const state = stateBody.data?.state;
|
|
370
|
+
const authUrl = stateBody.data?.authUrl;
|
|
371
|
+
if (!state || !authUrl) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
`CodeBuddy login could not start (code ${stateBody.code}): ${stateBody.msg ?? "missing state"}`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
const url = new URL(authUrl);
|
|
377
|
+
url.searchParams.set("version", CLI_VERSION);
|
|
378
|
+
if (environment === "iOA") url.searchParams.set("ioa", "1");
|
|
379
|
+
callbacks.onAuth({
|
|
380
|
+
url: url.toString(),
|
|
381
|
+
instructions: "Complete the CodeBuddy login in your browser."
|
|
382
|
+
});
|
|
383
|
+
callbacks.onProgress?.("Waiting for browser login...");
|
|
384
|
+
const token = await pollToken(endpoint, state, signal);
|
|
385
|
+
callbacks.onProgress?.("Fetching CodeBuddy account...");
|
|
386
|
+
const account = await pollAccount(
|
|
387
|
+
endpoint,
|
|
388
|
+
state,
|
|
389
|
+
token,
|
|
390
|
+
environment,
|
|
391
|
+
signal
|
|
392
|
+
);
|
|
393
|
+
return toCredentials(provider.id, endpoint, token, account, environment);
|
|
394
|
+
}
|
|
395
|
+
async function refreshCodeBuddy(credentials, signal) {
|
|
396
|
+
if (!credentials.endpoint)
|
|
397
|
+
throw new CredentialsInvalidError(
|
|
398
|
+
`CodeBuddy credentials have no endpoint. ${reloginHint(credentials)}`
|
|
399
|
+
);
|
|
400
|
+
if (!credentials.refresh)
|
|
401
|
+
throw new CredentialsInvalidError(
|
|
402
|
+
`CodeBuddy credentials have no refresh token. ${reloginHint(credentials)}`
|
|
403
|
+
);
|
|
404
|
+
let body;
|
|
405
|
+
try {
|
|
406
|
+
body = await requestEnvelope(
|
|
407
|
+
`${credentials.endpoint}/v2/plugin/auth/token/refresh`,
|
|
408
|
+
{
|
|
409
|
+
method: "POST",
|
|
410
|
+
headers: {
|
|
411
|
+
"Content-Type": "application/json",
|
|
412
|
+
Authorization: `Bearer ${credentials.access}`,
|
|
413
|
+
"X-Refresh-Token": credentials.refresh,
|
|
414
|
+
"X-Auth-Refresh-Source": "plugin",
|
|
415
|
+
"X-Domain": credentials.domain || (credentials.environment === "iOA" ? IOA_DEFAULT_DOMAIN : hostOf(credentials.endpoint))
|
|
416
|
+
},
|
|
417
|
+
body: "{}"
|
|
418
|
+
},
|
|
419
|
+
signal
|
|
420
|
+
);
|
|
421
|
+
} catch (error) {
|
|
422
|
+
if (error instanceof HttpError && (error.status === 401 || error.status === 403)) {
|
|
423
|
+
throw new CredentialsInvalidError(
|
|
424
|
+
`CodeBuddy rejected the token refresh (${error.status}). ${reloginHint(credentials)}`
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
const token = body.data?.accessToken ? body.data : body.data?.data;
|
|
430
|
+
if (!token?.accessToken) {
|
|
431
|
+
if (body.code === 401 || body.code === 403) {
|
|
432
|
+
throw new CredentialsInvalidError(
|
|
433
|
+
`CodeBuddy rejected the token refresh (code ${body.code}). ${reloginHint(credentials)}`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
throw new Error(
|
|
437
|
+
`CodeBuddy token refresh failed (code ${body.code}): ${body.msg ?? "no token returned"}`
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
return toCredentials(
|
|
441
|
+
credentials.providerId,
|
|
442
|
+
credentials.endpoint,
|
|
443
|
+
token,
|
|
444
|
+
void 0,
|
|
445
|
+
credentials.environment,
|
|
446
|
+
credentials
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/credits.ts
|
|
451
|
+
import {
|
|
452
|
+
createAssistantMessageEventStream
|
|
453
|
+
} from "@earendil-works/pi-ai";
|
|
454
|
+
|
|
455
|
+
// src/json.ts
|
|
456
|
+
function isRecord(value) {
|
|
457
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
458
|
+
}
|
|
459
|
+
function field(value, ...path) {
|
|
460
|
+
let current = value;
|
|
461
|
+
for (const key of path) {
|
|
462
|
+
if (!isRecord(current)) return void 0;
|
|
463
|
+
current = current[key];
|
|
464
|
+
}
|
|
465
|
+
return current;
|
|
466
|
+
}
|
|
467
|
+
function numberField(value, ...path) {
|
|
468
|
+
const raw = field(value, ...path);
|
|
469
|
+
const parsed = typeof raw === "string" && raw.trim() !== "" ? Number(raw) : raw;
|
|
470
|
+
return typeof parsed === "number" && Number.isFinite(parsed) ? parsed : void 0;
|
|
471
|
+
}
|
|
472
|
+
function stringField(value, ...path) {
|
|
473
|
+
const raw = field(value, ...path);
|
|
474
|
+
return typeof raw === "string" && raw !== "" ? raw : void 0;
|
|
475
|
+
}
|
|
476
|
+
function arrayField(value, ...path) {
|
|
477
|
+
const raw = field(value, ...path);
|
|
478
|
+
return Array.isArray(raw) ? raw : [];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/models.ts
|
|
482
|
+
import {
|
|
483
|
+
chmodSync,
|
|
484
|
+
mkdirSync,
|
|
485
|
+
readFileSync,
|
|
486
|
+
renameSync,
|
|
487
|
+
writeFileSync
|
|
488
|
+
} from "node:fs";
|
|
489
|
+
import { dirname, join } from "node:path";
|
|
490
|
+
import {
|
|
491
|
+
getAgentDir
|
|
492
|
+
} from "@earendil-works/pi-coding-agent";
|
|
493
|
+
var CODEBUDDY_API = "codebuddy-chat-completions";
|
|
494
|
+
var CACHE_FILE = "codebuddy-models-cache.json";
|
|
495
|
+
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
496
|
+
var VALID_EFFORTS = [
|
|
497
|
+
"minimal",
|
|
498
|
+
"low",
|
|
499
|
+
"medium",
|
|
500
|
+
"high",
|
|
501
|
+
"xhigh",
|
|
502
|
+
"max"
|
|
503
|
+
];
|
|
504
|
+
var DEFAULT_EFFORTS = [
|
|
505
|
+
"low",
|
|
506
|
+
"medium",
|
|
507
|
+
"high",
|
|
508
|
+
"xhigh",
|
|
509
|
+
"max"
|
|
510
|
+
];
|
|
511
|
+
var RESPONSES_APIS = /* @__PURE__ */ new Set([
|
|
512
|
+
"openai-responses",
|
|
513
|
+
"openai-codex-responses",
|
|
514
|
+
"azure-openai-responses"
|
|
515
|
+
]);
|
|
516
|
+
var CHAT_TAG = "chat";
|
|
517
|
+
var remoteModelsByEndpointAndId = /* @__PURE__ */ new Map();
|
|
518
|
+
function cachePath() {
|
|
519
|
+
return join(getAgentDir(), CACHE_FILE);
|
|
520
|
+
}
|
|
521
|
+
function remember(endpoint, models) {
|
|
522
|
+
for (const model of models)
|
|
523
|
+
remoteModelsByEndpointAndId.set(`${endpoint}|${model.id}`, model);
|
|
524
|
+
}
|
|
525
|
+
var cacheFile;
|
|
526
|
+
var cacheFileLoaded = false;
|
|
527
|
+
function readCacheFile() {
|
|
528
|
+
if (cacheFileLoaded) return cacheFile;
|
|
529
|
+
cacheFileLoaded = true;
|
|
530
|
+
try {
|
|
531
|
+
const cache = JSON.parse(
|
|
532
|
+
readFileSync(cachePath(), "utf8")
|
|
533
|
+
);
|
|
534
|
+
if (cache.endpoints && typeof cache.endpoints === "object") {
|
|
535
|
+
for (const [endpoint, entry] of Object.entries(cache.endpoints)) {
|
|
536
|
+
if (entry && Array.isArray(entry.models))
|
|
537
|
+
remember(endpoint, entry.models);
|
|
538
|
+
}
|
|
539
|
+
cacheFile = cache;
|
|
540
|
+
}
|
|
541
|
+
} catch {
|
|
542
|
+
}
|
|
543
|
+
return cacheFile;
|
|
544
|
+
}
|
|
545
|
+
function readModelCache(endpoint) {
|
|
546
|
+
const cache = readCacheFile();
|
|
547
|
+
const entry = cache?.endpoints[endpoint];
|
|
548
|
+
if (!entry) return void 0;
|
|
549
|
+
remember(endpoint, entry.models);
|
|
550
|
+
return entry;
|
|
551
|
+
}
|
|
552
|
+
function isModelCacheStale(endpoint) {
|
|
553
|
+
const entry = readModelCache(endpoint);
|
|
554
|
+
return !entry || Date.now() - entry.fetchedAt > CACHE_TTL_MS;
|
|
555
|
+
}
|
|
556
|
+
function writeModelCache(endpoint, models) {
|
|
557
|
+
try {
|
|
558
|
+
const cache = readCacheFile() ?? { endpoints: {} };
|
|
559
|
+
cache.endpoints[endpoint] = { fetchedAt: Date.now(), models };
|
|
560
|
+
const path = cachePath();
|
|
561
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
562
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
563
|
+
writeFileSync(tmp, JSON.stringify(cache, null, 2), { mode: 384 });
|
|
564
|
+
chmodSync(tmp, 384);
|
|
565
|
+
renameSync(tmp, path);
|
|
566
|
+
cacheFile = cache;
|
|
567
|
+
cacheFileLoaded = true;
|
|
568
|
+
} catch {
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
function remoteModel(id, endpoint) {
|
|
572
|
+
if (remoteModelsByEndpointAndId.size === 0) readCacheFile();
|
|
573
|
+
return remoteModelsByEndpointAndId.get(`${endpoint}|${id}`);
|
|
574
|
+
}
|
|
575
|
+
function cachedModels(endpoint) {
|
|
576
|
+
const entry = readModelCache(endpoint);
|
|
577
|
+
return entry ? entry.models.map((model) => toProviderModel(model, endpoint)) : [];
|
|
578
|
+
}
|
|
579
|
+
async function fetchEnterpriseModels(session, signal) {
|
|
580
|
+
const enterpriseId = sessionEnterpriseId(session);
|
|
581
|
+
if (!enterpriseId) return [];
|
|
582
|
+
try {
|
|
583
|
+
const models = await getData(
|
|
584
|
+
session,
|
|
585
|
+
`/console/enterprises/${encodeURIComponent(enterpriseId)}/config/models`,
|
|
586
|
+
signal
|
|
587
|
+
);
|
|
588
|
+
return Array.isArray(models) ? models : [];
|
|
589
|
+
} catch {
|
|
590
|
+
return [];
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function mergeCatalog(config, enterpriseModels) {
|
|
594
|
+
const byId = /* @__PURE__ */ new Map();
|
|
595
|
+
for (const model of config.models ?? [])
|
|
596
|
+
if (model.id) byId.set(model.id, model);
|
|
597
|
+
for (const model of enterpriseModels)
|
|
598
|
+
if (model.id) byId.set(model.id, { ...byId.get(model.id), ...model });
|
|
599
|
+
const cliModels = config.agents?.find(
|
|
600
|
+
(agent) => agent.name === "cli"
|
|
601
|
+
)?.models;
|
|
602
|
+
const enterpriseChat = enterpriseModels.filter((model) => model.tags?.includes(CHAT_TAG)).map((model) => model.id);
|
|
603
|
+
const allowed = cliModels ? [
|
|
604
|
+
...enterpriseChat.filter((id) => !cliModels.includes(id)).reverse(),
|
|
605
|
+
...cliModels
|
|
606
|
+
] : void 0;
|
|
607
|
+
if (!allowed) return [...byId.values()];
|
|
608
|
+
return allowed.map((id) => byId.get(id)).filter((model) => model !== void 0);
|
|
609
|
+
}
|
|
610
|
+
async function fetchRemoteModels(session, signal) {
|
|
611
|
+
const [config, enterpriseModels] = await Promise.all([
|
|
612
|
+
getData(session, "/v3/config", signal),
|
|
613
|
+
fetchEnterpriseModels(session, signal)
|
|
614
|
+
]);
|
|
615
|
+
const models = mergeCatalog(config ?? {}, enterpriseModels);
|
|
616
|
+
remember(session.endpoint, models);
|
|
617
|
+
writeModelCache(session.endpoint, models);
|
|
618
|
+
return models;
|
|
619
|
+
}
|
|
620
|
+
function isEffort(value) {
|
|
621
|
+
return VALID_EFFORTS.includes(value);
|
|
622
|
+
}
|
|
623
|
+
function supportedEfforts(model) {
|
|
624
|
+
const declared = model.reasoning?.supportedEfforts;
|
|
625
|
+
if (Array.isArray(declared)) return declared.filter(isEffort);
|
|
626
|
+
const map = model.thinkingLevelMap;
|
|
627
|
+
if (!map) return [...DEFAULT_EFFORTS];
|
|
628
|
+
const top = map.max != null ? "max" : map.xhigh != null ? "xhigh" : "high";
|
|
629
|
+
return VALID_EFFORTS.slice(0, VALID_EFFORTS.indexOf(top) + 1);
|
|
630
|
+
}
|
|
631
|
+
function wireEffort(model, effort) {
|
|
632
|
+
return model.thinkingLevelMap?.[effort] ?? effort;
|
|
633
|
+
}
|
|
634
|
+
function isResponsesApi(api) {
|
|
635
|
+
return api !== void 0 && RESPONSES_APIS.has(api);
|
|
636
|
+
}
|
|
637
|
+
function modelBaseUrl(model, endpoint) {
|
|
638
|
+
if (!model.url) return `${endpoint}/v2`;
|
|
639
|
+
const suffix = isResponsesApi(model.api) ? "/responses" : "/chat/completions";
|
|
640
|
+
let base = model.url.trim().replace(/[?#].*$/, "").replace(/\/+$/, "");
|
|
641
|
+
while (base.endsWith(suffix))
|
|
642
|
+
base = base.slice(0, -suffix.length).replace(/\/+$/, "");
|
|
643
|
+
return base;
|
|
644
|
+
}
|
|
645
|
+
function toProviderModel(model, endpoint) {
|
|
646
|
+
const reasoning = model.supportsReasoning === true;
|
|
647
|
+
const efforts = reasoning ? supportedEfforts(model) : [];
|
|
648
|
+
const credits = model.credits?.trim();
|
|
649
|
+
const defaultEffort = isEffort(model.reasoning?.effort) && efforts.includes(model.reasoning.effort) ? model.reasoning.effort : void 0;
|
|
650
|
+
const effortMap = Object.fromEntries(
|
|
651
|
+
efforts.map((effort) => [effort, wireEffort(model, effort)])
|
|
652
|
+
);
|
|
653
|
+
const config = {
|
|
654
|
+
id: model.id,
|
|
655
|
+
name: credits ? `${model.name ?? model.id} (${credits})` : model.name ?? model.id,
|
|
656
|
+
api: CODEBUDDY_API,
|
|
657
|
+
baseUrl: modelBaseUrl(model, endpoint),
|
|
658
|
+
reasoning: reasoning && efforts.length > 0,
|
|
659
|
+
input: model.supportsImages ? ["text", "image"] : ["text"],
|
|
660
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
661
|
+
contextWindow: model.maxInputTokens ?? 128e3,
|
|
662
|
+
maxTokens: model.maxOutputTokens ?? 16e3,
|
|
663
|
+
// pi: level → reasoning_effort value; null marks a level unsupported, `off` sends "none" like the CLI.
|
|
664
|
+
thinkingLevelMap: efforts.length > 0 ? {
|
|
665
|
+
off: "none",
|
|
666
|
+
...Object.fromEntries(
|
|
667
|
+
VALID_EFFORTS.map((effort) => [
|
|
668
|
+
effort,
|
|
669
|
+
effortMap[effort] ?? null
|
|
670
|
+
])
|
|
671
|
+
)
|
|
672
|
+
} : void 0,
|
|
673
|
+
// omp: the same ladder for its effort-based thinking control. `requiresEffort: false` stops omp's
|
|
674
|
+
// lineage rules from clamping "off" to the lowest effort; the server accepts "none".
|
|
675
|
+
thinking: efforts.length > 0 ? {
|
|
676
|
+
mode: "effort",
|
|
677
|
+
efforts,
|
|
678
|
+
defaultLevel: defaultEffort,
|
|
679
|
+
effortMap,
|
|
680
|
+
requiresEffort: false
|
|
681
|
+
} : void 0,
|
|
682
|
+
compat: {
|
|
683
|
+
supportsStore: false,
|
|
684
|
+
supportsDeveloperRole: false,
|
|
685
|
+
supportsReasoningEffort: efforts.length > 0,
|
|
686
|
+
reasoningDisableMode: efforts.length > 0 ? "none-effort" : void 0,
|
|
687
|
+
supportsUsageInStreaming: true,
|
|
688
|
+
maxTokensField: "max_tokens"
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
return config;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// src/credits.ts
|
|
695
|
+
var CREDIT_USAGE_FIELD = "codebuddyCredit";
|
|
696
|
+
function scanLine(line, meter) {
|
|
697
|
+
if (!line.startsWith("data:") || !line.includes('"credit"')) return;
|
|
698
|
+
try {
|
|
699
|
+
const credit = numberField(JSON.parse(line.slice(5)), "usage", "credit");
|
|
700
|
+
if (credit !== void 0) meter.credit = credit;
|
|
701
|
+
} catch {
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function creditScanner(meter) {
|
|
705
|
+
const decoder = new TextDecoder();
|
|
706
|
+
let buffer = "";
|
|
707
|
+
const scan = (text, flush) => {
|
|
708
|
+
buffer += text;
|
|
709
|
+
const lines = buffer.split(/\r?\n/);
|
|
710
|
+
buffer = flush ? "" : lines.pop() ?? "";
|
|
711
|
+
for (const line of lines) scanLine(line, meter);
|
|
712
|
+
};
|
|
713
|
+
return new TransformStream({
|
|
714
|
+
transform(chunk, controller) {
|
|
715
|
+
scan(decoder.decode(chunk, { stream: true }), false);
|
|
716
|
+
controller.enqueue(chunk);
|
|
717
|
+
},
|
|
718
|
+
flush() {
|
|
719
|
+
scan(decoder.decode(), true);
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
function creditFetch(meter, base = fetch) {
|
|
724
|
+
return async (input, init) => {
|
|
725
|
+
const response = await base(input, init);
|
|
726
|
+
if (!response.body) return response;
|
|
727
|
+
return new Response(response.body.pipeThrough(creditScanner(meter)), {
|
|
728
|
+
status: response.status,
|
|
729
|
+
statusText: response.statusText,
|
|
730
|
+
headers: response.headers
|
|
731
|
+
});
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
function attachCredit(inner, meter) {
|
|
735
|
+
const outer = createAssistantMessageEventStream();
|
|
736
|
+
const stamp = (message) => {
|
|
737
|
+
if (meter.credit !== void 0)
|
|
738
|
+
Object.assign(message.usage, { [CREDIT_USAGE_FIELD]: meter.credit });
|
|
739
|
+
};
|
|
740
|
+
const ZERO_USAGE = {
|
|
741
|
+
input: 0,
|
|
742
|
+
output: 0,
|
|
743
|
+
cacheRead: 0,
|
|
744
|
+
cacheWrite: 0,
|
|
745
|
+
totalTokens: 0,
|
|
746
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
|
|
747
|
+
};
|
|
748
|
+
void (async () => {
|
|
749
|
+
let partial;
|
|
750
|
+
try {
|
|
751
|
+
for await (const event of inner) {
|
|
752
|
+
if (event.type === "done") stamp(event.message);
|
|
753
|
+
else if (event.type === "error") stamp(event.error);
|
|
754
|
+
else partial = event.partial;
|
|
755
|
+
outer.push(event);
|
|
756
|
+
}
|
|
757
|
+
} catch (error) {
|
|
758
|
+
const message = {
|
|
759
|
+
...partial ?? {
|
|
760
|
+
role: "assistant",
|
|
761
|
+
content: [],
|
|
762
|
+
api: CODEBUDDY_API,
|
|
763
|
+
provider: "codebuddy",
|
|
764
|
+
model: "unknown",
|
|
765
|
+
usage: ZERO_USAGE,
|
|
766
|
+
timestamp: Date.now()
|
|
767
|
+
},
|
|
768
|
+
content: [],
|
|
769
|
+
usage: partial?.usage ?? ZERO_USAGE,
|
|
770
|
+
stopReason: "error",
|
|
771
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
772
|
+
timestamp: partial?.timestamp ?? Date.now()
|
|
773
|
+
};
|
|
774
|
+
outer.push({ type: "error", reason: "error", error: message });
|
|
775
|
+
} finally {
|
|
776
|
+
outer.end();
|
|
777
|
+
}
|
|
778
|
+
})();
|
|
779
|
+
return outer;
|
|
780
|
+
}
|
|
781
|
+
function messageCredit(message) {
|
|
782
|
+
return numberField(message, "usage", CREDIT_USAGE_FIELD);
|
|
783
|
+
}
|
|
784
|
+
function accumulateMessage(credits, sum, message) {
|
|
785
|
+
if (field(message, "role") !== "assistant" || !isCodeBuddyProvider(field(message, "provider")))
|
|
786
|
+
return;
|
|
787
|
+
const credit = messageCredit(message);
|
|
788
|
+
if (credit === void 0) {
|
|
789
|
+
credits.unknown++;
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
sum.value += credit;
|
|
793
|
+
credits.charged++;
|
|
794
|
+
credits.last = credit;
|
|
795
|
+
}
|
|
796
|
+
function collectSessionCredits(entries) {
|
|
797
|
+
const credits = { total: 0, charged: 0, unknown: 0 };
|
|
798
|
+
const sum = { value: 0 };
|
|
799
|
+
for (const entry of entries) {
|
|
800
|
+
if (field(entry, "type") !== "message") continue;
|
|
801
|
+
accumulateMessage(credits, sum, field(entry, "message"));
|
|
802
|
+
}
|
|
803
|
+
credits.total = roundCredit(sum.value);
|
|
804
|
+
return credits;
|
|
805
|
+
}
|
|
806
|
+
function roundCredit(value) {
|
|
807
|
+
return Math.round(100 * value) / 100;
|
|
808
|
+
}
|
|
809
|
+
function formatSessionCredits(credits) {
|
|
810
|
+
const unknown = credits.unknown ? ` (${credits.unknown} without charge info)` : "";
|
|
811
|
+
return `CodeBuddy session: ${credits.total} credits${unknown}`;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// src/quota.ts
|
|
815
|
+
var PACKAGE_STATUS_VALID = 0;
|
|
816
|
+
var PACKAGE_STATUS_USED_UP = 3;
|
|
817
|
+
var CAPACITY_TYPE_SLICE = 4;
|
|
818
|
+
var UNLIMITED = -1;
|
|
819
|
+
var PACKAGE_CODES = [
|
|
820
|
+
"TCACA_code_001_PqouKr6QWV",
|
|
821
|
+
// free (trial)
|
|
822
|
+
"TCACA_code_002_AkiJS3ZHF5",
|
|
823
|
+
// pro monthly
|
|
824
|
+
"TCACA_code_003_FAnt7lcmRT",
|
|
825
|
+
// pro yearly
|
|
826
|
+
"TCACA_code_006_DbXS0lrypC",
|
|
827
|
+
// gift pack
|
|
828
|
+
"TCACA_code_007_nzdH5h4Nl0",
|
|
829
|
+
// activity pack
|
|
830
|
+
"TCACA_code_008_cfWoLwvjU4",
|
|
831
|
+
// free monthly (experience plan)
|
|
832
|
+
"TCACA_code_009_0XmEQc2xOf",
|
|
833
|
+
// legacy top-up
|
|
834
|
+
"TCACA_code_023_4xbGhMrE6q",
|
|
835
|
+
"TCACA_code_026_BaESVICNoi",
|
|
836
|
+
"TCACA_code_027_0FCGVA6vSa",
|
|
837
|
+
"TCACA_code_028_NtpWi0jzXs",
|
|
838
|
+
// version bonus
|
|
839
|
+
"TCACA_code_029_6wCGEWquYy",
|
|
840
|
+
// failure compensation
|
|
841
|
+
"TCACA_code_030_BjSt89qTvr",
|
|
842
|
+
// new-user bonus
|
|
843
|
+
"TCACA_code_035_ArVxJcGDsm",
|
|
844
|
+
// free subscription
|
|
845
|
+
"TCACA_code_036_lupO5WgNdG",
|
|
846
|
+
// 500-credit top-up
|
|
847
|
+
"TCACA_code_037_WxOD3MpI2o",
|
|
848
|
+
// version bonus
|
|
849
|
+
"TCACA_code_038_OhvqZtiPKr",
|
|
850
|
+
// custom top-up
|
|
851
|
+
"TCACA_code_039_KRcQj7wUat",
|
|
852
|
+
// pro trial monthly
|
|
853
|
+
"TCACA_code_040_mi9rCYg46x",
|
|
854
|
+
// pro trial yearly
|
|
855
|
+
"TCACA_code_043_6oIkxT7Z0S",
|
|
856
|
+
"TCACA_code_043_6olxxT7Z0S",
|
|
857
|
+
"TCACA_code_044_JRZ9sYCgua",
|
|
858
|
+
// 200-credit top-up
|
|
859
|
+
"TCACA_code_045_JuyAXGlovx",
|
|
860
|
+
// 5000-credit top-up
|
|
861
|
+
"TCACA_code_046_LfNpbzEa5X"
|
|
862
|
+
// 10000-credit top-up
|
|
863
|
+
];
|
|
864
|
+
async function postBilling(session, path, body, signal) {
|
|
865
|
+
const envelope = await requestEnvelope(
|
|
866
|
+
`${session.endpoint}/billing/meter/${path}`,
|
|
867
|
+
{
|
|
868
|
+
method: "POST",
|
|
869
|
+
headers: {
|
|
870
|
+
...identityHeaders(session),
|
|
871
|
+
"Content-Type": "application/json"
|
|
872
|
+
},
|
|
873
|
+
body: JSON.stringify(body)
|
|
874
|
+
},
|
|
875
|
+
signal
|
|
876
|
+
);
|
|
877
|
+
if (envelope.code !== 0)
|
|
878
|
+
throw new Error(
|
|
879
|
+
`CodeBuddy ${path} failed (code ${envelope.code}): ${envelope.msg ?? "unknown error"}`
|
|
880
|
+
);
|
|
881
|
+
return envelope.data;
|
|
882
|
+
}
|
|
883
|
+
function parseEnterpriseUsage(data) {
|
|
884
|
+
const limitNum = numberField(data, "limitNum");
|
|
885
|
+
const unlimited = limitNum === UNLIMITED;
|
|
886
|
+
const hasGift = field(data, "exclusiveGift", "hasGift") === true;
|
|
887
|
+
const giftUsed = hasGift ? numberField(data, "exclusiveGift", "usedCredits") ?? 0 : 0;
|
|
888
|
+
const giftTotal = hasGift ? numberField(data, "exclusiveGift", "totalCredits") ?? 0 : 0;
|
|
889
|
+
return {
|
|
890
|
+
kind: "enterprise",
|
|
891
|
+
used: roundCredit((numberField(data, "credit") ?? 0) + giftUsed),
|
|
892
|
+
limit: unlimited || limitNum === void 0 ? void 0 : roundCredit(limitNum + giftTotal),
|
|
893
|
+
cycleStart: stringField(data, "cycleStartTime"),
|
|
894
|
+
cycleEnd: stringField(data, "cycleEndTime"),
|
|
895
|
+
resetAt: stringField(data, "cycleResetTime") ?? stringField(data, "cycleEndTime")
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
function packageCapacity(account) {
|
|
899
|
+
const status = numberField(account, "Status");
|
|
900
|
+
if (status !== PACKAGE_STATUS_VALID && status !== PACKAGE_STATUS_USED_UP)
|
|
901
|
+
return void 0;
|
|
902
|
+
const today = numberField(account, "CapacityType") === CAPACITY_TYPE_SLICE ? arrayField(account, "SlicePeriodUsageDetails")[0] : void 0;
|
|
903
|
+
const total = today ? numberField(today, "SlicePeriodCapacitySizePrecise") : numberField(account, "CycleCapacitySizePrecise");
|
|
904
|
+
const left = today ? numberField(today, "SlicePeriodCapacityRemainPrecise") : numberField(account, "CycleCapacityRemainPrecise");
|
|
905
|
+
return total === void 0 || left === void 0 ? void 0 : { total, left };
|
|
906
|
+
}
|
|
907
|
+
function parsePersonalPackages(...responses) {
|
|
908
|
+
let total = 0;
|
|
909
|
+
let left = 0;
|
|
910
|
+
for (const data of responses) {
|
|
911
|
+
for (const account of arrayField(data, "Accounts")) {
|
|
912
|
+
const capacity = packageCapacity(account);
|
|
913
|
+
if (!capacity) continue;
|
|
914
|
+
total += capacity.total;
|
|
915
|
+
left += capacity.left;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return {
|
|
919
|
+
kind: "personal",
|
|
920
|
+
used: roundCredit(total - left),
|
|
921
|
+
limit: roundCredit(total)
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
function localDateTime(date, endOfDay) {
|
|
925
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
926
|
+
const day = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
927
|
+
return `${day} ${endOfDay ? "23:59:59" : "00:00:00"}`;
|
|
928
|
+
}
|
|
929
|
+
async function fetchAccountQuota(session, signal) {
|
|
930
|
+
if (sessionEnterpriseId(session)) {
|
|
931
|
+
return parseEnterpriseUsage(
|
|
932
|
+
await postBilling(session, "get-enterprise-user-usage", {}, signal)
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
const now = /* @__PURE__ */ new Date();
|
|
936
|
+
const [paid, free] = await Promise.all([
|
|
937
|
+
postBilling(
|
|
938
|
+
session,
|
|
939
|
+
"get-user-resource-paid-packages",
|
|
940
|
+
{
|
|
941
|
+
PageNumber: 1,
|
|
942
|
+
PageSize: 200,
|
|
943
|
+
Status: [PACKAGE_STATUS_VALID, PACKAGE_STATUS_USED_UP],
|
|
944
|
+
PackageCodes: PACKAGE_CODES
|
|
945
|
+
},
|
|
946
|
+
signal
|
|
947
|
+
),
|
|
948
|
+
postBilling(
|
|
949
|
+
session,
|
|
950
|
+
"get-user-resource-free-packages",
|
|
951
|
+
{
|
|
952
|
+
PageNumber: 1,
|
|
953
|
+
PageSize: 200,
|
|
954
|
+
Status: [PACKAGE_STATUS_VALID],
|
|
955
|
+
PackageCodes: PACKAGE_CODES,
|
|
956
|
+
SlicePeriodStartTime: localDateTime(now, false),
|
|
957
|
+
SlicePeriodEndTime: localDateTime(now, true)
|
|
958
|
+
},
|
|
959
|
+
signal
|
|
960
|
+
)
|
|
961
|
+
]);
|
|
962
|
+
return parsePersonalPackages(paid, free);
|
|
963
|
+
}
|
|
964
|
+
function formatAccountQuota(quota) {
|
|
965
|
+
const allowance = quota.limit === void 0 ? "unlimited" : `${quota.limit}`;
|
|
966
|
+
return `CodeBuddy cycle: ${quota.used} / ${allowance} credits`;
|
|
967
|
+
}
|
|
968
|
+
function describeAccountQuota(quota) {
|
|
969
|
+
const lines = [formatAccountQuota(quota)];
|
|
970
|
+
if (quota.limit !== void 0 && quota.limit > 0) {
|
|
971
|
+
lines.push(
|
|
972
|
+
`Remaining: ${roundCredit(Math.max(0, quota.limit - quota.used))} credits (${(quota.used / quota.limit * 100).toFixed(1)}% used)`
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
if (quota.cycleStart && quota.cycleEnd)
|
|
976
|
+
lines.push(`Cycle: ${quota.cycleStart} \u2013 ${quota.cycleEnd}`);
|
|
977
|
+
if (quota.resetAt) lines.push(`Resets: ${quota.resetAt}`);
|
|
978
|
+
return lines;
|
|
979
|
+
}
|
|
980
|
+
var QuotaService = class _QuotaService {
|
|
981
|
+
states = /* @__PURE__ */ new Map();
|
|
982
|
+
checkedAt = /* @__PURE__ */ new Map();
|
|
983
|
+
pending = /* @__PURE__ */ new Map();
|
|
984
|
+
fetchQuota;
|
|
985
|
+
ttlMs;
|
|
986
|
+
now;
|
|
987
|
+
constructor(fetchQuota = fetchAccountQuota, ttlMs = 6e4, now = Date.now) {
|
|
988
|
+
this.fetchQuota = fetchQuota;
|
|
989
|
+
this.ttlMs = ttlMs;
|
|
990
|
+
this.now = now;
|
|
991
|
+
}
|
|
992
|
+
/** Token refreshes keep the account; only a different endpoint or user resets the snapshot. */
|
|
993
|
+
static identity(session) {
|
|
994
|
+
return `${session.endpoint}|${session.uid ?? ""}|${sessionEnterpriseId(session) ?? ""}`;
|
|
995
|
+
}
|
|
996
|
+
peek(identity) {
|
|
997
|
+
return this.states.get(identity) ?? { status: "unavailable" };
|
|
998
|
+
}
|
|
999
|
+
read(session, force = false) {
|
|
1000
|
+
const identity = _QuotaService.identity(session);
|
|
1001
|
+
const state = this.peek(identity);
|
|
1002
|
+
const checkedAt = this.checkedAt.get(identity);
|
|
1003
|
+
const inflight = this.pending.get(identity);
|
|
1004
|
+
if (inflight) return inflight;
|
|
1005
|
+
if (!force && checkedAt !== void 0 && this.now() - checkedAt < this.ttlMs)
|
|
1006
|
+
return Promise.resolve(state);
|
|
1007
|
+
const request = this.fetchQuota(session, AbortSignal.timeout(1e4)).then((quota) => ({ status: "fresh", quota })).catch((error) => {
|
|
1008
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1009
|
+
return state.status === "unavailable" ? { status: "unavailable", error: message } : { status: "stale", quota: state.quota, error: message };
|
|
1010
|
+
}).then((next) => {
|
|
1011
|
+
if (_QuotaService.identity(session) === identity) {
|
|
1012
|
+
this.states.set(identity, next);
|
|
1013
|
+
this.checkedAt.set(identity, this.now());
|
|
1014
|
+
this.pending.delete(identity);
|
|
1015
|
+
}
|
|
1016
|
+
return next;
|
|
1017
|
+
});
|
|
1018
|
+
this.pending.set(identity, request);
|
|
1019
|
+
return request;
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
// src/credit-display.ts
|
|
1024
|
+
var STATUS_KEY = "codebuddy-credits";
|
|
1025
|
+
var AUTH_TIMEOUT_MS = 1e4;
|
|
1026
|
+
function quotaText(state) {
|
|
1027
|
+
if (state.status === "unavailable") return void 0;
|
|
1028
|
+
return `${formatAccountQuota(state.quota)}${state.status === "stale" ? " [stale]" : ""}`;
|
|
1029
|
+
}
|
|
1030
|
+
function registerCreditDisplay(pi, service = new QuotaService()) {
|
|
1031
|
+
let refreshId = 0;
|
|
1032
|
+
let activeIdentity;
|
|
1033
|
+
let credits = { total: 0, charged: 0, unknown: 0 };
|
|
1034
|
+
const sum = { value: 0 };
|
|
1035
|
+
const rebuildCredits = (ctx) => {
|
|
1036
|
+
credits = { total: 0, charged: 0, unknown: 0 };
|
|
1037
|
+
sum.value = 0;
|
|
1038
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
1039
|
+
if (field(entry, "type") !== "message") continue;
|
|
1040
|
+
accumulateMessage(credits, sum, field(entry, "message"));
|
|
1041
|
+
}
|
|
1042
|
+
};
|
|
1043
|
+
const isActive = (ctx) => /^codebuddy(-|$)/.test(ctx.model?.provider ?? "");
|
|
1044
|
+
const publish = (ctx) => {
|
|
1045
|
+
if (!ctx.hasUI) return;
|
|
1046
|
+
credits.total = roundCredit(sum.value);
|
|
1047
|
+
if (!isActive(ctx) && credits.charged + credits.unknown === 0) {
|
|
1048
|
+
ctx.ui.setStatus(STATUS_KEY, void 0);
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
const quota = activeIdentity ? quotaText(service.peek(activeIdentity)) : void 0;
|
|
1052
|
+
ctx.ui.setStatus(
|
|
1053
|
+
STATUS_KEY,
|
|
1054
|
+
[formatSessionCredits(credits), quota].filter(Boolean).join(" \xB7 ")
|
|
1055
|
+
);
|
|
1056
|
+
};
|
|
1057
|
+
const resolveSession = async (ctx) => {
|
|
1058
|
+
if (!isActive(ctx) || !ctx.model?.provider) return void 0;
|
|
1059
|
+
let timer;
|
|
1060
|
+
try {
|
|
1061
|
+
const token = await Promise.race([
|
|
1062
|
+
ctx.modelRegistry.getApiKeyForProvider(ctx.model.provider),
|
|
1063
|
+
new Promise((resolve) => {
|
|
1064
|
+
timer = setTimeout(() => resolve(void 0), AUTH_TIMEOUT_MS);
|
|
1065
|
+
})
|
|
1066
|
+
]);
|
|
1067
|
+
const session = lookupSession(token);
|
|
1068
|
+
if (session) activeIdentity = QuotaService.identity(session);
|
|
1069
|
+
return session;
|
|
1070
|
+
} catch {
|
|
1071
|
+
return void 0;
|
|
1072
|
+
} finally {
|
|
1073
|
+
clearTimeout(timer);
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
const refresh = async (ctx, force = false) => {
|
|
1077
|
+
const id = ++refreshId;
|
|
1078
|
+
publish(ctx);
|
|
1079
|
+
if (!isActive(ctx)) return;
|
|
1080
|
+
const session = await resolveSession(ctx);
|
|
1081
|
+
if (!session || id !== refreshId) return;
|
|
1082
|
+
await service.read(session, force);
|
|
1083
|
+
if (id === refreshId) publish(ctx);
|
|
1084
|
+
};
|
|
1085
|
+
pi.on("session_start", (_event, ctx) => {
|
|
1086
|
+
rebuildCredits(ctx);
|
|
1087
|
+
void refresh(ctx);
|
|
1088
|
+
});
|
|
1089
|
+
pi.on("message_end", (event, ctx) => {
|
|
1090
|
+
accumulateMessage(credits, sum, event.message);
|
|
1091
|
+
publish(ctx);
|
|
1092
|
+
});
|
|
1093
|
+
pi.on("agent_end", (_event, ctx) => void refresh(ctx, true));
|
|
1094
|
+
pi.on("model_select", (_event, ctx) => void refresh(ctx));
|
|
1095
|
+
pi.registerCommand("codebuddy-usage", {
|
|
1096
|
+
description: "Show CodeBuddy credits spent in this session and every logged-in account's billing-cycle quota",
|
|
1097
|
+
handler: async (_args, ctx) => {
|
|
1098
|
+
const credits2 = collectSessionCredits(ctx.sessionManager.getEntries());
|
|
1099
|
+
const lines = [
|
|
1100
|
+
formatSessionCredits(credits2),
|
|
1101
|
+
`Charged requests: ${credits2.charged}${credits2.last !== void 0 ? ` \xB7 last request: ${credits2.last} credits` : ""}`
|
|
1102
|
+
];
|
|
1103
|
+
const session = await resolveSession(ctx);
|
|
1104
|
+
if (!session) {
|
|
1105
|
+
lines.push("Account quota: not logged in to CodeBuddy.");
|
|
1106
|
+
} else {
|
|
1107
|
+
const provider = providerById(ctx.model?.provider);
|
|
1108
|
+
lines.push(`Account: ${provider?.label ?? ctx.model?.provider}`);
|
|
1109
|
+
const state = await service.read(session, true);
|
|
1110
|
+
if (state.status === "unavailable")
|
|
1111
|
+
lines.push(
|
|
1112
|
+
`Account quota unavailable: ${state.error ?? "unknown error"}`
|
|
1113
|
+
);
|
|
1114
|
+
else {
|
|
1115
|
+
lines.push(...describeAccountQuota(state.quota));
|
|
1116
|
+
if (state.status === "stale")
|
|
1117
|
+
lines.push(
|
|
1118
|
+
`Refresh failed (${state.error}); showing the last snapshot.`
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
for (const site of PROVIDERS) {
|
|
1123
|
+
if (site.id === ctx.model?.provider) continue;
|
|
1124
|
+
try {
|
|
1125
|
+
const token = await ctx.modelRegistry.getApiKeyForProvider(site.id);
|
|
1126
|
+
const other = lookupSession(token);
|
|
1127
|
+
if (!other) continue;
|
|
1128
|
+
const state = service.peek(QuotaService.identity(other));
|
|
1129
|
+
const text = quotaText(state) ?? "quota not read yet";
|
|
1130
|
+
lines.push(`${site.label}: ${text}`);
|
|
1131
|
+
} catch {
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
1135
|
+
publish(ctx);
|
|
1136
|
+
}
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// src/stream.ts
|
|
1141
|
+
import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
1142
|
+
function streamCodeBuddy(model, context, options) {
|
|
1143
|
+
const accessToken = options?.apiKey;
|
|
1144
|
+
if (!accessToken)
|
|
1145
|
+
throw new Error(
|
|
1146
|
+
"CodeBuddy is not logged in. Run /login and select CodeBuddy."
|
|
1147
|
+
);
|
|
1148
|
+
const session = lookupSession(accessToken) ?? {
|
|
1149
|
+
endpoint: new URL(model.baseUrl).origin,
|
|
1150
|
+
accessToken
|
|
1151
|
+
};
|
|
1152
|
+
const remote = remoteModel(model.id, session.endpoint);
|
|
1153
|
+
const modelApiKey = remote?.url ? remote.apiKey?.trim() : void 0;
|
|
1154
|
+
const requestId = uuidv7Hex();
|
|
1155
|
+
const target = {
|
|
1156
|
+
...model,
|
|
1157
|
+
api: isResponsesApi(remote?.api) ? "openai-responses" : "openai-completions",
|
|
1158
|
+
baseUrl: remote ? modelBaseUrl(remote, session.endpoint) : `${session.endpoint}/v2`,
|
|
1159
|
+
// A model-level key replaces the CodeBuddy session, as in the CLI.
|
|
1160
|
+
headers: modelApiKey ? { ...model.headers } : { ...model.headers, ...identityHeaders(session) }
|
|
1161
|
+
};
|
|
1162
|
+
const meter = {};
|
|
1163
|
+
const inner = streamSimple(target, context, {
|
|
1164
|
+
...options,
|
|
1165
|
+
fetch: creditFetch(meter, options?.fetch),
|
|
1166
|
+
apiKey: modelApiKey || accessToken,
|
|
1167
|
+
headers: modelApiKey ? options?.headers : {
|
|
1168
|
+
...options?.headers,
|
|
1169
|
+
"X-Conversation-ID": options?.sessionId ?? crypto.randomUUID(),
|
|
1170
|
+
"X-Conversation-Request-ID": requestId,
|
|
1171
|
+
"X-Conversation-Message-ID": requestId,
|
|
1172
|
+
"X-Request-ID": requestId,
|
|
1173
|
+
"X-Agent-Intent": "craft"
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
return attachCredit(inner, meter);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// src/index.ts
|
|
1180
|
+
var STARTUP_FETCH_TIMEOUT_MS = 1e4;
|
|
1181
|
+
function rememberCredentials(credentials) {
|
|
1182
|
+
const session = sessionFromCredentials(credentials);
|
|
1183
|
+
rememberSession(session);
|
|
1184
|
+
return session;
|
|
1185
|
+
}
|
|
1186
|
+
function toProviderModels(models, session) {
|
|
1187
|
+
return models.map((model) => toProviderModel(model, session.endpoint));
|
|
1188
|
+
}
|
|
1189
|
+
function storedCredentials() {
|
|
1190
|
+
try {
|
|
1191
|
+
const auth = JSON.parse(
|
|
1192
|
+
readFileSync2(join2(getAgentDir2(), "auth.json"), "utf8")
|
|
1193
|
+
);
|
|
1194
|
+
return Object.entries(auth).filter(([id]) => /^codebuddy(-|$)/.test(id)).map(([, value]) => value);
|
|
1195
|
+
} catch {
|
|
1196
|
+
return [];
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
function endpointFor(site) {
|
|
1200
|
+
return site.endpoint ?? storedCredentials().find((credential) => credential.providerId === site.id)?.endpoint;
|
|
1201
|
+
}
|
|
1202
|
+
async function startupModelsFor(site) {
|
|
1203
|
+
const endpoint = endpointFor(site);
|
|
1204
|
+
if (!endpoint) return [];
|
|
1205
|
+
if (!isModelCacheStale(endpoint)) return cachedModels(endpoint);
|
|
1206
|
+
try {
|
|
1207
|
+
const credential = storedCredentials().find(
|
|
1208
|
+
(candidate) => candidate.type === "oauth" && candidate.endpoint === endpoint
|
|
1209
|
+
);
|
|
1210
|
+
if (!credential || credential.expires <= Date.now())
|
|
1211
|
+
return cachedModels(endpoint);
|
|
1212
|
+
const session = rememberCredentials(credential);
|
|
1213
|
+
return toProviderModels(
|
|
1214
|
+
await fetchRemoteModels(
|
|
1215
|
+
session,
|
|
1216
|
+
AbortSignal.timeout(STARTUP_FETCH_TIMEOUT_MS)
|
|
1217
|
+
),
|
|
1218
|
+
session
|
|
1219
|
+
);
|
|
1220
|
+
} catch {
|
|
1221
|
+
return cachedModels(endpoint);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
async function codebuddyExtension(pi) {
|
|
1225
|
+
const buildConfig = (site, models) => {
|
|
1226
|
+
const config = {
|
|
1227
|
+
name: site.label,
|
|
1228
|
+
baseUrl: models[0]?.baseUrl,
|
|
1229
|
+
api: CODEBUDDY_API,
|
|
1230
|
+
streamSimple: streamCodeBuddy,
|
|
1231
|
+
models,
|
|
1232
|
+
oauth: {
|
|
1233
|
+
name: site.label,
|
|
1234
|
+
isSubscription: true,
|
|
1235
|
+
async login(callbacks) {
|
|
1236
|
+
const credentials = await loginCodeBuddy(site, callbacks);
|
|
1237
|
+
const session = rememberCredentials(credentials);
|
|
1238
|
+
try {
|
|
1239
|
+
callbacks.onProgress?.("Fetching CodeBuddy models...");
|
|
1240
|
+
const remote = await fetchRemoteModels(session, callbacks.signal);
|
|
1241
|
+
pi.registerProvider(
|
|
1242
|
+
site.id,
|
|
1243
|
+
buildConfig(site, toProviderModels(remote, session))
|
|
1244
|
+
);
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
callbacks.onProgress?.(
|
|
1247
|
+
`Logged in, but fetching models failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
return credentials;
|
|
1251
|
+
},
|
|
1252
|
+
async refreshToken(credentials, signal) {
|
|
1253
|
+
const refreshed = await refreshCodeBuddy(
|
|
1254
|
+
credentials,
|
|
1255
|
+
signal
|
|
1256
|
+
);
|
|
1257
|
+
rememberCredentials(refreshed);
|
|
1258
|
+
return refreshed;
|
|
1259
|
+
},
|
|
1260
|
+
getApiKey(credentials) {
|
|
1261
|
+
rememberCredentials(credentials);
|
|
1262
|
+
return credentials.access;
|
|
1263
|
+
}
|
|
1264
|
+
},
|
|
1265
|
+
// pi: invoked with the stored credential when the catalog refreshes.
|
|
1266
|
+
async refreshModels(context) {
|
|
1267
|
+
const credential = context.credential;
|
|
1268
|
+
if (context.allowNetwork && credential?.type === "oauth" && credential.endpoint) {
|
|
1269
|
+
const session = rememberCredentials(
|
|
1270
|
+
credential
|
|
1271
|
+
);
|
|
1272
|
+
return toProviderModels(
|
|
1273
|
+
await fetchRemoteModels(session, context.signal),
|
|
1274
|
+
session
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
const endpoint = credential?.type === "oauth" ? credential.endpoint : site.endpoint;
|
|
1278
|
+
return endpoint ? cachedModels(endpoint) : [];
|
|
1279
|
+
},
|
|
1280
|
+
// omp: invoked with the raw stored access token.
|
|
1281
|
+
async fetchDynamicModels(apiKey) {
|
|
1282
|
+
const session = lookupSession(apiKey);
|
|
1283
|
+
if (!session) return cachedModels(site.endpoint ?? "");
|
|
1284
|
+
try {
|
|
1285
|
+
return toProviderModels(await fetchRemoteModels(session), session);
|
|
1286
|
+
} catch {
|
|
1287
|
+
return cachedModels(session.endpoint);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
return config;
|
|
1292
|
+
};
|
|
1293
|
+
await Promise.all(
|
|
1294
|
+
PROVIDERS.map(async (site) => {
|
|
1295
|
+
pi.registerProvider(
|
|
1296
|
+
site.id,
|
|
1297
|
+
buildConfig(site, await startupModelsFor(site))
|
|
1298
|
+
);
|
|
1299
|
+
})
|
|
1300
|
+
);
|
|
1301
|
+
registerCreditDisplay(pi);
|
|
1302
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
1303
|
+
const registry = ctx.modelRegistry;
|
|
1304
|
+
if (typeof registry.getProviderAuth !== "function") return;
|
|
1305
|
+
for (const site of PROVIDERS) {
|
|
1306
|
+
const endpoint = endpointFor(site);
|
|
1307
|
+
if (!endpoint || !isModelCacheStale(endpoint)) continue;
|
|
1308
|
+
try {
|
|
1309
|
+
await registry.refresh({
|
|
1310
|
+
providers: [site.id],
|
|
1311
|
+
allowNetwork: true,
|
|
1312
|
+
force: true
|
|
1313
|
+
});
|
|
1314
|
+
} catch {
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
export {
|
|
1320
|
+
codebuddyExtension as default,
|
|
1321
|
+
endpointFor,
|
|
1322
|
+
startupModelsFor,
|
|
1323
|
+
storedCredentials
|
|
1324
|
+
};
|