dsh-llm-codebuddy 1.3.3 → 1.3.5
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 +189 -132
- package/cli.js +59 -2
- package/client.js +152 -0
- package/codebuddy-auth.js +186 -0
- package/codebuddy-web.js +98 -0
- package/docs//345/217/215/345/220/221/344/273/243/347/220/206/350/260/203/347/224/250WorkBuddy-API/345/274/200/345/217/221/346/226/207/346/241/243.md +576 -0
- package/index.js +151 -22
- package/package.json +14 -4
package/index.js
CHANGED
|
@@ -6,6 +6,15 @@ import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-sett
|
|
|
6
6
|
import { createProvider } from "@earendil-works/pi-ai";
|
|
7
7
|
import * as openAICompletionsApi from "@earendil-works/pi-ai/api/openai-completions";
|
|
8
8
|
import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
|
|
9
|
+
import {
|
|
10
|
+
CODEBUDDY_SESSION_REF,
|
|
11
|
+
parseCodeBuddySession,
|
|
12
|
+
refreshCodeBuddySession,
|
|
13
|
+
serializeCodeBuddySession,
|
|
14
|
+
sessionCacheDeadline,
|
|
15
|
+
sessionNeedsRefresh,
|
|
16
|
+
} from "./codebuddy-auth.js";
|
|
17
|
+
import { installCodeBuddyWeb } from "./codebuddy-web.js";
|
|
9
18
|
|
|
10
19
|
export { Config };
|
|
11
20
|
|
|
@@ -18,14 +27,11 @@ const DISPLAY_NAME = "CodeBuddy 中国区";
|
|
|
18
27
|
const API_KEY_ENV = "CODEBUDDY_API_KEY";
|
|
19
28
|
const BASE_URL = "https://copilot.tencent.com/v2";
|
|
20
29
|
const CONFIG_URL = "https://copilot.tencent.com/v3/config";
|
|
21
|
-
const USER_AGENT = "CLI/unknown CodeBuddy/2.
|
|
30
|
+
const USER_AGENT = "CLI/unknown CodeBuddy/2.137.1";
|
|
22
31
|
const STREAM_IDLE_TIMEOUT_MS = 300_000;
|
|
23
32
|
const NO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
24
33
|
const EFFORTS = ["minimal", "low", "medium", "high", "xhigh", "max"];
|
|
25
|
-
const
|
|
26
|
-
["off", null],
|
|
27
|
-
...EFFORTS.map((effort) => [effort, effort]),
|
|
28
|
-
]);
|
|
34
|
+
const THINKING_LEVELS = ["off", ...EFFORTS];
|
|
29
35
|
const COMPAT = {
|
|
30
36
|
supportsStore: false,
|
|
31
37
|
supportsDeveloperRole: false,
|
|
@@ -34,6 +40,16 @@ const COMPAT = {
|
|
|
34
40
|
thinkingFormat: "openai",
|
|
35
41
|
};
|
|
36
42
|
|
|
43
|
+
function codeBuddyRequestOptions(options) {
|
|
44
|
+
return { ...options, headers: { ...(options?.headers ?? {}), "user-agent": USER_AGENT } };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const codeBuddyApi = {
|
|
48
|
+
...openAICompletionsApi,
|
|
49
|
+
stream: (model, context, options) => openAICompletionsApi.stream(model, context, codeBuddyRequestOptions(options)),
|
|
50
|
+
streamSimple: (model, context, options) => openAICompletionsApi.streamSimple(model, context, codeBuddyRequestOptions(options)),
|
|
51
|
+
};
|
|
52
|
+
|
|
37
53
|
const FALLBACK_MODELS = [
|
|
38
54
|
["hy3", "Hy3", 192000, 64000, true],
|
|
39
55
|
["glm-5.2", "GLM-5.2", 1000000, 48000, false],
|
|
@@ -50,23 +66,60 @@ const FALLBACK_MODELS = [
|
|
|
50
66
|
codeBuddyModel({ id, name: modelName, contextWindow, maxTokens, images }),
|
|
51
67
|
);
|
|
52
68
|
|
|
53
|
-
function codeBuddyModel({ id, name: modelName, contextWindow, maxTokens, images }) {
|
|
69
|
+
function codeBuddyModel({ id, name: modelName, contextWindow, maxTokens, images, reasoning = true, thinkingLevelMap = { off: null }, defaultReasoningEffort, thinkingFormat }) {
|
|
54
70
|
return {
|
|
55
71
|
id,
|
|
56
72
|
name: modelName,
|
|
57
73
|
api: "openai-completions",
|
|
58
74
|
provider: PROVIDER,
|
|
59
75
|
baseUrl: BASE_URL,
|
|
60
|
-
reasoning
|
|
61
|
-
thinkingLevelMap: { ...
|
|
76
|
+
reasoning,
|
|
77
|
+
...(reasoning ? { thinkingLevelMap: { ...thinkingLevelMap } } : {}),
|
|
78
|
+
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
62
79
|
input: images ? ["text", "image"] : ["text"],
|
|
63
80
|
cost: { ...NO_COST },
|
|
64
81
|
contextWindow,
|
|
65
82
|
maxTokens,
|
|
66
|
-
compat: { ...COMPAT },
|
|
83
|
+
compat: { ...COMPAT, ...(thinkingFormat ? { thinkingFormat } : {}) },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function remoteReasoning(raw, fallback) {
|
|
88
|
+
const reasoning = raw.supportsReasoning ?? fallback?.reasoning ?? raw.onlyReasoning === true;
|
|
89
|
+
if (!reasoning) return { reasoning: false };
|
|
90
|
+
const declared = raw.thinkingLevelMap && typeof raw.thinkingLevelMap === "object" ? raw.thinkingLevelMap : undefined;
|
|
91
|
+
const thinkingLevelMap = declared
|
|
92
|
+
? Object.fromEntries(THINKING_LEVELS.map((level) => [level,
|
|
93
|
+
Object.hasOwn(declared, level) && (typeof declared[level] === "string" || declared[level] === null) ? declared[level] : null]))
|
|
94
|
+
: { ...(fallback?.thinkingLevelMap ?? {}), ...(raw.onlyReasoning === true ? { off: null } : {}) };
|
|
95
|
+
const effort = raw.reasoning?.effort;
|
|
96
|
+
const defaultReasoningEffort = EFFORTS.includes(effort) && thinkingLevelMap[effort] !== null ? effort : undefined;
|
|
97
|
+
return {
|
|
98
|
+
reasoning: true,
|
|
99
|
+
thinkingLevelMap,
|
|
100
|
+
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
101
|
+
...(typeof raw.thinkingFormat === "string" ? { thinkingFormat: raw.thinkingFormat } : {}),
|
|
67
102
|
};
|
|
68
103
|
}
|
|
69
104
|
|
|
105
|
+
function configuredReasoning(entry, base) {
|
|
106
|
+
if (entry.reasoningEfforts === false) return { reasoning: false };
|
|
107
|
+
if (!entry.reasoningEfforts || typeof entry.reasoningEfforts !== "object") {
|
|
108
|
+
return base ? {
|
|
109
|
+
reasoning: base.reasoning,
|
|
110
|
+
thinkingLevelMap: base.thinkingLevelMap,
|
|
111
|
+
defaultReasoningEffort: base.defaultReasoningEffort,
|
|
112
|
+
thinkingFormat: base.compat?.thinkingFormat,
|
|
113
|
+
} : { reasoning: false };
|
|
114
|
+
}
|
|
115
|
+
const map = {};
|
|
116
|
+
for (const level of THINKING_LEVELS) {
|
|
117
|
+
if (!Object.hasOwn(entry.reasoningEfforts, level)) map[level] = null;
|
|
118
|
+
else if (!(level === "off" && entry.reasoningEfforts[level] === null)) map[level] = entry.reasoningEfforts[level];
|
|
119
|
+
}
|
|
120
|
+
return { reasoning: true, thinkingLevelMap: map, thinkingFormat: entry.compat?.thinkingFormat };
|
|
121
|
+
}
|
|
122
|
+
|
|
70
123
|
function positiveInteger(...values) {
|
|
71
124
|
return values.find((value) => Number.isSafeInteger(value) && value > 0);
|
|
72
125
|
}
|
|
@@ -94,17 +147,23 @@ function modelsFromConfig(data) {
|
|
|
94
147
|
contextWindow,
|
|
95
148
|
maxTokens,
|
|
96
149
|
images: raw.supportsImages === true || fallback?.input.includes("image") === true,
|
|
150
|
+
...remoteReasoning(raw, fallback),
|
|
97
151
|
})];
|
|
98
152
|
});
|
|
99
153
|
}
|
|
100
154
|
|
|
101
|
-
|
|
155
|
+
function authenticationHeaders(credential) {
|
|
156
|
+
const value = assertUsableApiKey(credential.value, name, credential.ref ?? API_KEY_ENV);
|
|
157
|
+
return credential.kind === "bearer" ? { authorization: `Bearer ${value}` } : { "x-api-key": value };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function fetchCodeBuddyModels(credential, signal) {
|
|
102
161
|
let response;
|
|
103
162
|
try {
|
|
104
163
|
response = await fetch(CONFIG_URL, {
|
|
105
164
|
headers: {
|
|
106
165
|
accept: "application/json",
|
|
107
|
-
|
|
166
|
+
...authenticationHeaders(credential),
|
|
108
167
|
"user-agent": USER_AGENT,
|
|
109
168
|
"x-product": "SaaS",
|
|
110
169
|
},
|
|
@@ -129,7 +188,7 @@ function codeBuddyProvider(models, auth) {
|
|
|
129
188
|
baseUrl: BASE_URL,
|
|
130
189
|
auth,
|
|
131
190
|
models,
|
|
132
|
-
api:
|
|
191
|
+
api: codeBuddyApi,
|
|
133
192
|
});
|
|
134
193
|
}
|
|
135
194
|
|
|
@@ -137,6 +196,7 @@ function resolvedProfile(provider, source, piProvider, configuredMaxTokens = new
|
|
|
137
196
|
const apiKeyEnv = source.apiKeyEnv === undefined ? undefined : credentialRef(source.apiKeyEnv);
|
|
138
197
|
return {
|
|
139
198
|
...source,
|
|
199
|
+
headers: runtimeHeaders(source.headers),
|
|
140
200
|
provider,
|
|
141
201
|
displayName: source.displayName ?? piProvider.name ?? provider,
|
|
142
202
|
...(apiKeyEnv === undefined ? {} : { apiKeyEnv }),
|
|
@@ -169,23 +229,42 @@ function selectCodeBuddyModels(base, entries) {
|
|
|
169
229
|
const byId = new Map(base.map((model) => [model.id, model]));
|
|
170
230
|
return entries.map((entry) => {
|
|
171
231
|
const model = byId.get(entry.id);
|
|
232
|
+
const reasoning = configuredReasoning(entry, model);
|
|
172
233
|
return codeBuddyModel({
|
|
173
234
|
id: entry.id,
|
|
174
235
|
name: entry.name ?? model?.name ?? entry.id,
|
|
175
236
|
contextWindow: entry.contextWindow ?? model?.contextWindow ?? 262144,
|
|
176
237
|
maxTokens: entry.maxTokens ?? model?.maxTokens ?? 32768,
|
|
177
238
|
images: entry.input?.includes("image") ?? model?.input.includes("image") ?? false,
|
|
239
|
+
...reasoning,
|
|
178
240
|
});
|
|
179
241
|
});
|
|
180
242
|
}
|
|
181
243
|
|
|
244
|
+
function ownsProvider(provider, builtins) {
|
|
245
|
+
return provider === PROVIDER || builtins.has(provider);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function runtimeHeaders(headers) {
|
|
249
|
+
return { ...(headers ?? {}) };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function codeBuddySource(config, source) {
|
|
253
|
+
return Object.hasOwn(config?.providers ?? {}, PROVIDER) ? source : { ...source, apiKeyEnv: source.apiKeyEnv ?? API_KEY_ENV };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export const __testing = Object.freeze({ authenticationHeaders, codeBuddyRequestOptions, codeBuddySource, modelsFromConfig, ownsProvider, runtimeHeaders, selectCodeBuddyModels });
|
|
257
|
+
|
|
182
258
|
export function apply(ctx, config) {
|
|
259
|
+
installCodeBuddyWeb(ctx);
|
|
183
260
|
let current = () => config;
|
|
184
261
|
let remoteModels;
|
|
185
262
|
let generation = 0;
|
|
186
263
|
let memoRaw;
|
|
187
264
|
let memoGeneration = -1;
|
|
188
265
|
let memoized;
|
|
266
|
+
let loginSession;
|
|
267
|
+
let loginSessionPromise;
|
|
189
268
|
const builtins = new Map(builtinProviders().map((provider) => [provider.id, provider]));
|
|
190
269
|
const apiKeyAuth = builtins.get("deepseek")?.auth;
|
|
191
270
|
if (!apiKeyAuth) throw new Error(`${name}: pi-ai DeepSeek auth helper is unavailable`);
|
|
@@ -206,20 +285,20 @@ export function apply(ctx, config) {
|
|
|
206
285
|
if (memoRaw === current() && memoGeneration === generation && memoized) return memoized;
|
|
207
286
|
const result = new Map();
|
|
208
287
|
for (const [provider, source] of Object.entries(raw.providers)) {
|
|
288
|
+
if (!ownsProvider(provider, builtins)) continue;
|
|
209
289
|
if (provider === PROVIDER) {
|
|
290
|
+
const sourceWithAuth = codeBuddySource(current(), source);
|
|
210
291
|
const models = selectCodeBuddyModels(remoteModels ?? FALLBACK_MODELS, source.models);
|
|
211
292
|
const configured = new Map((source.models ?? []).flatMap((model) =>
|
|
212
293
|
Number.isSafeInteger(model.maxTokens) && model.maxTokens > 0 ? [[model.id, model.maxTokens]] : [],
|
|
213
294
|
));
|
|
214
295
|
result.set(provider, resolvedProfile(provider, {
|
|
215
|
-
...
|
|
216
|
-
apiKeyEnv: source.apiKeyEnv ?? API_KEY_ENV,
|
|
296
|
+
...sourceWithAuth,
|
|
217
297
|
displayName: DISPLAY_NAME,
|
|
218
298
|
}, codeBuddyProvider(models, apiKeyAuth), configured));
|
|
219
299
|
continue;
|
|
220
300
|
}
|
|
221
301
|
const base = builtins.get(provider);
|
|
222
|
-
if (!base) throw new Error(`${name}: 不支持非内置 Provider "${provider}"`);
|
|
223
302
|
const selected = selectBuiltinModels(base, source.models);
|
|
224
303
|
const configured = new Map((source.models ?? []).flatMap((model) =>
|
|
225
304
|
Number.isSafeInteger(model.maxTokens) && model.maxTokens > 0 ? [[model.id, model.maxTokens]] : [],
|
|
@@ -232,20 +311,68 @@ export function apply(ctx, config) {
|
|
|
232
311
|
return result;
|
|
233
312
|
};
|
|
234
313
|
|
|
235
|
-
const
|
|
314
|
+
const resolveLoginSession = async () => {
|
|
315
|
+
if (loginSession?.expiresAt > Date.now()) return loginSession;
|
|
316
|
+
loginSessionPromise ??= (async () => {
|
|
317
|
+
const credentials = ctx.get("credentials");
|
|
318
|
+
const ref = credentialRef(CODEBUDDY_SESSION_REF);
|
|
319
|
+
const stored = await credentials?.resolve(ref);
|
|
320
|
+
const value = stored?.value ?? launchEnvironmentOf(ctx).get(ref)?.value;
|
|
321
|
+
if (!value) throw new Error("未找到 CodeBuddy 登录凭据");
|
|
322
|
+
let session = parseCodeBuddySession(value);
|
|
323
|
+
if (sessionNeedsRefresh(session)) {
|
|
324
|
+
session = await refreshCodeBuddySession(session);
|
|
325
|
+
await credentials?.set(ref, serializeCodeBuddySession(session));
|
|
326
|
+
}
|
|
327
|
+
return { ...session, expiresAt: sessionCacheDeadline(session) };
|
|
328
|
+
})().finally(() => {
|
|
329
|
+
loginSessionPromise = undefined;
|
|
330
|
+
});
|
|
331
|
+
loginSession = await loginSessionPromise;
|
|
332
|
+
return loginSession;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const resolveCredential = async (provider, profile) => {
|
|
236
336
|
const ref = profile.apiKeyEnv;
|
|
237
|
-
if (!ref)
|
|
337
|
+
if (!ref && provider === PROVIDER) {
|
|
338
|
+
let session;
|
|
339
|
+
try {
|
|
340
|
+
session = await resolveLoginSession();
|
|
341
|
+
} catch (error) {
|
|
342
|
+
throw new LlmError(`${name}: 未找到可用的 CodeBuddy 登录令牌,请运行 dsh-llm-codebuddy login`, "MISSING_CREDENTIAL", { cause: error });
|
|
343
|
+
}
|
|
344
|
+
profile.headers ??= {};
|
|
345
|
+
if (session.account.userId) profile.headers["X-User-Id"] = session.account.userId;
|
|
346
|
+
if (session.account.enterpriseId) {
|
|
347
|
+
profile.headers["X-Enterprise-Id"] = session.account.enterpriseId;
|
|
348
|
+
profile.headers["X-Tenant-Id"] = session.account.enterpriseId;
|
|
349
|
+
}
|
|
350
|
+
if (session.auth.domain) profile.headers["X-Domain"] = session.auth.domain;
|
|
351
|
+
return { value: assertUsableApiKey(session.auth.accessToken, name, "CodeBuddy login session"), kind: "bearer" };
|
|
352
|
+
}
|
|
353
|
+
if (!ref) return { value: undefined, kind: "none" };
|
|
238
354
|
const stored = await ctx.get("credentials")?.resolve(ref);
|
|
239
355
|
const value = stored?.value ?? launchEnvironmentOf(ctx).get(ref)?.value;
|
|
240
|
-
if (value) return assertUsableApiKey(value, name, ref);
|
|
356
|
+
if (value) return { value: assertUsableApiKey(value, name, ref), kind: "api-key", ref };
|
|
241
357
|
throw new LlmError(`${name}: Provider "${provider}" 缺少 API Key,请在 WebUI 的模型设置中填写`, "MISSING_CREDENTIAL");
|
|
242
358
|
};
|
|
243
359
|
|
|
360
|
+
const resolveApiKey = async (provider, profile) => (await resolveCredential(provider, profile)).value;
|
|
361
|
+
|
|
244
362
|
const adapter = new PiAiAdapter({
|
|
245
363
|
profiles,
|
|
246
364
|
resolveApiKey,
|
|
247
365
|
resolveAttachments: () => ctx.get("attachments"),
|
|
248
366
|
});
|
|
367
|
+
const resolveModel = adapter.resolveModel.bind(adapter);
|
|
368
|
+
adapter.resolveModel = async (provider, model, signal) => {
|
|
369
|
+
const resolved = await resolveModel(provider, model, signal);
|
|
370
|
+
if (provider !== PROVIDER || !resolved.reasoning) return resolved;
|
|
371
|
+
const configured = profiles().get(PROVIDER)?.piProvider.getModels().find((entry) => entry.id === model);
|
|
372
|
+
const effort = configured?.defaultReasoningEffort;
|
|
373
|
+
if (!effort || !resolved.reasoning.efforts.some((entry) => entry.id === effort)) return resolved;
|
|
374
|
+
return { ...resolved, reasoning: { ...resolved.reasoning, defaultEffort: effort } };
|
|
375
|
+
};
|
|
249
376
|
const listModels = adapter.listModels.bind(adapter);
|
|
250
377
|
let refreshPromise;
|
|
251
378
|
adapter.listModels = async (provider) => {
|
|
@@ -253,8 +380,8 @@ export function apply(ctx, config) {
|
|
|
253
380
|
refreshPromise ??= (async () => {
|
|
254
381
|
try {
|
|
255
382
|
const profile = profiles().get(PROVIDER);
|
|
256
|
-
const
|
|
257
|
-
remoteModels = await fetchCodeBuddyModels(
|
|
383
|
+
const credential = await resolveCredential(PROVIDER, profile);
|
|
384
|
+
remoteModels = await fetchCodeBuddyModels(credential);
|
|
258
385
|
generation += 1;
|
|
259
386
|
} catch {
|
|
260
387
|
// Keep the built-in catalog available while the key or network is absent.
|
|
@@ -285,8 +412,10 @@ export function apply(ctx, config) {
|
|
|
285
412
|
ctx.llm.registerModelDiscovery(NS, async (request) => {
|
|
286
413
|
if (request.provider === PROVIDER) {
|
|
287
414
|
const profile = profiles().get(PROVIDER);
|
|
288
|
-
const
|
|
289
|
-
|
|
415
|
+
const credential = request.apiKey
|
|
416
|
+
? { value: request.apiKey, kind: "api-key", ref: API_KEY_ENV }
|
|
417
|
+
: await resolveCredential(PROVIDER, profile);
|
|
418
|
+
remoteModels = await fetchCodeBuddyModels(credential, request.signal);
|
|
290
419
|
generation += 1;
|
|
291
420
|
return remoteModels.map((model) => ({
|
|
292
421
|
id: model.id,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-llm-codebuddy",
|
|
3
|
-
"version": "1.3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.3.5",
|
|
4
|
+
"description": "通过 WorkBuddy API Key 或 CodeBuddy 登录令牌为 DeepSeek Harness 接入 CodeBuddy 模型",
|
|
5
5
|
"author": "Axiaohungry",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"deepseek-harness",
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
"type": "module",
|
|
15
15
|
"main": "index.js",
|
|
16
16
|
"exports": {
|
|
17
|
-
".": "./index.js"
|
|
17
|
+
".": "./index.js",
|
|
18
|
+
"./client": "./client.js",
|
|
19
|
+
"./package.json": "./package.json"
|
|
18
20
|
},
|
|
19
21
|
"bin": {
|
|
20
22
|
"dsh-llm-codebuddy": "cli.js"
|
|
@@ -29,15 +31,23 @@
|
|
|
29
31
|
},
|
|
30
32
|
"files": [
|
|
31
33
|
"index.js",
|
|
34
|
+
"codebuddy-auth.js",
|
|
35
|
+
"codebuddy-web.js",
|
|
36
|
+
"client.js",
|
|
32
37
|
"cli.js",
|
|
33
38
|
"cordis.patch.yml",
|
|
39
|
+
"docs",
|
|
34
40
|
"README.md",
|
|
35
41
|
"LICENSE"
|
|
36
42
|
],
|
|
37
43
|
"scripts": {
|
|
38
|
-
"check": "node --check index.js && node --check cli.js && node cli.js --self-test"
|
|
44
|
+
"check": "node --check index.js && node --check codebuddy-auth.js && node --check codebuddy-web.js && node --check client.js && node --check cli.js && node --test test.js && node cli.js --self-test"
|
|
39
45
|
},
|
|
40
46
|
"dsh": {
|
|
47
|
+
"client": {
|
|
48
|
+
"inject": [],
|
|
49
|
+
"platform": "web"
|
|
50
|
+
},
|
|
41
51
|
"bundle": {
|
|
42
52
|
"patch": "./cordis.patch.yml"
|
|
43
53
|
}
|