feat: 个人定制版 —— API Key 模式显示剩余积分 + 悬浮卡片
基于 @axiaohungry/dsh-llm-workbuddy 1.3.21 (MIT)。 详见 README.CUSTOM.md。
This commit is contained in:
commit
a44227b69a
14 files changed
+5517
No files matched your search
@@ -0,0 +1,982 @@
|
||||
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();
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user