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/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.136.0";
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 THINKING_LEVEL_MAP = Object.fromEntries([
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: true,
61
- thinkingLevelMap: { ...THINKING_LEVEL_MAP },
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
- async function fetchCodeBuddyModels(apiKey, signal) {
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
- "x-api-key": assertUsableApiKey(apiKey, name, API_KEY_ENV),
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: openAICompletionsApi,
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
- ...source,
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 resolveApiKey = async (provider, profile) => {
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) return undefined;
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 apiKey = await resolveApiKey(PROVIDER, profile);
257
- remoteModels = await fetchCodeBuddyModels(apiKey);
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 apiKey = request.apiKey ?? await resolveApiKey(PROVIDER, profile);
289
- remoteModels = await fetchCodeBuddyModels(apiKey, request.signal);
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.3",
4
- "description": "使用 WorkBuddy API Key DeepSeek Harness 接入 CodeBuddy 模型",
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
  }