import { AsyncLocalStorage } from "node:async_hooks"; import { credentialRef } from "@deepseek-ai/dsh-credentials"; import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment"; import { LlmError, assertUsableApiKey, resolveRetryPolicy } from "@deepseek-ai/dsh-llm"; import { Config, PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai"; import * as dshSettings from "@deepseek-ai/dsh-settings"; import { createProvider } from "@earendil-works/pi-ai"; import * as openAICompletionsApi from "@earendil-works/pi-ai/api/openai-completions"; import * as openAIResponsesApi from "@earendil-works/pi-ai/api/openai-responses"; import * as anthropicMessagesApi from "@earendil-works/pi-ai/api/anthropic-messages"; import { builtinProviders } from "@earendil-works/pi-ai/providers/all"; import { WORKBUDDY_SESSION_REF, WORKBUDDY_SESSIONS_REF, WORKBUDDY_SESSION_ROUTING_REF, LEGACY_SESSION_REF, LEGACY_SESSIONS_REF, activeWorkBuddySession, createWorkBuddySessionStore, createWorkBuddySessionRoutingState, parseWorkBuddySession, parseWorkBuddySessions, parseWorkBuddySessionRouting, refreshWorkBuddySession, serializeWorkBuddySession, serializeWorkBuddySessionRouting, serializeWorkBuddySessions, sessionCacheDeadline, sessionNeedsRefresh, upsertWorkBuddySession, } from "./workbuddy-auth.js"; import { installWorkBuddyWeb } from "./workbuddy-web.js"; import { probeEndpoint } from "./workbuddy-discovery.js"; export { Config }; export const name = "llm-workbuddy"; export const inject = ["llm"]; const NS = typeof dshSettings.settingsNamespace === "function" ? dshSettings.settingsNamespace("llm-pi-ai") : "llm-pi-ai"; const PROVIDER = "workbuddy-cn"; const LEGACY_PROVIDER = "codebuddy-cn"; const WORKBUDDY_PROVIDERS = new Set([PROVIDER, LEGACY_PROVIDER]); const WORKBUDDY_PROVIDER_PATTERN = /(?:^|-)(?:work-?buddy|code-?buddy)(?:-|$)/; const DISPLAY_NAME = "WorkBuddy"; const API_KEY_ENV = "WORKBUDDY_API_KEY"; const LEGACY_API_KEY_ENV = "CODEBUDDY_API_KEY"; const BASE_URL = "https://copilot.tencent.com/v2"; const CONFIG_URL = "https://copilot.tencent.com/v3/config"; const USER_AGENT = "CLI/unknown CodeBuddy/2.137.1"; const STREAM_IDLE_TIMEOUT_MS = 300_000; const traceContext = new AsyncLocalStorage(); let nextTraceId = 0; const NO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }; const EFFORTS = ["minimal", "low", "medium", "high", "xhigh", "max"]; const THINKING_LEVELS = ["off", ...EFFORTS]; const COMPAT = { supportsStore: false, supportsDeveloperRole: false, supportsReasoningEffort: true, maxTokensField: "max_tokens", thinkingFormat: "openai", }; function workBuddyRequestOptions(options) { const trace = traceContext.getStore(); const fetchImpl = options?.fetch ?? globalThis.fetch; return { ...options, timeoutMs: options?.timeoutMs ?? STREAM_IDLE_TIMEOUT_MS, headers: { ...(options?.headers ?? {}), "user-agent": USER_AGENT }, ...(trace ? { fetch: async (input, init) => { const started = Date.now(); traceEvent(trace, "http.start"); try { const response = await fetchImpl(input, init); traceEvent(trace, "http.headers", { elapsedMs: Date.now() - started, status: response.status }); return response; } catch (error) { traceEvent(trace, "http.error", { elapsedMs: Date.now() - started, ...errorFields(error) }); throw error; } } } : {}), }; } function errorFields(error) { const safe = (value) => typeof value === "string" && /^[A-Za-z_][A-Za-z_0-9-]{0,39}$/.test(value) ? value : undefined; return { ...(safe(error?.name) ? { errorName: safe(error.name) } : {}), ...(safe(error?.code) ? { errorCode: safe(error.code) } : {}), ...(Number.isInteger(error?.status) ? { status: error.status } : {}), }; } function traceEvent(trace, stage, details = {}) { if (!trace) return; console.info("[dsh-llm-workbuddy]", JSON.stringify({ request: trace.id, stage, ...details })); } function observedWorkBuddyStream(model, context, options) { const trace = traceContext.getStore(); const started = Date.now(); const stream = openAICompletionsApi.streamSimple(model, context, workBuddyRequestOptions(options)); return (async function* () { traceEvent(trace, "model.start"); let first = true; let completed = false; let failed = false; try { for await (const chunk of stream) { if (first) { first = false; traceEvent(trace, "model.first-chunk", { elapsedMs: Date.now() - started }); } yield chunk; } completed = true; traceEvent(trace, "model.done", { elapsedMs: Date.now() - started }); } catch (error) { failed = true; traceEvent(trace, "model.error", { elapsedMs: Date.now() - started, ...errorFields(error) }); throw error; } finally { if (!completed && !failed) traceEvent(trace, "model.cancelled", { elapsedMs: Date.now() - started }); } })(); } const workBuddyApi = { ...openAICompletionsApi, stream: (model, context, options) => openAICompletionsApi.stream(model, context, workBuddyRequestOptions(options)), streamSimple: observedWorkBuddyStream, }; const FALLBACK_MODELS = [ ["hy3", "Hy3", 192000, 64000, true], ["glm-5.2", "GLM-5.2", 1000000, 48000, false], ["glm-5.1", "GLM-5.1", 200000, 48000, false], ["glm-5v-turbo", "GLM-5v-Turbo", 200000, 64000, true], ["minimax-m3-pay", "MiniMax-M3", 512000, 128000, true], ["minimax-m2.7", "MiniMax-M2.7", 200000, 48000, true], ["kimi-k3-2", "Kimi-K3", 1000000, 32000, true], ["kimi-k2.7", "Kimi-K2.7-Code", 256000, 32000, true], ["kimi-k2.6", "Kimi-K2.6", 256000, 32000, true], ["deepseek-v4-pro", "DeepSeek V4 Pro", 1000000, 50000, true], ["deepseek-v4-flash", "DeepSeek V4 Flash", 1000000, 50000, true], ].map(([id, modelName, contextWindow, maxTokens, images]) => workBuddyModel({ id, name: modelName, contextWindow, maxTokens, images }), ); function workBuddyModel({ provider = PROVIDER, id, name: modelName, contextWindow, maxTokens, images, reasoning = true, thinkingLevelMap = { off: null }, defaultReasoningEffort, thinkingFormat }) { return { id, name: modelName, api: "openai-completions", provider, baseUrl: BASE_URL, reasoning, ...(reasoning ? { thinkingLevelMap: { ...thinkingLevelMap } } : {}), ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}), input: images ? ["text", "image"] : ["text"], cost: { ...NO_COST }, contextWindow, maxTokens, compat: { ...COMPAT, ...(thinkingFormat ? { thinkingFormat } : {}) }, }; } function remoteReasoning(raw, fallback) { const reasoning = raw.supportsReasoning ?? fallback?.reasoning ?? raw.onlyReasoning === true; if (!reasoning) return { reasoning: false }; const declared = raw.thinkingLevelMap && typeof raw.thinkingLevelMap === "object" ? raw.thinkingLevelMap : undefined; const thinkingLevelMap = declared ? Object.fromEntries(THINKING_LEVELS.map((level) => [level, Object.hasOwn(declared, level) && (typeof declared[level] === "string" || declared[level] === null) ? declared[level] : null])) : { ...(fallback?.thinkingLevelMap ?? {}), ...(raw.onlyReasoning === true ? { off: null } : {}) }; const effort = raw.reasoning?.effort; const defaultReasoningEffort = EFFORTS.includes(effort) && thinkingLevelMap[effort] !== null ? effort : undefined; return { reasoning: true, thinkingLevelMap, ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}), ...(typeof raw.thinkingFormat === "string" ? { thinkingFormat: raw.thinkingFormat } : {}), }; } function configuredReasoning(entry, base) { if (entry.reasoningEfforts === false) return { reasoning: false }; if (!entry.reasoningEfforts || typeof entry.reasoningEfforts !== "object") { return base ? { reasoning: base.reasoning, thinkingLevelMap: base.thinkingLevelMap, defaultReasoningEffort: base.defaultReasoningEffort, thinkingFormat: base.compat?.thinkingFormat, } : { reasoning: false }; } const map = {}; for (const level of THINKING_LEVELS) { if (!Object.hasOwn(entry.reasoningEfforts, level)) map[level] = null; else if (!(level === "off" && entry.reasoningEfforts[level] === null)) map[level] = entry.reasoningEfforts[level]; } return { reasoning: true, thinkingLevelMap: map, thinkingFormat: entry.compat?.thinkingFormat }; } function positiveInteger(...values) { return values.find((value) => Number.isSafeInteger(value) && value > 0); } function text(...values) { return values.find((value) => typeof value === "string" && value.length > 0); } function modelsFromConfig(data) { const agents = Array.isArray(data?.agents) ? data.agents : data?.agent?.agents; const cli = Array.isArray(agents) ? agents.find((agent) => agent?.name === "cli") : undefined; const allowed = Array.isArray(cli?.models) ? cli.models : []; const source = Array.isArray(data?.models) ? data.models : []; const byId = new Map(source.map((model) => [model?.id, model])); return allowed.flatMap((id) => { const raw = byId.get(id); if (!raw) return []; const fallback = FALLBACK_MODELS.find((model) => model.id === id); const contextWindow = positiveInteger(raw.maxInputTokens, raw.maxAllowedSize, fallback?.contextWindow); const maxTokens = positiveInteger(raw.maxOutputTokens, fallback?.maxTokens); if (!contextWindow || !maxTokens) return []; return [workBuddyModel({ id, name: text(raw.name, fallback?.name, id), contextWindow, maxTokens, images: raw.supportsImages === true || fallback?.input.includes("image") === true, ...remoteReasoning(raw, fallback), })]; }); } function authenticationHeaders(credential) { const value = assertUsableApiKey(credential.value, name, credential.ref ?? API_KEY_ENV); return credential.kind === "bearer" ? { authorization: `Bearer ${value}` } : { "x-api-key": value }; } async function fetchWorkBuddyModels(credential, signal) { let response; try { response = await fetch(CONFIG_URL, { headers: { accept: "application/json", ...authenticationHeaders(credential), "user-agent": USER_AGENT, "x-product": "SaaS", }, signal, }); } catch (error) { if (signal?.aborted) throw new LlmError("WorkBuddy 模型列表获取已取消", "ABORTED", { cause: error }); throw new LlmError("无法连接 WorkBuddy 模型配置接口", "DISCOVERY_FAILED", { cause: error }); } if (!response.ok) throw new LlmError(`WorkBuddy 模型配置接口返回 ${response.status}`, "DISCOVERY_FAILED"); const body = await response.json(); if (body?.code !== 0) throw new LlmError(`WorkBuddy 模型配置接口错误:${body?.msg ?? body?.code}`, "DISCOVERY_FAILED"); const models = modelsFromConfig(body.data); if (models.length === 0) throw new LlmError("WorkBuddy 没有返回 CLI 可用模型", "DISCOVERY_FAILED"); return models; } /** * WorkBuddy's credential is already resolved by the DSH adapter. Do not * reuse pi-ai's DeepSeek envApiKeyAuth here: newer pi-ai releases require a * signal argument while older DSH adapters call auth resolvers without one. * This small adapter accepts both contracts and keeps bearer/API-key values * opaque to the provider implementation. */ function workBuddyApiKeyAuth() { return { name: `${DISPLAY_NAME} API Key`, login: async (interaction) => { const signal = interaction?.signal; signal?.throwIfAborted?.(); const key = await interaction.prompt({ type: "secret", message: `Enter ${DISPLAY_NAME} API Key` }); signal?.throwIfAborted?.(); return { type: "api_key", key }; }, resolve: async ({ credential, signal } = {}) => { signal?.throwIfAborted?.(); if (!credential?.key) return undefined; return { auth: { apiKey: credential.key }, ...(credential.env ? { env: credential.env } : {}), source: "DSH credential", }; }, }; } function workBuddyProvider(models, provider = PROVIDER) { return createProvider({ id: provider, name: DISPLAY_NAME, baseUrl: BASE_URL, auth: { apiKey: workBuddyApiKeyAuth() }, models: models.map((model) => ({ ...model, provider })), api: workBuddyApi, }); } const GENERIC_APIS = Object.freeze({ "openai-completions": openAICompletionsApi, "openai-responses": openAIResponsesApi, "anthropic-messages": anthropicMessagesApi, }); const GENERIC_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"]; function genericApiKeyAuth(provider) { return { name: `${provider} API Key`, resolve: async ({ credential, signal } = {}) => { signal?.throwIfAborted?.(); if (!credential?.key) return undefined; return { auth: { apiKey: credential.key }, source: "DSH credential" }; }, }; } function genericModel(provider, source, entry) { const reasoningEfforts = entry.reasoningEfforts; const reasoning = reasoningEfforts !== false && reasoningEfforts && typeof reasoningEfforts === "object"; const thinkingLevelMap = reasoning ? Object.fromEntries(GENERIC_LEVELS.filter((level) => Object.hasOwn(reasoningEfforts, level)).map((level) => [level, reasoningEfforts[level]])) : undefined; return { id: entry.id, name: entry.name ?? entry.id, api: source.api, provider, baseUrl: source.baseURL, input: Array.isArray(entry.input) && entry.input.length > 0 ? [...entry.input] : [...source.defaultInput ?? ["text"]], cost: { ...NO_COST }, contextWindow: entry.contextWindow ?? source.defaultContextWindow ?? 262144, maxTokens: entry.maxTokens ?? source.defaultMaxTokens ?? 32768, ...(reasoning ? { reasoning: true, thinkingLevelMap } : {}), ...(entry.compat ?? source.compat ? { compat: { ...(source.compat ?? {}), ...(entry.compat ?? {}) } } : {}), }; } function genericProvider(provider, source = {}) { const api = GENERIC_APIS[source.api]; if (!api || !source.baseURL || !Array.isArray(source.models) || source.models.length === 0) return undefined; return createProvider({ id: provider, name: source.displayName ?? provider, baseUrl: source.baseURL, headers: source.headers, auth: { apiKey: genericApiKeyAuth(provider) }, models: source.models.map((entry) => genericModel(provider, source, entry)), api, }); } function resolvedProfile(provider, source, piProvider, configuredMaxTokens = new Map(), requestContext) { const apiKeyEnv = source.apiKeyEnv === undefined ? undefined : credentialRef(source.apiKeyEnv); return { ...source, headers: runtimeHeaders(source.headers, requestContext), provider, displayName: source.displayName ?? piProvider.name ?? provider, // dsh-llm-pi-ai reads this map for every exact model during catalog // resolution. WorkBuddy profiles have no per-model validation failures // here, but must still provide the empty map for the shared adapter API. modelErrors: new Map(), ...(apiKeyEnv === undefined ? {} : { apiKeyEnv }), streamIdleTimeoutMs: source.streamIdleTimeoutMs ?? STREAM_IDLE_TIMEOUT_MS, retryPolicy: resolveRetryPolicy(source.retryPolicy, `${name}: provider "${provider}" retryPolicy`), configuredMaxTokens, piProvider, }; } function selectBuiltinModels(base, entries) { if (!Array.isArray(entries) || entries.length === 0) return base; const byId = new Map(base.getModels().map((model) => [model.id, model])); const selected = entries.flatMap((entry) => { const model = byId.get(entry.id); if (!model) return []; return [{ ...model, ...(entry.name ? { name: entry.name } : {}), ...(entry.contextWindow ? { contextWindow: entry.contextWindow } : {}), ...(entry.maxTokens ? { maxTokens: entry.maxTokens } : {}), ...(Array.isArray(entry.input) && entry.input.length ? { input: [...entry.input] } : {}), }]; }); return { ...base, getModels: () => selected }; } function selectWorkBuddyModels(base, entries) { if (!Array.isArray(entries) || entries.length === 0) return base; const byId = new Map(base.map((model) => [model.id, model])); return entries.map((entry) => { const model = byId.get(entry.id); const reasoning = configuredReasoning(entry, model); return workBuddyModel({ id: entry.id, name: entry.name ?? model?.name ?? entry.id, contextWindow: entry.contextWindow ?? model?.contextWindow ?? 262144, maxTokens: entry.maxTokens ?? model?.maxTokens ?? 32768, images: entry.input?.includes("image") ?? model?.input.includes("image") ?? false, ...reasoning, }); }); } function ownsProvider(provider, builtins, source) { return WORKBUDDY_PROVIDERS.has(provider) || builtins.has(provider) || genericProvider(provider, source) !== undefined; } function runtimeHeaders(headers, requestContext) { const base = { ...(headers ?? {}) }; if (!requestContext) return base; return new Proxy(base, { ownKeys(target) { const extra = requestContext.getStore()?.headers ?? {}; return [...new Set([...Reflect.ownKeys(target), ...Reflect.ownKeys(extra)])]; }, getOwnPropertyDescriptor(target, property) { const extra = requestContext.getStore()?.headers ?? {}; if (!Reflect.has(target, property) && !Reflect.has(extra, property)) return undefined; return { configurable: true, enumerable: true, writable: true, value: this.get(target, property) }; }, get(target, property, receiver) { const extra = requestContext.getStore()?.headers ?? {}; return Reflect.has(extra, property) ? extra[property] : Reflect.get(target, property, receiver); }, }); } function sessionBindingFor(routing, sessionId) { if (!routing?.enabled) return undefined; const id = typeof sessionId === "string" && sessionId.trim() ? sessionId.trim() : undefined; return id && Object.hasOwn(routing.bindings, id) ? routing.bindings[id] : undefined; } function normalizedProviderName(value) { return String(value ?? "") .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); } function isWorkBuddyProviderName(value) { const normalized = normalizedProviderName(value); return normalized.length > 0 && WORKBUDDY_PROVIDER_PATTERN.test(normalized); } function directWorkBuddyProvider(value) { const normalized = normalizedProviderName(value); if (normalized === PROVIDER || normalized === LEGACY_PROVIDER) return normalized; return undefined; } function interruptedToolTailAssistantIndex(messages) { let tailIndex = messages.length - 1; while (tailIndex >= 0 && messages[tailIndex]?.role === "system") tailIndex -= 1; if (tailIndex < 0) return -1; const tail = messages[tailIndex]; if (tail?.role !== "user" || !Array.isArray(tail.content)) return -1; if (!tail.content.some((block) => block?.type === "tool-result" && block.isError === true)) return -1; for (let index = tailIndex - 1; index >= 0; index -= 1) { const message = messages[index]; if (message?.role !== "assistant") continue; return Array.isArray(message.content) && message.content.some((block) => block?.type === "tool-call") ? index : -1; } return -1; } /** * Make WorkBuddy replay metadata safe across direct and wrapped provider ids. * The returned messages are request-only copies; durable session history is * never rewritten. An interrupted tool result deliberately loses only the * preceding assistant replayState so the model receives ordinary history. */ function normalizeWorkBuddyReplay(options) { if (!Array.isArray(options?.messages)) return options; const currentProvider = options.provider; if (!isWorkBuddyProviderName(currentProvider)) return options; const interruptedIndex = interruptedToolTailAssistantIndex(options.messages); let changed = false; const messages = options.messages.map((message, index) => { const source = message?.source; const state = source?.replayState; if (message?.role !== "assistant" || !source || state?.kind !== "pi-ai" || state?.version !== 1) return message; let nextSource = source; const sourceProvider = source.provider; const replayProvider = state.provider; if (isWorkBuddyProviderName(sourceProvider) && isWorkBuddyProviderName(replayProvider)) { const canonical = directWorkBuddyProvider(replayProvider) ?? directWorkBuddyProvider(sourceProvider) ?? directWorkBuddyProvider(currentProvider); if (canonical && (sourceProvider !== canonical || replayProvider !== canonical)) { nextSource = { ...nextSource, provider: canonical, replayState: { ...state, provider: canonical }, }; } } if (index === interruptedIndex && nextSource.replayState !== undefined) { const { replayState: _ignored, ...withoutReplay } = nextSource; nextSource = withoutReplay; } if (nextSource === source) return message; changed = true; return { ...message, source: nextSource }; }); return changed ? { ...options, messages } : options; } // The rc.6 pi-ai adapter rejects replay metadata it does not understand. A // newer DSH may persist a v2 envelope, so let old adapters use the durable // message content as provider-neutral history instead of failing the request. function stripUnsupportedReplay(options) { if (!Array.isArray(options?.messages)) return options; let changed = false; const messages = options.messages.map((message) => { const source = message?.source; const state = source?.replayState; if (state === undefined || (state?.kind === "pi-ai" && state?.version === 1)) return message; changed = true; const { replayState: _ignored, ...sourceWithoutReplay } = source; return { ...message, source: sourceWithoutReplay }; }); return changed ? { ...options, messages } : options; } function prepareWorkBuddyOptions(options, legacyReplay = true) { const normalized = normalizeWorkBuddyReplay(options); return legacyReplay ? stripUnsupportedReplay(normalized) : normalized; } function workBuddySource(config, source) { const providers = providerSettings(config); return Object.hasOwn(providers, PROVIDER) || Object.hasOwn(providers, LEGACY_PROVIDER) ? source : { ...source, apiKeyEnv: source.apiKeyEnv ?? API_KEY_ENV }; } function providerSettings(config) { return typeof config?.providers?.get === "function" ? config.providers.get() : config?.providers ?? {}; } function installSettingsCompat(ctx, ns, schema, entry, hooks) { if (typeof entry?.providers?.get === "function") { return ctx.inject(["settings"], (child) => { child.effect(() => child.settings.configure({ auto: false }, ctx.fiber)); }); } if (typeof dshSettings.installSettingsSection === "function") { return dshSettings.installSettingsSection(ctx, ns, schema, entry, hooks); } return ctx.inject(["settings"], (settingsCtx) => { if (!settingsCtx.settings || typeof settingsCtx.settings.installSection !== "function") { throw new Error(`${name}: DSH settings service does not provide installSection`); } return settingsCtx.settings.installSection(ctx, ns, schema, entry, hooks); }); } export const __testing = Object.freeze({ authenticationHeaders, workBuddyApiKeyAuth, workBuddyRequestOptions, workBuddySource, providerSettings, genericProvider, modelsFromConfig, ownsProvider, runtimeHeaders, stripUnsupportedReplay, normalizeWorkBuddyReplay, prepareWorkBuddyOptions, selectWorkBuddyModels, sessionBindingFor }); export function apply(ctx, config) { const modernSettings = typeof config?.providers?.get === "function"; const settingsNs = modernSettings ? ctx.fiber?.entry?.options?.id ?? name : NS; installWorkBuddyWeb(ctx, settingsNs); let current = () => config; const requestContext = new AsyncLocalStorage(); let remoteModels; let generation = 0; let memoRaw; let memoProviders; let memoGeneration = -1; let memoized; const loginSessionPromises = new Map(); let remoteModelsKey; const builtins = new Map(builtinProviders().map((provider) => [provider.id, provider])); const effectiveConfig = () => { const raw = current() ?? {}; const providers = providerSettings(raw); const configured = providers[PROVIDER] ?? providers[LEGACY_PROVIDER]; return { ...raw, providers: { ...providers, [PROVIDER]: configured ?? { apiKeyEnv: API_KEY_ENV }, }, }; }; const profiles = () => { const raw = effectiveConfig(); const configuredProviders = providerSettings(current()); if (memoRaw === current() && memoProviders === configuredProviders && memoGeneration === generation && memoized) return memoized; const result = new Map(); for (const [provider, source] of Object.entries(raw.providers)) { if (modernSettings && !WORKBUDDY_PROVIDERS.has(provider)) continue; if (!ownsProvider(provider, builtins, source)) continue; if (WORKBUDDY_PROVIDERS.has(provider)) { const sourceWithAuth = workBuddySource(current(), source); const models = selectWorkBuddyModels(remoteModels ?? FALLBACK_MODELS, source.models); const configured = new Map((source.models ?? []).flatMap((model) => Number.isSafeInteger(model.maxTokens) && model.maxTokens > 0 ? [[model.id, model.maxTokens]] : [], )); result.set(provider, resolvedProfile(provider, { ...sourceWithAuth, headers: runtimeHeaders(sourceWithAuth.headers, requestContext), displayName: DISPLAY_NAME, }, workBuddyProvider(models, provider), configured, requestContext)); continue; } const base = builtins.get(provider); if (!base) { const generic = genericProvider(provider, source); if (!generic) continue; const configured = new Map((source.models ?? []).flatMap((model) => Number.isSafeInteger(model.maxTokens) && model.maxTokens > 0 ? [[model.id, model.maxTokens]] : [], )); result.set(provider, resolvedProfile(provider, source, generic, configured)); continue; } const selected = selectBuiltinModels(base, source.models); const configured = new Map((source.models ?? []).flatMap((model) => Number.isSafeInteger(model.maxTokens) && model.maxTokens > 0 ? [[model.id, model.maxTokens]] : [], )); result.set(provider, resolvedProfile(provider, source, selected, configured)); } memoRaw = current(); memoProviders = configuredProviders; memoGeneration = generation; memoized = result; return result; }; const readSessionRouting = async () => { const credentials = ctx.get("credentials"); const env = launchEnvironmentOf(ctx); const ref = credentialRef(WORKBUDDY_SESSION_ROUTING_REF); const stored = await credentials?.resolve(ref); const value = stored?.value ?? env.get(ref)?.value; return value ? parseWorkBuddySessionRouting(value) : createWorkBuddySessionRoutingState(); }; let routingBindingQueue = Promise.resolve(); const persistDefaultSessionBinding = (sessionId, fallbackBinding) => { const task = routingBindingQueue.then(async () => { const latest = await readSessionRouting(); const existing = sessionBindingFor(latest, sessionId); if (existing) return existing; const binding = latest.lastUsed ?? fallbackBinding; if (!binding) return undefined; const credentials = ctx.get("credentials"); if (!credentials) throw new Error("DSH 凭据服务不可用,无法保存会话认证"); await credentials.set(credentialRef(WORKBUDDY_SESSION_ROUTING_REF), serializeWorkBuddySessionRouting({ ...latest, bindings: { ...latest.bindings, [sessionId]: binding }, })); return binding; }); routingBindingQueue = task.then(() => undefined, () => undefined); return task; }; const resolveLoginSession = async (requestedId) => { const key = typeof requestedId === "string" && requestedId ? requestedId : "active"; let promise = loginSessionPromises.get(key); if (!promise) { promise = (async () => { const credentials = ctx.get("credentials"); const env = launchEnvironmentOf(ctx); const sessionsRef = credentialRef(WORKBUDDY_SESSIONS_REF); const sessionRefs = [sessionsRef, credentialRef(LEGACY_SESSIONS_REF)]; let sessionsValue; for (const ref of sessionRefs) { const storedSessions = await credentials?.resolve(ref); sessionsValue = storedSessions?.value ?? env.get(ref)?.value; if (sessionsValue) break; } let store; if (sessionsValue) { store = parseWorkBuddySessions(sessionsValue); } else { const legacyRefs = [credentialRef(WORKBUDDY_SESSION_REF), credentialRef(LEGACY_SESSION_REF)]; let legacyValue; for (const ref of legacyRefs) { const storedLegacy = await credentials?.resolve(ref); legacyValue = storedLegacy?.value ?? env.get(ref)?.value; if (legacyValue) break; } if (!legacyValue) throw new Error("未找到 WorkBuddy 登录凭据"); store = createWorkBuddySessionStore([parseWorkBuddySession(legacyValue)]); } const active = typeof requestedId === "string" && requestedId ? store.sessions.find((entry) => entry.id === requestedId) : activeWorkBuddySession(store); if (!active) throw new Error("未找到 WorkBuddy 登录账号"); let session = active; if (sessionNeedsRefresh(session)) { const trace = traceContext.getStore(); const started = Date.now(); traceEvent(trace, "auth.refresh-start"); try { session = { ...session, ...(await refreshWorkBuddySession(session)), updatedAt: Date.now() }; traceEvent(trace, "auth.refresh-done", { elapsedMs: Date.now() - started }); } catch (error) { traceEvent(trace, "auth.refresh-error", { elapsedMs: Date.now() - started, ...errorFields(error) }); throw error; } const nextStore = { ...store, sessions: store.sessions.map((entry) => entry.id === session.id ? session : entry), }; await credentials?.set(sessionsRef, serializeWorkBuddySessions(nextStore)); if (nextStore.activeId === session.id) await credentials?.set(credentialRef(WORKBUDDY_SESSION_REF), serializeWorkBuddySession(session)); } return { ...session, sessionId: active.id, expiresAt: sessionCacheDeadline(session) }; })().finally(() => { loginSessionPromises.delete(key); }); loginSessionPromises.set(key, promise); } return promise; }; const resolveCredential = async (provider, profile, context = requestContext.getStore()) => { context ??= {}; const ref = profile.apiKeyEnv; const routing = WORKBUDDY_PROVIDERS.has(provider) ? await readSessionRouting() : createWorkBuddySessionRoutingState(); const sessionId = context?.sessionId ? String(context.sessionId) : undefined; let binding = sessionBindingFor(routing, sessionId); if (WORKBUDDY_PROVIDERS.has(provider) && routing.enabled && sessionId && !binding) { let fallbackBinding; if (!routing.lastUsed) { if (ref) fallbackBinding = { mode: "api-key", apiKeyRef: ref }; else { try { const active = await resolveLoginSession(); fallbackBinding = { mode: "token", accountId: active.sessionId }; } catch (error) { throw new LlmError(`${name}: 没有可用于当前会话的默认 WorkBuddy 凭证`, "MISSING_CREDENTIAL", { cause: error }); } } } try { binding = await persistDefaultSessionBinding(sessionId, fallbackBinding); } catch (error) { throw new LlmError(`${name}: 无法保存当前会话的 WorkBuddy 凭证绑定`, "MISSING_CREDENTIAL", { cause: error }); } if (!binding) throw new LlmError(`${name}: 没有可用于当前会话的默认 WorkBuddy 凭证`, "MISSING_CREDENTIAL"); } if (WORKBUDDY_PROVIDERS.has(provider) && binding?.mode === "token") { let session; try { session = await resolveLoginSession(binding.accountId); } catch (error) { throw new LlmError(`${name}: 当前会话绑定的 WorkBuddy 登录账号不可用`, "MISSING_CREDENTIAL", { cause: error }); } context.headers = { ...(session.account.userId ? { "X-User-Id": session.account.userId } : {}), ...(session.account.enterpriseId ? { "X-Enterprise-Id": session.account.enterpriseId, "X-Tenant-Id": session.account.enterpriseId } : {}), ...(session.auth.domain ? { "X-Domain": session.auth.domain } : {}), }; return { value: assertUsableApiKey(session.auth.accessToken, name, "WorkBuddy login session"), kind: "bearer", sessionId: session.sessionId }; } if (WORKBUDDY_PROVIDERS.has(provider) && binding?.mode === "api-key") { const bindingRef = binding.apiKeyRef; if (!bindingRef) throw new LlmError(`${name}: 当前会话绑定的 API Key 引用无效`, "MISSING_CREDENTIAL"); const stored = await ctx.get("credentials")?.resolve(credentialRef(bindingRef)); const value = stored?.value ?? launchEnvironmentOf(ctx).get(credentialRef(bindingRef))?.value; if (value) return { value: assertUsableApiKey(value, name, bindingRef), kind: "api-key", ref: bindingRef }; throw new LlmError(`${name}: 当前会话绑定的 API Key 不可用`, "MISSING_CREDENTIAL"); } if (!ref && WORKBUDDY_PROVIDERS.has(provider)) { let session; try { session = await resolveLoginSession(); } catch (error) { throw new LlmError(`${name}: 未找到可用的 WorkBuddy 登录令牌,请运行 dsh-llm-workbuddy login`, "MISSING_CREDENTIAL", { cause: error }); } context.headers = { ...(session.account.userId ? { "X-User-Id": session.account.userId } : {}), ...(session.account.enterpriseId ? { "X-Enterprise-Id": session.account.enterpriseId, "X-Tenant-Id": session.account.enterpriseId } : {}), ...(session.auth.domain ? { "X-Domain": session.auth.domain } : {}), }; return { value: assertUsableApiKey(session.auth.accessToken, name, "WorkBuddy login session"), kind: "bearer", sessionId: session.sessionId }; } if (!ref) return { value: undefined, kind: "none" }; const stored = await ctx.get("credentials")?.resolve(ref); let value = stored?.value ?? launchEnvironmentOf(ctx).get(ref)?.value; if (!value && ref === API_KEY_ENV) { const legacyRef = credentialRef(LEGACY_API_KEY_ENV); const legacyStored = await ctx.get("credentials")?.resolve(legacyRef); value = legacyStored?.value ?? launchEnvironmentOf(ctx).get(legacyRef)?.value; } if (value) return { value: assertUsableApiKey(value, name, ref), kind: "api-key", ref }; throw new LlmError(`${name}: Provider "${provider}" 缺少 API Key,请在 WebUI 的模型设置中填写`, "MISSING_CREDENTIAL"); }; const resolveApiKey = async (provider, profile) => { const trace = WORKBUDDY_PROVIDERS.has(provider) ? traceContext.getStore() : undefined; const started = Date.now(); traceEvent(trace, "auth.start"); try { const credential = await resolveCredential(provider, profile); traceEvent(trace, "auth.done", { elapsedMs: Date.now() - started, mode: credential.kind }); return credential.value; } catch (error) { traceEvent(trace, "auth.error", { elapsedMs: Date.now() - started, ...errorFields(error) }); throw error; } }; const adapter = new PiAiAdapter({ profiles, resolveApiKey, resolveAttachments: () => ctx.get("attachments"), }); const refreshPromises = new Map(); const refreshWorkBuddyModels = async (provider, signal) => { const profile = profiles().get(provider); const credential = await resolveCredential(provider, profile); const cacheKey = credential.kind === "bearer" ? `token:${credential.sessionId ?? "active"}` : `api:${credential.ref ?? API_KEY_ENV}`; if (remoteModels && remoteModelsKey === cacheKey) return; let pending = refreshPromises.get(cacheKey); if (!pending) { pending = (async () => { remoteModels = await fetchWorkBuddyModels(credential, signal); remoteModelsKey = cacheKey; generation += 1; })().finally(() => refreshPromises.delete(cacheKey)); refreshPromises.set(cacheKey, pending); } return pending; }; const ensureWorkBuddyModel = async (provider, model, signal) => { if (!WORKBUDDY_PROVIDERS.has(provider)) return; if (profiles().get(provider)?.piProvider.getModels().some((entry) => entry.id === model)) return; await refreshWorkBuddyModels(provider, signal); }; const sessionScopedStream = (stream, options) => { const context = { sessionId: options?.sessionId === undefined ? undefined : String(options.sessionId), headers: {}, id: ++nextTraceId }; const run = (action) => traceContext.run(context, () => requestContext.run(context, action)); const source = run(() => stream(options)); const iterator = source[Symbol.asyncIterator](); return { [Symbol.asyncIterator]() { return this; }, next(value) { return run(() => iterator.next(value)); }, return(value) { return run(() => iterator.return?.(value) ?? Promise.resolve({ done: true, value })); }, throw(error) { return run(() => iterator.throw?.(error) ?? Promise.reject(error)); }, }; }; const adapterStream = adapter.stream.bind(adapter); const legacyAdapter = typeof adapter.prepareCall !== "function"; const invokeAdapterStream = (options) => adapterStream(prepareWorkBuddyOptions(options, legacyAdapter)); adapter.stream = (options) => sessionScopedStream(invokeAdapterStream, options); // `prepareCall` was added after the DSH rc.6 adapter. Keep the direct // `stream` path working on older hosts while wrapping prepared calls on // newer hosts, whose runtime dispatches through the returned stream handle. if (!legacyAdapter) { const adapterPrepareCall = adapter.prepareCall.bind(adapter); adapter.prepareCall = async (...args) => { await ensureWorkBuddyModel(args[0], args[1], args[2]); const prepared = await adapterPrepareCall(...args); return { ...prepared, stream: (options) => sessionScopedStream((preparedOptions) => prepared.stream(prepareWorkBuddyOptions(preparedOptions, false)), options), }; }; } else { // DSH 0.1.5 calls prepareCall unconditionally, while the rc.6 pi-ai // adapter shipped without it. Keep that older adapter usable by exposing // the same prepared-call shape from its existing methods. adapter.prepareCall = async (provider, model, signal) => ({ model: await adapter.resolveModel(provider, model, signal), stream: (options) => sessionScopedStream(invokeAdapterStream, options), }); } const resolveModel = adapter.resolveModel.bind(adapter); adapter.resolveModel = async (provider, model, signal) => { await ensureWorkBuddyModel(provider, model, signal); const resolved = await resolveModel(provider, model, signal); if (!WORKBUDDY_PROVIDERS.has(provider) || !resolved.reasoning) return resolved; const configured = profiles().get(provider)?.piProvider.getModels().find((entry) => entry.id === model); const effort = configured?.defaultReasoningEffort; if (!effort || !resolved.reasoning.efforts.some((entry) => entry.id === effort)) return resolved; return { ...resolved, reasoning: { ...resolved.reasoning, defaultEffort: effort } }; }; const listModels = adapter.listModels.bind(adapter); adapter.listModels = async (provider) => { if (WORKBUDDY_PROVIDERS.has(provider)) { try { await refreshWorkBuddyModels(provider); } catch { // Keep the built-in catalog available while the key or network is absent. } } return listModels(provider); }; const workBuddyDirectory = () => [{ provider: PROVIDER, displayName: DISPLAY_NAME, settingsNs, settingsPath: ["providers", PROVIDER], declared: false, }]; const directoryEntries = () => modernSettings ? workBuddyDirectory() : [...workBuddyDirectory(), ...[...builtins.values()].flatMap((provider) => provider.auth?.apiKey ? [{ provider: provider.id, displayName: provider.name, settingsNs, settingsPath: ["providers", provider.id], declared: false, }] : []), ...Object.entries(effectiveConfig().providers ?? {}).flatMap(([provider, source]) => { if (WORKBUDDY_PROVIDERS.has(provider) || builtins.has(provider) || !genericProvider(provider, source)) return []; return [{ provider, displayName: source.displayName ?? provider, settingsNs, settingsPath: ["providers", provider], declared: true, }]; })]; let directory = ctx.llm.registerConfigurableProviders(directoryEntries()); let registration = ctx.llm.registerAdapter([...profiles().keys()], adapter); ctx.llm.registerModelDiscovery(settingsNs, async (request, signal) => { const discoverySignal = signal ?? request.signal; if (WORKBUDDY_PROVIDERS.has(request.provider)) { const profile = profiles().get(request.provider); const credential = request.apiKey ? { value: request.apiKey, kind: "api-key", ref: API_KEY_ENV } : await resolveCredential(request.provider, profile); remoteModels = await fetchWorkBuddyModels(credential, discoverySignal); remoteModelsKey = credential.kind === "bearer" ? `token:${credential.sessionId ?? "active"}` : `api:${credential.ref ?? API_KEY_ENV}`; generation += 1; return remoteModels.map((model) => ({ id: model.id, name: model.name, contextWindow: model.contextWindow, maxTokens: model.maxTokens, })); } const provider = builtins.get(request.provider); if (!provider) return probeEndpoint({ ...request, signal: discoverySignal }, { profiles, resolveCredential }); return provider.getModels().map((model) => ({ id: model.id, name: model.name, contextWindow: model.contextWindow, maxTokens: model.maxTokens, })); }); // Keep WorkBuddy out of the settings base layer so it appears in WebUI's // "Add provider" dropdown. The runtime profile above still exists as the // built-in implementation; selecting it only persists the credential ref. const refreshRegistrations = () => { memoRaw = undefined; const providers = profiles(); registration.replace([...providers.keys()]); directory.replace(directoryEntries()); }; if (modernSettings) ctx.on("loader/volatile-update", refreshRegistrations); installSettingsCompat(ctx, settingsNs, Config, config ?? { providers: {} }, { setSource(source) { current = source; }, onChange() { refreshRegistrations(); }, }); }