Merge branch 'main' into fix/tts-tool-no-channel-hang
This commit is contained in:
commit
4632b642be
@ -1,10 +1,5 @@
|
||||
import {
|
||||
buildOllamaProvider,
|
||||
emptyPluginConfigSchema,
|
||||
ensureOllamaModelPulled,
|
||||
OLLAMA_DEFAULT_BASE_URL,
|
||||
promptAndConfigureOllama,
|
||||
configureOllamaNonInteractive,
|
||||
type OpenClawPluginApi,
|
||||
type ProviderAuthContext,
|
||||
type ProviderAuthMethodNonInteractiveContext,
|
||||
@ -12,10 +7,15 @@ import {
|
||||
type ProviderDiscoveryContext,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
import { resolveOllamaApiBase } from "../../src/agents/models-config.providers.discovery.js";
|
||||
import { OLLAMA_DEFAULT_BASE_URL } from "../../src/agents/ollama-defaults.js";
|
||||
|
||||
const PROVIDER_ID = "ollama";
|
||||
const DEFAULT_API_KEY = "ollama-local";
|
||||
|
||||
async function loadProviderSetup() {
|
||||
return await import("openclaw/plugin-sdk/provider-setup");
|
||||
}
|
||||
|
||||
const ollamaPlugin = {
|
||||
id: "ollama",
|
||||
name: "Ollama Provider",
|
||||
@ -34,7 +34,8 @@ const ollamaPlugin = {
|
||||
hint: "Cloud and local open models",
|
||||
kind: "custom",
|
||||
run: async (ctx: ProviderAuthContext): Promise<ProviderAuthResult> => {
|
||||
const result = await promptAndConfigureOllama({
|
||||
const providerSetup = await loadProviderSetup();
|
||||
const result = await providerSetup.promptAndConfigureOllama({
|
||||
cfg: ctx.config,
|
||||
prompter: ctx.prompter,
|
||||
});
|
||||
@ -53,12 +54,14 @@ const ollamaPlugin = {
|
||||
defaultModel: `ollama/${result.defaultModelId}`,
|
||||
};
|
||||
},
|
||||
runNonInteractive: async (ctx: ProviderAuthMethodNonInteractiveContext) =>
|
||||
configureOllamaNonInteractive({
|
||||
runNonInteractive: async (ctx: ProviderAuthMethodNonInteractiveContext) => {
|
||||
const providerSetup = await loadProviderSetup();
|
||||
return await providerSetup.configureOllamaNonInteractive({
|
||||
nextConfig: ctx.config,
|
||||
opts: ctx.opts,
|
||||
runtime: ctx.runtime,
|
||||
}),
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
discovery: {
|
||||
@ -81,7 +84,8 @@ const ollamaPlugin = {
|
||||
};
|
||||
}
|
||||
|
||||
const provider = await buildOllamaProvider(explicit?.baseUrl, {
|
||||
const providerSetup = await loadProviderSetup();
|
||||
const provider = await providerSetup.buildOllamaProvider(explicit?.baseUrl, {
|
||||
quiet: !ollamaKey && !explicit,
|
||||
});
|
||||
if (provider.models.length === 0 && !ollamaKey && !explicit?.apiKey) {
|
||||
@ -115,7 +119,8 @@ const ollamaPlugin = {
|
||||
if (!model.startsWith("ollama/")) {
|
||||
return;
|
||||
}
|
||||
await ensureOllamaModelPulled({ config, prompter });
|
||||
const providerSetup = await loadProviderSetup();
|
||||
await providerSetup.ensureOllamaModelPulled({ config, prompter });
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
import { registerSandboxBackend } from "openclaw/plugin-sdk/core";
|
||||
import { registerSandboxBackend } from "openclaw/plugin-sdk/sandbox";
|
||||
import {
|
||||
createOpenShellSandboxBackendFactory,
|
||||
createOpenShellSandboxBackendManager,
|
||||
|
||||
@ -11,13 +11,13 @@ import type {
|
||||
SandboxBackendHandle,
|
||||
SandboxBackendManager,
|
||||
SshSandboxSession,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
} from "openclaw/plugin-sdk/sandbox";
|
||||
import {
|
||||
createRemoteShellSandboxFsBridge,
|
||||
disposeSshSandboxSession,
|
||||
resolvePreferredOpenClawTmpDir,
|
||||
runSshSandboxCommand,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
} from "openclaw/plugin-sdk/sandbox";
|
||||
import {
|
||||
buildExecRemoteCommand,
|
||||
buildRemoteCommand,
|
||||
|
||||
@ -4,10 +4,10 @@ import {
|
||||
runPluginCommandWithTimeout,
|
||||
shellEscape,
|
||||
type SshSandboxSession,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
} from "openclaw/plugin-sdk/sandbox";
|
||||
import type { ResolvedOpenShellPluginConfig } from "./config.js";
|
||||
|
||||
export { buildExecRemoteCommand, shellEscape } from "openclaw/plugin-sdk/core";
|
||||
export { buildExecRemoteCommand, shellEscape } from "openclaw/plugin-sdk/sandbox";
|
||||
|
||||
export type OpenShellExecContext = {
|
||||
config: ResolvedOpenShellPluginConfig;
|
||||
|
||||
@ -5,7 +5,7 @@ import type {
|
||||
SandboxFsBridge,
|
||||
SandboxFsStat,
|
||||
SandboxResolvedPath,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
} from "openclaw/plugin-sdk/sandbox";
|
||||
import type { OpenShellSandboxBackend } from "./backend.js";
|
||||
import { movePathWithCopyFallback } from "./mirror.js";
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import {
|
||||
type RemoteShellSandboxHandle,
|
||||
type SandboxContext,
|
||||
type SandboxFsBridge,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
} from "openclaw/plugin-sdk/sandbox";
|
||||
|
||||
export function createOpenShellRemoteFsBridge(params: {
|
||||
sandbox: SandboxContext;
|
||||
|
||||
@ -1,15 +1,20 @@
|
||||
import {
|
||||
buildSglangProvider,
|
||||
configureOpenAICompatibleSelfHostedProviderNonInteractive,
|
||||
discoverOpenAICompatibleSelfHostedProvider,
|
||||
emptyPluginConfigSchema,
|
||||
promptAndConfigureOpenAICompatibleSelfHostedProviderAuth,
|
||||
type OpenClawPluginApi,
|
||||
type ProviderAuthMethodNonInteractiveContext,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
SGLANG_DEFAULT_API_KEY_ENV_VAR,
|
||||
SGLANG_DEFAULT_BASE_URL,
|
||||
SGLANG_MODEL_PLACEHOLDER,
|
||||
SGLANG_PROVIDER_LABEL,
|
||||
} from "../../src/agents/sglang-defaults.js";
|
||||
|
||||
const PROVIDER_ID = "sglang";
|
||||
const DEFAULT_BASE_URL = "http://127.0.0.1:30000/v1";
|
||||
|
||||
async function loadProviderSetup() {
|
||||
return await import("openclaw/plugin-sdk/provider-setup");
|
||||
}
|
||||
|
||||
const sglangPlugin = {
|
||||
id: "sglang",
|
||||
@ -25,38 +30,44 @@ const sglangPlugin = {
|
||||
auth: [
|
||||
{
|
||||
id: "custom",
|
||||
label: "SGLang",
|
||||
label: SGLANG_PROVIDER_LABEL,
|
||||
hint: "Fast self-hosted OpenAI-compatible server",
|
||||
kind: "custom",
|
||||
run: async (ctx) =>
|
||||
promptAndConfigureOpenAICompatibleSelfHostedProviderAuth({
|
||||
run: async (ctx) => {
|
||||
const providerSetup = await loadProviderSetup();
|
||||
return await providerSetup.promptAndConfigureOpenAICompatibleSelfHostedProviderAuth({
|
||||
cfg: ctx.config,
|
||||
prompter: ctx.prompter,
|
||||
providerId: PROVIDER_ID,
|
||||
providerLabel: "SGLang",
|
||||
defaultBaseUrl: DEFAULT_BASE_URL,
|
||||
defaultApiKeyEnvVar: "SGLANG_API_KEY",
|
||||
modelPlaceholder: "Qwen/Qwen3-8B",
|
||||
}),
|
||||
runNonInteractive: async (ctx: ProviderAuthMethodNonInteractiveContext) =>
|
||||
configureOpenAICompatibleSelfHostedProviderNonInteractive({
|
||||
providerLabel: SGLANG_PROVIDER_LABEL,
|
||||
defaultBaseUrl: SGLANG_DEFAULT_BASE_URL,
|
||||
defaultApiKeyEnvVar: SGLANG_DEFAULT_API_KEY_ENV_VAR,
|
||||
modelPlaceholder: SGLANG_MODEL_PLACEHOLDER,
|
||||
});
|
||||
},
|
||||
runNonInteractive: async (ctx: ProviderAuthMethodNonInteractiveContext) => {
|
||||
const providerSetup = await loadProviderSetup();
|
||||
return await providerSetup.configureOpenAICompatibleSelfHostedProviderNonInteractive({
|
||||
ctx,
|
||||
providerId: PROVIDER_ID,
|
||||
providerLabel: "SGLang",
|
||||
defaultBaseUrl: DEFAULT_BASE_URL,
|
||||
defaultApiKeyEnvVar: "SGLANG_API_KEY",
|
||||
modelPlaceholder: "Qwen/Qwen3-8B",
|
||||
}),
|
||||
providerLabel: SGLANG_PROVIDER_LABEL,
|
||||
defaultBaseUrl: SGLANG_DEFAULT_BASE_URL,
|
||||
defaultApiKeyEnvVar: SGLANG_DEFAULT_API_KEY_ENV_VAR,
|
||||
modelPlaceholder: SGLANG_MODEL_PLACEHOLDER,
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
discovery: {
|
||||
order: "late",
|
||||
run: async (ctx) =>
|
||||
discoverOpenAICompatibleSelfHostedProvider({
|
||||
run: async (ctx) => {
|
||||
const providerSetup = await loadProviderSetup();
|
||||
return await providerSetup.discoverOpenAICompatibleSelfHostedProvider({
|
||||
ctx,
|
||||
providerId: PROVIDER_ID,
|
||||
buildProvider: buildSglangProvider,
|
||||
}),
|
||||
buildProvider: providerSetup.buildSglangProvider,
|
||||
});
|
||||
},
|
||||
},
|
||||
wizard: {
|
||||
setup: {
|
||||
|
||||
@ -335,6 +335,29 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps fallback replies silent after an error reply is skipped", async () => {
|
||||
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
|
||||
dispatcherOptions.onSkip?.(
|
||||
{ text: "oops", isError: true },
|
||||
{ kind: "final", reason: "empty" },
|
||||
);
|
||||
return { queuedFinal: false };
|
||||
});
|
||||
deliverReplies.mockResolvedValue({ delivered: true });
|
||||
|
||||
await dispatchWithContext({
|
||||
context: createContext(),
|
||||
telegramCfg: { silentErrorReplies: true },
|
||||
});
|
||||
|
||||
expect(deliverReplies).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
silent: true,
|
||||
replies: [expect.objectContaining({ text: expect.any(String) })],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps block streaming enabled when session reasoning level is on", async () => {
|
||||
loadSessionStore.mockReturnValue({
|
||||
s1: { reasoningLevel: "on" },
|
||||
|
||||
@ -515,6 +515,7 @@ export const dispatchTelegramMessage = async ({
|
||||
});
|
||||
|
||||
let queuedFinal = false;
|
||||
let hadErrorReplyFailureOrSkip = false;
|
||||
|
||||
if (statusReactionController) {
|
||||
void statusReactionController.setThinking();
|
||||
@ -541,6 +542,9 @@ export const dispatchTelegramMessage = async ({
|
||||
...prefixOptions,
|
||||
typingCallbacks,
|
||||
deliver: async (payload, info) => {
|
||||
if (payload.isError === true) {
|
||||
hadErrorReplyFailureOrSkip = true;
|
||||
}
|
||||
if (info.kind === "final") {
|
||||
// Assistant callbacks are fire-and-forget; ensure queued boundary
|
||||
// rotations/partials are applied before final delivery mapping.
|
||||
@ -654,7 +658,10 @@ export const dispatchTelegramMessage = async ({
|
||||
await flushBufferedFinalAnswer();
|
||||
}
|
||||
},
|
||||
onSkip: (_payload, info) => {
|
||||
onSkip: (payload, info) => {
|
||||
if (payload.isError === true) {
|
||||
hadErrorReplyFailureOrSkip = true;
|
||||
}
|
||||
if (info.reason !== "silent") {
|
||||
deliveryState.markNonSilentSkip();
|
||||
}
|
||||
@ -811,7 +818,7 @@ export const dispatchTelegramMessage = async ({
|
||||
const result = await deliverReplies({
|
||||
replies: [{ text: fallbackText }],
|
||||
...deliveryBaseOptions,
|
||||
silent: silentErrorReplies && dispatchError != null,
|
||||
silent: silentErrorReplies && (dispatchError != null || hadErrorReplyFailureOrSkip),
|
||||
});
|
||||
sentFallback = result.delivered;
|
||||
}
|
||||
|
||||
@ -1,15 +1,20 @@
|
||||
import {
|
||||
buildVllmProvider,
|
||||
configureOpenAICompatibleSelfHostedProviderNonInteractive,
|
||||
discoverOpenAICompatibleSelfHostedProvider,
|
||||
emptyPluginConfigSchema,
|
||||
promptAndConfigureOpenAICompatibleSelfHostedProviderAuth,
|
||||
type OpenClawPluginApi,
|
||||
type ProviderAuthMethodNonInteractiveContext,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
VLLM_DEFAULT_API_KEY_ENV_VAR,
|
||||
VLLM_DEFAULT_BASE_URL,
|
||||
VLLM_MODEL_PLACEHOLDER,
|
||||
VLLM_PROVIDER_LABEL,
|
||||
} from "../../src/agents/vllm-defaults.js";
|
||||
|
||||
const PROVIDER_ID = "vllm";
|
||||
const DEFAULT_BASE_URL = "http://127.0.0.1:8000/v1";
|
||||
|
||||
async function loadProviderSetup() {
|
||||
return await import("openclaw/plugin-sdk/provider-setup");
|
||||
}
|
||||
|
||||
const vllmPlugin = {
|
||||
id: "vllm",
|
||||
@ -25,38 +30,44 @@ const vllmPlugin = {
|
||||
auth: [
|
||||
{
|
||||
id: "custom",
|
||||
label: "vLLM",
|
||||
label: VLLM_PROVIDER_LABEL,
|
||||
hint: "Local/self-hosted OpenAI-compatible server",
|
||||
kind: "custom",
|
||||
run: async (ctx) =>
|
||||
promptAndConfigureOpenAICompatibleSelfHostedProviderAuth({
|
||||
run: async (ctx) => {
|
||||
const providerSetup = await loadProviderSetup();
|
||||
return await providerSetup.promptAndConfigureOpenAICompatibleSelfHostedProviderAuth({
|
||||
cfg: ctx.config,
|
||||
prompter: ctx.prompter,
|
||||
providerId: PROVIDER_ID,
|
||||
providerLabel: "vLLM",
|
||||
defaultBaseUrl: DEFAULT_BASE_URL,
|
||||
defaultApiKeyEnvVar: "VLLM_API_KEY",
|
||||
modelPlaceholder: "meta-llama/Meta-Llama-3-8B-Instruct",
|
||||
}),
|
||||
runNonInteractive: async (ctx: ProviderAuthMethodNonInteractiveContext) =>
|
||||
configureOpenAICompatibleSelfHostedProviderNonInteractive({
|
||||
providerLabel: VLLM_PROVIDER_LABEL,
|
||||
defaultBaseUrl: VLLM_DEFAULT_BASE_URL,
|
||||
defaultApiKeyEnvVar: VLLM_DEFAULT_API_KEY_ENV_VAR,
|
||||
modelPlaceholder: VLLM_MODEL_PLACEHOLDER,
|
||||
});
|
||||
},
|
||||
runNonInteractive: async (ctx: ProviderAuthMethodNonInteractiveContext) => {
|
||||
const providerSetup = await loadProviderSetup();
|
||||
return await providerSetup.configureOpenAICompatibleSelfHostedProviderNonInteractive({
|
||||
ctx,
|
||||
providerId: PROVIDER_ID,
|
||||
providerLabel: "vLLM",
|
||||
defaultBaseUrl: DEFAULT_BASE_URL,
|
||||
defaultApiKeyEnvVar: "VLLM_API_KEY",
|
||||
modelPlaceholder: "meta-llama/Meta-Llama-3-8B-Instruct",
|
||||
}),
|
||||
providerLabel: VLLM_PROVIDER_LABEL,
|
||||
defaultBaseUrl: VLLM_DEFAULT_BASE_URL,
|
||||
defaultApiKeyEnvVar: VLLM_DEFAULT_API_KEY_ENV_VAR,
|
||||
modelPlaceholder: VLLM_MODEL_PLACEHOLDER,
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
discovery: {
|
||||
order: "late",
|
||||
run: async (ctx) =>
|
||||
discoverOpenAICompatibleSelfHostedProvider({
|
||||
run: async (ctx) => {
|
||||
const providerSetup = await loadProviderSetup();
|
||||
return await providerSetup.discoverOpenAICompatibleSelfHostedProvider({
|
||||
ctx,
|
||||
providerId: PROVIDER_ID,
|
||||
buildProvider: buildVllmProvider,
|
||||
}),
|
||||
buildProvider: providerSetup.buildVllmProvider,
|
||||
});
|
||||
},
|
||||
},
|
||||
wizard: {
|
||||
setup: {
|
||||
|
||||
21
openclaw.mjs
21
openclaw.mjs
@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import module from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const MIN_NODE_MAJOR = 22;
|
||||
const MIN_NODE_MINOR = 12;
|
||||
@ -47,6 +48,20 @@ if (module.enableCompileCache && !process.env.NODE_DISABLE_COMPILE_CACHE) {
|
||||
const isModuleNotFoundError = (err) =>
|
||||
err && typeof err === "object" && "code" in err && err.code === "ERR_MODULE_NOT_FOUND";
|
||||
|
||||
const isDirectModuleNotFoundError = (err, specifier) => {
|
||||
if (!isModuleNotFoundError(err)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expectedUrl = new URL(specifier, import.meta.url);
|
||||
if ("url" in err && err.url === expectedUrl.href) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const message = "message" in err && typeof err.message === "string" ? err.message : "";
|
||||
return message.includes(fileURLToPath(expectedUrl));
|
||||
};
|
||||
|
||||
const installProcessWarningFilter = async () => {
|
||||
// Keep bootstrap warnings consistent with the TypeScript runtime.
|
||||
for (const specifier of ["./dist/warning-filter.js", "./dist/warning-filter.mjs"]) {
|
||||
@ -57,7 +72,7 @@ const installProcessWarningFilter = async () => {
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
if (isModuleNotFoundError(err)) {
|
||||
if (isDirectModuleNotFoundError(err, specifier)) {
|
||||
continue;
|
||||
}
|
||||
throw err;
|
||||
@ -72,8 +87,8 @@ const tryImport = async (specifier) => {
|
||||
await import(specifier);
|
||||
return true;
|
||||
} catch (err) {
|
||||
// Only swallow missing-module errors; rethrow real runtime errors.
|
||||
if (isModuleNotFoundError(err)) {
|
||||
// Only swallow direct entry misses; rethrow transitive resolution failures.
|
||||
if (isDirectModuleNotFoundError(err, specifier)) {
|
||||
return false;
|
||||
}
|
||||
throw err;
|
||||
|
||||
@ -50,6 +50,14 @@
|
||||
"types": "./dist/plugin-sdk/compat.d.ts",
|
||||
"default": "./dist/plugin-sdk/compat.js"
|
||||
},
|
||||
"./plugin-sdk/provider-setup": {
|
||||
"types": "./dist/plugin-sdk/provider-setup.d.ts",
|
||||
"default": "./dist/plugin-sdk/provider-setup.js"
|
||||
},
|
||||
"./plugin-sdk/sandbox": {
|
||||
"types": "./dist/plugin-sdk/sandbox.d.ts",
|
||||
"default": "./dist/plugin-sdk/sandbox.js"
|
||||
},
|
||||
"./plugin-sdk/routing": {
|
||||
"types": "./dist/plugin-sdk/routing.d.ts",
|
||||
"default": "./dist/plugin-sdk/routing.js"
|
||||
|
||||
56
src/agents/auth-profiles/upsert-with-lock.ts
Normal file
56
src/agents/auth-profiles/upsert-with-lock.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { withFileLock } from "../../infra/file-lock.js";
|
||||
import { loadJsonFile, saveJsonFile } from "../../infra/json-file.js";
|
||||
import { AUTH_STORE_LOCK_OPTIONS, AUTH_STORE_VERSION } from "./constants.js";
|
||||
import { ensureAuthStoreFile, resolveAuthStorePath } from "./paths.js";
|
||||
import type { AuthProfileCredential, AuthProfileStore, ProfileUsageStats } from "./types.js";
|
||||
|
||||
function coerceAuthProfileStore(raw: unknown): AuthProfileStore {
|
||||
const record = raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
|
||||
const profiles =
|
||||
record.profiles && typeof record.profiles === "object" && !Array.isArray(record.profiles)
|
||||
? { ...(record.profiles as Record<string, AuthProfileCredential>) }
|
||||
: {};
|
||||
const order =
|
||||
record.order && typeof record.order === "object" && !Array.isArray(record.order)
|
||||
? (record.order as Record<string, string[]>)
|
||||
: undefined;
|
||||
const lastGood =
|
||||
record.lastGood && typeof record.lastGood === "object" && !Array.isArray(record.lastGood)
|
||||
? (record.lastGood as Record<string, string>)
|
||||
: undefined;
|
||||
const usageStats =
|
||||
record.usageStats && typeof record.usageStats === "object" && !Array.isArray(record.usageStats)
|
||||
? (record.usageStats as Record<string, ProfileUsageStats>)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
version:
|
||||
typeof record.version === "number" && Number.isFinite(record.version)
|
||||
? record.version
|
||||
: AUTH_STORE_VERSION,
|
||||
profiles,
|
||||
...(order ? { order } : {}),
|
||||
...(lastGood ? { lastGood } : {}),
|
||||
...(usageStats ? { usageStats } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function upsertAuthProfileWithLock(params: {
|
||||
profileId: string;
|
||||
credential: AuthProfileCredential;
|
||||
agentDir?: string;
|
||||
}): Promise<AuthProfileStore | null> {
|
||||
const authPath = resolveAuthStorePath(params.agentDir);
|
||||
ensureAuthStoreFile(authPath);
|
||||
|
||||
try {
|
||||
return await withFileLock(authPath, AUTH_STORE_LOCK_OPTIONS, async () => {
|
||||
const store = coerceAuthProfileStore(loadJsonFile(authPath));
|
||||
store.profiles[params.profileId] = params.credential;
|
||||
saveJsonFile(authPath, store);
|
||||
return store;
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -18,8 +18,15 @@ import {
|
||||
resolveOllamaApiBase,
|
||||
type OllamaTagsResponse,
|
||||
} from "./ollama-models.js";
|
||||
import {
|
||||
SELF_HOSTED_DEFAULT_CONTEXT_WINDOW,
|
||||
SELF_HOSTED_DEFAULT_COST,
|
||||
SELF_HOSTED_DEFAULT_MAX_TOKENS,
|
||||
} from "./self-hosted-provider-defaults.js";
|
||||
import { SGLANG_DEFAULT_BASE_URL, SGLANG_PROVIDER_LABEL } from "./sglang-defaults.js";
|
||||
import { discoverVeniceModels, VENICE_BASE_URL } from "./venice-models.js";
|
||||
import { discoverVercelAiGatewayModels, VERCEL_AI_GATEWAY_BASE_URL } from "./vercel-ai-gateway.js";
|
||||
import { VLLM_DEFAULT_BASE_URL, VLLM_PROVIDER_LABEL } from "./vllm-defaults.js";
|
||||
|
||||
export { resolveOllamaApiBase } from "./ollama-models.js";
|
||||
|
||||
@ -31,19 +38,6 @@ const log = createSubsystemLogger("agents/model-providers");
|
||||
const OLLAMA_SHOW_CONCURRENCY = 8;
|
||||
const OLLAMA_SHOW_MAX_MODELS = 200;
|
||||
|
||||
const OPENAI_COMPAT_LOCAL_DEFAULT_CONTEXT_WINDOW = 128000;
|
||||
const OPENAI_COMPAT_LOCAL_DEFAULT_MAX_TOKENS = 8192;
|
||||
const OPENAI_COMPAT_LOCAL_DEFAULT_COST = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
};
|
||||
|
||||
const SGLANG_BASE_URL = "http://127.0.0.1:30000/v1";
|
||||
|
||||
const VLLM_BASE_URL = "http://127.0.0.1:8000/v1";
|
||||
|
||||
type OpenAICompatModelsResponse = {
|
||||
data?: Array<{
|
||||
id?: string;
|
||||
@ -140,9 +134,9 @@ async function discoverOpenAICompatibleLocalModels(params: {
|
||||
name: modelId,
|
||||
reasoning: isReasoningModelHeuristic(modelId),
|
||||
input: ["text"],
|
||||
cost: OPENAI_COMPAT_LOCAL_DEFAULT_COST,
|
||||
contextWindow: params.contextWindow ?? OPENAI_COMPAT_LOCAL_DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: params.maxTokens ?? OPENAI_COMPAT_LOCAL_DEFAULT_MAX_TOKENS,
|
||||
cost: SELF_HOSTED_DEFAULT_COST,
|
||||
contextWindow: params.contextWindow ?? SELF_HOSTED_DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: params.maxTokens ?? SELF_HOSTED_DEFAULT_MAX_TOKENS,
|
||||
} satisfies ModelDefinitionConfig;
|
||||
});
|
||||
} catch (error) {
|
||||
@ -197,11 +191,11 @@ export async function buildVllmProvider(params?: {
|
||||
baseUrl?: string;
|
||||
apiKey?: string;
|
||||
}): Promise<ProviderConfig> {
|
||||
const baseUrl = (params?.baseUrl?.trim() || VLLM_BASE_URL).replace(/\/+$/, "");
|
||||
const baseUrl = (params?.baseUrl?.trim() || VLLM_DEFAULT_BASE_URL).replace(/\/+$/, "");
|
||||
const models = await discoverOpenAICompatibleLocalModels({
|
||||
baseUrl,
|
||||
apiKey: params?.apiKey,
|
||||
label: "vLLM",
|
||||
label: VLLM_PROVIDER_LABEL,
|
||||
});
|
||||
return {
|
||||
baseUrl,
|
||||
@ -214,11 +208,11 @@ export async function buildSglangProvider(params?: {
|
||||
baseUrl?: string;
|
||||
apiKey?: string;
|
||||
}): Promise<ProviderConfig> {
|
||||
const baseUrl = (params?.baseUrl?.trim() || SGLANG_BASE_URL).replace(/\/+$/, "");
|
||||
const baseUrl = (params?.baseUrl?.trim() || SGLANG_DEFAULT_BASE_URL).replace(/\/+$/, "");
|
||||
const models = await discoverOpenAICompatibleLocalModels({
|
||||
baseUrl,
|
||||
apiKey: params?.apiKey,
|
||||
label: "SGLang",
|
||||
label: SGLANG_PROVIDER_LABEL,
|
||||
});
|
||||
return {
|
||||
baseUrl,
|
||||
|
||||
1
src/agents/ollama-defaults.ts
Normal file
1
src/agents/ollama-defaults.ts
Normal file
@ -0,0 +1 @@
|
||||
export const OLLAMA_DEFAULT_BASE_URL = "http://127.0.0.1:11434";
|
||||
@ -1,7 +1,6 @@
|
||||
import type { ModelDefinitionConfig } from "../config/types.models.js";
|
||||
import { OLLAMA_NATIVE_BASE_URL } from "./ollama-stream.js";
|
||||
import { OLLAMA_DEFAULT_BASE_URL } from "./ollama-defaults.js";
|
||||
|
||||
export const OLLAMA_DEFAULT_BASE_URL = OLLAMA_NATIVE_BASE_URL;
|
||||
export const OLLAMA_DEFAULT_CONTEXT_WINDOW = 128000;
|
||||
export const OLLAMA_DEFAULT_MAX_TOKENS = 8192;
|
||||
export const OLLAMA_DEFAULT_COST = {
|
||||
|
||||
@ -10,6 +10,7 @@ import type {
|
||||
import { createAssistantMessageEventStream } from "@mariozechner/pi-ai";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { isNonSecretApiKeyMarker } from "./model-auth-markers.js";
|
||||
import { OLLAMA_DEFAULT_BASE_URL } from "./ollama-defaults.js";
|
||||
import {
|
||||
buildAssistantMessage as buildStreamAssistantMessage,
|
||||
buildStreamErrorAssistantMessage,
|
||||
@ -18,7 +19,7 @@ import {
|
||||
|
||||
const log = createSubsystemLogger("ollama-stream");
|
||||
|
||||
export const OLLAMA_NATIVE_BASE_URL = "http://127.0.0.1:11434";
|
||||
export const OLLAMA_NATIVE_BASE_URL = OLLAMA_DEFAULT_BASE_URL;
|
||||
|
||||
export function resolveOllamaBaseUrlForRun(params: {
|
||||
modelBaseUrl?: string;
|
||||
|
||||
8
src/agents/self-hosted-provider-defaults.ts
Normal file
8
src/agents/self-hosted-provider-defaults.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export const SELF_HOSTED_DEFAULT_CONTEXT_WINDOW = 128000;
|
||||
export const SELF_HOSTED_DEFAULT_MAX_TOKENS = 8192;
|
||||
export const SELF_HOSTED_DEFAULT_COST = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
};
|
||||
4
src/agents/sglang-defaults.ts
Normal file
4
src/agents/sglang-defaults.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const SGLANG_DEFAULT_BASE_URL = "http://127.0.0.1:30000/v1";
|
||||
export const SGLANG_PROVIDER_LABEL = "SGLang";
|
||||
export const SGLANG_DEFAULT_API_KEY_ENV_VAR = "SGLANG_API_KEY";
|
||||
export const SGLANG_MODEL_PLACEHOLDER = "Qwen/Qwen3-8B";
|
||||
4
src/agents/vllm-defaults.ts
Normal file
4
src/agents/vllm-defaults.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const VLLM_DEFAULT_BASE_URL = "http://127.0.0.1:8000/v1";
|
||||
export const VLLM_PROVIDER_LABEL = "vLLM";
|
||||
export const VLLM_DEFAULT_API_KEY_ENV_VAR = "VLLM_API_KEY";
|
||||
export const VLLM_MODEL_PLACEHOLDER = "meta-llama/Meta-Llama-3-8B-Instruct";
|
||||
@ -1,4 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||
import {
|
||||
filterMessagingToolMediaDuplicates,
|
||||
shouldSuppressMessagingToolReplies,
|
||||
@ -153,4 +155,18 @@ describe("shouldSuppressMessagingToolReplies", () => {
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("suppresses telegram replies even when the active plugin registry omits telegram", () => {
|
||||
setActivePluginRegistry(createTestRegistry([]));
|
||||
|
||||
expect(
|
||||
shouldSuppressMessagingToolReplies({
|
||||
messageProvider: "telegram",
|
||||
originatingTo: "telegram:group:-100123:topic:77",
|
||||
messagingToolSentTargets: [
|
||||
{ tool: "message", provider: "telegram", to: "-100123", threadId: "77" },
|
||||
],
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
59
src/channels/plugins/target-parsing.test.ts
Normal file
59
src/channels/plugins/target-parsing.test.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||
import { parseExplicitTargetForChannel } from "./target-parsing.js";
|
||||
|
||||
describe("parseExplicitTargetForChannel", () => {
|
||||
beforeEach(() => {
|
||||
setActivePluginRegistry(createTestRegistry([]));
|
||||
});
|
||||
|
||||
it("parses bundled Telegram targets without an active Telegram registry entry", () => {
|
||||
expect(parseExplicitTargetForChannel("telegram", "telegram:group:-100123:topic:77")).toEqual({
|
||||
to: "-100123",
|
||||
threadId: 77,
|
||||
chatType: "group",
|
||||
});
|
||||
expect(parseExplicitTargetForChannel("telegram", "-100123")).toEqual({
|
||||
to: "-100123",
|
||||
chatType: "group",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses registered non-bundled channel targets via the active plugin contract", () => {
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "msteams",
|
||||
source: "test",
|
||||
plugin: {
|
||||
id: "msteams",
|
||||
meta: {
|
||||
id: "msteams",
|
||||
label: "Microsoft Teams",
|
||||
selectionLabel: "Microsoft Teams",
|
||||
docsPath: "/channels/msteams",
|
||||
blurb: "test stub",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => [],
|
||||
resolveAccount: () => ({}),
|
||||
},
|
||||
messaging: {
|
||||
parseExplicitTarget: ({ raw }: { raw: string }) => ({
|
||||
to: raw.trim().toUpperCase(),
|
||||
chatType: "direct" as const,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
expect(parseExplicitTargetForChannel("msteams", "team-room")).toEqual({
|
||||
to: "TEAM-ROOM",
|
||||
chatType: "direct",
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,4 +1,7 @@
|
||||
import { parseDiscordTarget } from "../../../extensions/discord/src/targets.js";
|
||||
import { parseTelegramTarget } from "../../../extensions/telegram/src/targets.js";
|
||||
import type { ChatType } from "../chat-type.js";
|
||||
import { normalizeChatChannelId } from "../registry.js";
|
||||
import { getChannelPlugin, normalizeChannelId } from "./registry.js";
|
||||
|
||||
export type ParsedChannelExplicitTarget = {
|
||||
@ -11,10 +14,28 @@ function parseWithPlugin(
|
||||
rawChannel: string,
|
||||
rawTarget: string,
|
||||
): ParsedChannelExplicitTarget | null {
|
||||
const channel = normalizeChannelId(rawChannel);
|
||||
const channel = normalizeChatChannelId(rawChannel) ?? normalizeChannelId(rawChannel);
|
||||
if (!channel) {
|
||||
return null;
|
||||
}
|
||||
if (channel === "telegram") {
|
||||
const target = parseTelegramTarget(rawTarget);
|
||||
return {
|
||||
to: target.chatId,
|
||||
...(target.messageThreadId != null ? { threadId: target.messageThreadId } : {}),
|
||||
...(target.chatType === "unknown" ? {} : { chatType: target.chatType }),
|
||||
};
|
||||
}
|
||||
if (channel === "discord") {
|
||||
const target = parseDiscordTarget(rawTarget, { defaultKind: "channel" });
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
to: target.id,
|
||||
chatType: target.kind === "user" ? "direct" : "channel",
|
||||
};
|
||||
}
|
||||
return getChannelPlugin(channel)?.messaging?.parseExplicitTarget?.({ raw: rawTarget }) ?? null;
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { Command } from "commander";
|
||||
import JSON5 from "json5";
|
||||
import { OLLAMA_DEFAULT_BASE_URL } from "../agents/ollama-defaults.js";
|
||||
import { readConfigFileSnapshot, writeConfigFile } from "../config/config.js";
|
||||
import { formatConfigIssueLines, normalizeConfigIssues } from "../config/issue-format.js";
|
||||
import { CONFIG_PATH } from "../config/paths.js";
|
||||
@ -20,7 +21,6 @@ type ConfigSetParseOpts = {
|
||||
|
||||
const OLLAMA_API_KEY_PATH: PathSegment[] = ["models", "providers", "ollama", "apiKey"];
|
||||
const OLLAMA_PROVIDER_PATH: PathSegment[] = ["models", "providers", "ollama"];
|
||||
const OLLAMA_DEFAULT_BASE_URL = "http://127.0.0.1:11434";
|
||||
|
||||
function isIndexSegment(raw: string): boolean {
|
||||
return /^[0-9]+$/.test(raw);
|
||||
|
||||
73
src/commands/auth-profile-config.ts
Normal file
73
src/commands/auth-profile-config.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
|
||||
export function applyAuthProfileConfig(
|
||||
cfg: OpenClawConfig,
|
||||
params: {
|
||||
profileId: string;
|
||||
provider: string;
|
||||
mode: "api_key" | "oauth" | "token";
|
||||
email?: string;
|
||||
preferProfileFirst?: boolean;
|
||||
},
|
||||
): OpenClawConfig {
|
||||
const normalizedProvider = params.provider.toLowerCase();
|
||||
const profiles = {
|
||||
...cfg.auth?.profiles,
|
||||
[params.profileId]: {
|
||||
provider: params.provider,
|
||||
mode: params.mode,
|
||||
...(params.email ? { email: params.email } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
const configuredProviderProfiles = Object.entries(cfg.auth?.profiles ?? {})
|
||||
.filter(([, profile]) => profile.provider.toLowerCase() === normalizedProvider)
|
||||
.map(([profileId, profile]) => ({ profileId, mode: profile.mode }));
|
||||
|
||||
// Maintain `auth.order` when it already exists. Additionally, if we detect
|
||||
// mixed auth modes for the same provider (e.g. legacy oauth + newly selected
|
||||
// api_key), create an explicit order to keep the newly selected profile first.
|
||||
const existingProviderOrder = cfg.auth?.order?.[params.provider];
|
||||
const preferProfileFirst = params.preferProfileFirst ?? true;
|
||||
const reorderedProviderOrder =
|
||||
existingProviderOrder && preferProfileFirst
|
||||
? [
|
||||
params.profileId,
|
||||
...existingProviderOrder.filter((profileId) => profileId !== params.profileId),
|
||||
]
|
||||
: existingProviderOrder;
|
||||
const hasMixedConfiguredModes = configuredProviderProfiles.some(
|
||||
({ profileId, mode }) => profileId !== params.profileId && mode !== params.mode,
|
||||
);
|
||||
const derivedProviderOrder =
|
||||
existingProviderOrder === undefined && preferProfileFirst && hasMixedConfiguredModes
|
||||
? [
|
||||
params.profileId,
|
||||
...configuredProviderProfiles
|
||||
.map(({ profileId }) => profileId)
|
||||
.filter((profileId) => profileId !== params.profileId),
|
||||
]
|
||||
: undefined;
|
||||
const order =
|
||||
existingProviderOrder !== undefined
|
||||
? {
|
||||
...cfg.auth?.order,
|
||||
[params.provider]: reorderedProviderOrder?.includes(params.profileId)
|
||||
? reorderedProviderOrder
|
||||
: [...(reorderedProviderOrder ?? []), params.profileId],
|
||||
}
|
||||
: derivedProviderOrder
|
||||
? {
|
||||
...cfg.auth?.order,
|
||||
[params.provider]: derivedProviderOrder,
|
||||
}
|
||||
: cfg.auth?.order;
|
||||
return {
|
||||
...cfg,
|
||||
auth: {
|
||||
...cfg.auth,
|
||||
profiles,
|
||||
...(order ? { order } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import { upsertAuthProfileWithLock } from "../agents/auth-profiles.js";
|
||||
import { upsertAuthProfileWithLock } from "../agents/auth-profiles/upsert-with-lock.js";
|
||||
import { OLLAMA_DEFAULT_BASE_URL } from "../agents/ollama-defaults.js";
|
||||
import {
|
||||
OLLAMA_DEFAULT_BASE_URL,
|
||||
buildOllamaModelDefinition,
|
||||
enrichOllamaModelsWithContext,
|
||||
fetchOllamaModels,
|
||||
@ -15,7 +15,7 @@ import { applyAgentDefaultModelPrimary } from "./onboard-auth.config-shared.js";
|
||||
import { openUrl } from "./onboard-helpers.js";
|
||||
import type { OnboardMode, OnboardOptions } from "./onboard-types.js";
|
||||
|
||||
export { OLLAMA_DEFAULT_BASE_URL } from "../agents/ollama-models.js";
|
||||
export { OLLAMA_DEFAULT_BASE_URL } from "../agents/ollama-defaults.js";
|
||||
export const OLLAMA_DEFAULT_MODEL = "glm-4.7-flash";
|
||||
|
||||
const OLLAMA_SUGGESTED_MODELS_LOCAL = ["glm-4.7-flash"];
|
||||
|
||||
@ -84,6 +84,7 @@ import {
|
||||
MODELSTUDIO_GLOBAL_BASE_URL,
|
||||
MODELSTUDIO_DEFAULT_MODEL_REF,
|
||||
} from "./onboard-auth.models.js";
|
||||
export { applyAuthProfileConfig } from "./auth-profile-config.js";
|
||||
|
||||
function mergeProviderModels<T extends { id: string }>(
|
||||
existingProvider: Record<string, unknown> | undefined,
|
||||
@ -484,78 +485,6 @@ export function applyKilocodeConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
return applyAgentDefaultModelPrimary(next, KILOCODE_DEFAULT_MODEL_REF);
|
||||
}
|
||||
|
||||
export function applyAuthProfileConfig(
|
||||
cfg: OpenClawConfig,
|
||||
params: {
|
||||
profileId: string;
|
||||
provider: string;
|
||||
mode: "api_key" | "oauth" | "token";
|
||||
email?: string;
|
||||
preferProfileFirst?: boolean;
|
||||
},
|
||||
): OpenClawConfig {
|
||||
const normalizedProvider = params.provider.toLowerCase();
|
||||
const profiles = {
|
||||
...cfg.auth?.profiles,
|
||||
[params.profileId]: {
|
||||
provider: params.provider,
|
||||
mode: params.mode,
|
||||
...(params.email ? { email: params.email } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
const configuredProviderProfiles = Object.entries(cfg.auth?.profiles ?? {})
|
||||
.filter(([, profile]) => profile.provider.toLowerCase() === normalizedProvider)
|
||||
.map(([profileId, profile]) => ({ profileId, mode: profile.mode }));
|
||||
|
||||
// Maintain `auth.order` when it already exists. Additionally, if we detect
|
||||
// mixed auth modes for the same provider (e.g. legacy oauth + newly selected
|
||||
// api_key), create an explicit order to keep the newly selected profile first.
|
||||
const existingProviderOrder = cfg.auth?.order?.[params.provider];
|
||||
const preferProfileFirst = params.preferProfileFirst ?? true;
|
||||
const reorderedProviderOrder =
|
||||
existingProviderOrder && preferProfileFirst
|
||||
? [
|
||||
params.profileId,
|
||||
...existingProviderOrder.filter((profileId) => profileId !== params.profileId),
|
||||
]
|
||||
: existingProviderOrder;
|
||||
const hasMixedConfiguredModes = configuredProviderProfiles.some(
|
||||
({ profileId, mode }) => profileId !== params.profileId && mode !== params.mode,
|
||||
);
|
||||
const derivedProviderOrder =
|
||||
existingProviderOrder === undefined && preferProfileFirst && hasMixedConfiguredModes
|
||||
? [
|
||||
params.profileId,
|
||||
...configuredProviderProfiles
|
||||
.map(({ profileId }) => profileId)
|
||||
.filter((profileId) => profileId !== params.profileId),
|
||||
]
|
||||
: undefined;
|
||||
const order =
|
||||
existingProviderOrder !== undefined
|
||||
? {
|
||||
...cfg.auth?.order,
|
||||
[params.provider]: reorderedProviderOrder?.includes(params.profileId)
|
||||
? reorderedProviderOrder
|
||||
: [...(reorderedProviderOrder ?? []), params.profileId],
|
||||
}
|
||||
: derivedProviderOrder
|
||||
? {
|
||||
...cfg.auth?.order,
|
||||
[params.provider]: derivedProviderOrder,
|
||||
}
|
||||
: cfg.auth?.order;
|
||||
return {
|
||||
...cfg,
|
||||
auth: {
|
||||
...cfg.auth,
|
||||
profiles,
|
||||
...(order ? { order } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyQianfanProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[QIANFAN_DEFAULT_MODEL_REF] = {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { CONTEXT_WINDOW_HARD_MIN_TOKENS } from "../agents/context-window-guard.js";
|
||||
import { OLLAMA_DEFAULT_BASE_URL } from "../agents/ollama-models.js";
|
||||
import { OLLAMA_DEFAULT_BASE_URL } from "../agents/ollama-defaults.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { CONTEXT_WINDOW_HARD_MIN_TOKENS } from "../agents/context-window-guard.js";
|
||||
import { DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import { buildModelAliasIndex, modelKey } from "../agents/model-selection.js";
|
||||
import { OLLAMA_DEFAULT_BASE_URL } from "../agents/ollama-models.js";
|
||||
import { OLLAMA_DEFAULT_BASE_URL } from "../agents/ollama-defaults.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { ModelProviderConfig } from "../config/types.models.js";
|
||||
import { isSecretRef, type SecretInput } from "../config/types.secrets.js";
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import { upsertAuthProfileWithLock } from "../agents/auth-profiles.js";
|
||||
import type { ApiKeyCredential, AuthProfileCredential } from "../agents/auth-profiles/types.js";
|
||||
import { upsertAuthProfileWithLock } from "../agents/auth-profiles/upsert-with-lock.js";
|
||||
import {
|
||||
SELF_HOSTED_DEFAULT_CONTEXT_WINDOW,
|
||||
SELF_HOSTED_DEFAULT_COST,
|
||||
SELF_HOSTED_DEFAULT_MAX_TOKENS,
|
||||
} from "../agents/self-hosted-provider-defaults.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type {
|
||||
ProviderDiscoveryContext,
|
||||
@ -8,16 +13,13 @@ import type {
|
||||
ProviderNonInteractiveApiKeyResult,
|
||||
} from "../plugins/types.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import { applyAuthProfileConfig } from "./onboard-auth.js";
|
||||
import { applyAuthProfileConfig } from "./auth-profile-config.js";
|
||||
|
||||
export const SELF_HOSTED_DEFAULT_CONTEXT_WINDOW = 128000;
|
||||
export const SELF_HOSTED_DEFAULT_MAX_TOKENS = 8192;
|
||||
export const SELF_HOSTED_DEFAULT_COST = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
};
|
||||
export {
|
||||
SELF_HOSTED_DEFAULT_CONTEXT_WINDOW,
|
||||
SELF_HOSTED_DEFAULT_COST,
|
||||
SELF_HOSTED_DEFAULT_MAX_TOKENS,
|
||||
} from "../agents/self-hosted-provider-defaults.js";
|
||||
|
||||
export function applyProviderDefaultModel(cfg: OpenClawConfig, modelRef: string): OpenClawConfig {
|
||||
const existingModel = cfg.agents?.defaults?.model;
|
||||
|
||||
@ -1,14 +1,20 @@
|
||||
import {
|
||||
VLLM_DEFAULT_API_KEY_ENV_VAR,
|
||||
VLLM_DEFAULT_BASE_URL,
|
||||
VLLM_MODEL_PLACEHOLDER,
|
||||
VLLM_PROVIDER_LABEL,
|
||||
} from "../agents/vllm-defaults.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import {
|
||||
applyProviderDefaultModel,
|
||||
promptAndConfigureOpenAICompatibleSelfHostedProvider,
|
||||
SELF_HOSTED_DEFAULT_CONTEXT_WINDOW,
|
||||
SELF_HOSTED_DEFAULT_COST,
|
||||
SELF_HOSTED_DEFAULT_MAX_TOKENS,
|
||||
promptAndConfigureOpenAICompatibleSelfHostedProvider,
|
||||
} from "./self-hosted-provider-setup.js";
|
||||
|
||||
export const VLLM_DEFAULT_BASE_URL = "http://127.0.0.1:8000/v1";
|
||||
export { VLLM_DEFAULT_BASE_URL } from "../agents/vllm-defaults.js";
|
||||
export const VLLM_DEFAULT_CONTEXT_WINDOW = SELF_HOSTED_DEFAULT_CONTEXT_WINDOW;
|
||||
export const VLLM_DEFAULT_MAX_TOKENS = SELF_HOSTED_DEFAULT_MAX_TOKENS;
|
||||
export const VLLM_DEFAULT_COST = SELF_HOSTED_DEFAULT_COST;
|
||||
@ -21,10 +27,10 @@ export async function promptAndConfigureVllm(params: {
|
||||
cfg: params.cfg,
|
||||
prompter: params.prompter,
|
||||
providerId: "vllm",
|
||||
providerLabel: "vLLM",
|
||||
providerLabel: VLLM_PROVIDER_LABEL,
|
||||
defaultBaseUrl: VLLM_DEFAULT_BASE_URL,
|
||||
defaultApiKeyEnvVar: "VLLM_API_KEY",
|
||||
modelPlaceholder: "meta-llama/Meta-Llama-3-8B-Instruct",
|
||||
defaultApiKeyEnvVar: VLLM_DEFAULT_API_KEY_ENV_VAR,
|
||||
modelPlaceholder: VLLM_MODEL_PLACEHOLDER,
|
||||
});
|
||||
return {
|
||||
config: result.config,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import "./isolated-agent.mocks.js";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
||||
import * as modelSelection from "../agents/model-selection.js";
|
||||
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import { runSubagentAnnounceFlow } from "../agents/subagent-announce.js";
|
||||
import type { CliDeps } from "../cli/deps.js";
|
||||
@ -72,6 +73,7 @@ async function runTelegramAnnounceTurn(params: {
|
||||
|
||||
describe("runCronIsolatedAgentTurn", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(modelSelection, "resolveThinkingDefault").mockReturnValue(undefined);
|
||||
setupIsolatedAgentTurnMocks({ fast: true });
|
||||
});
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import "./isolated-agent.mocks.js";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { loadModelCatalog } from "../agents/model-catalog.js";
|
||||
import * as modelSelection from "../agents/model-selection.js";
|
||||
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import { createCliDeps, mockAgentPayloads } from "./isolated-agent.delivery.test-helpers.js";
|
||||
import { runCronIsolatedAgentTurn } from "./isolated-agent.js";
|
||||
@ -125,6 +126,7 @@ async function expectInvalidModel(home: string, model: string) {
|
||||
|
||||
describe("cron model formatting and precedence edge cases", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(modelSelection, "resolveThinkingDefault").mockReturnValue(undefined);
|
||||
vi.mocked(runEmbeddedPiAgent).mockClear();
|
||||
vi.mocked(loadModelCatalog).mockResolvedValue([]);
|
||||
});
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import "./isolated-agent.mocks.js";
|
||||
import fs from "node:fs/promises";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as modelSelection from "../agents/model-selection.js";
|
||||
import { runSubagentAnnounceFlow } from "../agents/subagent-announce.js";
|
||||
import type { CliDeps } from "../cli/deps.js";
|
||||
import {
|
||||
@ -261,6 +262,7 @@ async function assertExplicitTelegramTargetDelivery(params: {
|
||||
|
||||
describe("runCronIsolatedAgentTurn", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(modelSelection, "resolveThinkingDefault").mockReturnValue(undefined);
|
||||
setupIsolatedAgentTurnMocks();
|
||||
});
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { loadModelCatalog } from "../agents/model-catalog.js";
|
||||
import * as modelSelection from "../agents/model-selection.js";
|
||||
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import type { CliDeps } from "../cli/deps.js";
|
||||
import { runCronIsolatedAgentTurn } from "./isolated-agent.js";
|
||||
@ -15,6 +16,10 @@ import {
|
||||
} from "./isolated-agent.test-harness.js";
|
||||
import type { CronJob } from "./types.js";
|
||||
|
||||
let resolveThinkingDefaultSpy: ReturnType<
|
||||
typeof vi.spyOn<typeof modelSelection, "resolveThinkingDefault">
|
||||
>;
|
||||
|
||||
function makeDeps(): CliDeps {
|
||||
return {
|
||||
sendMessageSlack: vi.fn(),
|
||||
@ -163,6 +168,9 @@ async function runStoredOverrideAndExpectModel(params: {
|
||||
|
||||
describe("runCronIsolatedAgentTurn", () => {
|
||||
beforeEach(() => {
|
||||
resolveThinkingDefaultSpy = vi
|
||||
.spyOn(modelSelection, "resolveThinkingDefault")
|
||||
.mockReturnValue(undefined);
|
||||
vi.mocked(runEmbeddedPiAgent).mockClear();
|
||||
vi.mocked(loadModelCatalog).mockResolvedValue([]);
|
||||
});
|
||||
@ -503,16 +511,9 @@ describe("runCronIsolatedAgentTurn", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults thinking to low for reasoning-capable models", async () => {
|
||||
it("passes through the resolved default thinking level", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
vi.mocked(loadModelCatalog).mockResolvedValueOnce([
|
||||
{
|
||||
id: "claude-opus-4-5",
|
||||
name: "Opus 4.5",
|
||||
provider: "anthropic",
|
||||
reasoning: true,
|
||||
},
|
||||
]);
|
||||
resolveThinkingDefaultSpy.mockReturnValueOnce("low");
|
||||
|
||||
await runCronTurn(home, {
|
||||
jobPayload: DEFAULT_AGENT_TURN_PAYLOAD,
|
||||
|
||||
@ -171,6 +171,27 @@ async function resolveCronDeliveryContext(params: {
|
||||
deliveryContract: IsolatedDeliveryContract;
|
||||
}) {
|
||||
const deliveryPlan = resolveCronDeliveryPlan(params.job);
|
||||
if (!deliveryPlan.requested) {
|
||||
const resolvedDelivery = {
|
||||
ok: false as const,
|
||||
channel: undefined,
|
||||
to: undefined,
|
||||
accountId: undefined,
|
||||
threadId: undefined,
|
||||
mode: "implicit" as const,
|
||||
error: new Error("cron delivery not requested"),
|
||||
};
|
||||
return {
|
||||
deliveryPlan,
|
||||
deliveryRequested: false,
|
||||
resolvedDelivery,
|
||||
toolPolicy: resolveCronToolPolicy({
|
||||
deliveryRequested: false,
|
||||
resolvedDelivery,
|
||||
deliveryContract: params.deliveryContract,
|
||||
}),
|
||||
};
|
||||
}
|
||||
const resolvedDelivery = await resolveDeliveryTarget(params.cfg, params.agentId, {
|
||||
channel: deliveryPlan.channel ?? "last",
|
||||
to: deliveryPlan.to,
|
||||
|
||||
@ -59,6 +59,13 @@ vi.mock("../infra/device-identity.js", () => ({
|
||||
}));
|
||||
vi.mock("./session-utils.js", () => ({
|
||||
loadSessionEntry: vi.fn((sessionKey: string) => buildSessionLookup(sessionKey)),
|
||||
migrateAndPruneGatewaySessionStoreKey: vi.fn(
|
||||
({ key, store }: { key: string; store: Record<string, unknown> }) => ({
|
||||
target: { canonicalKey: key, storeKeys: [key] },
|
||||
primaryKey: key,
|
||||
entry: store[key],
|
||||
}),
|
||||
),
|
||||
pruneLegacyStoreKeys: vi.fn(),
|
||||
resolveGatewaySessionStoreTarget: vi.fn(({ key }: { key: string }) => ({
|
||||
canonicalKey: key,
|
||||
|
||||
@ -12,12 +12,23 @@ import { withEnvAsync } from "../test-utils/env.js";
|
||||
import { clearMediaUnderstandingBinaryCacheForTests } from "./runner.js";
|
||||
import { createSafeAudioFixtureBuffer } from "./runner.test-utils.js";
|
||||
|
||||
vi.mock("../agents/model-auth.js", () => ({
|
||||
resolveApiKeyForProvider: vi.fn(async () => ({
|
||||
const resolveApiKeyForProviderMock = vi.hoisted(() =>
|
||||
vi.fn(async () => ({
|
||||
apiKey: "test-key", // pragma: allowlist secret
|
||||
source: "test",
|
||||
mode: "api-key",
|
||||
})),
|
||||
);
|
||||
const hasAvailableAuthForProviderMock = vi.hoisted(() =>
|
||||
vi.fn(async (...args: unknown[]) => {
|
||||
const resolved = await resolveApiKeyForProviderMock(...args);
|
||||
return Boolean(resolved?.apiKey);
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock("../agents/model-auth.js", () => ({
|
||||
resolveApiKeyForProvider: resolveApiKeyForProviderMock,
|
||||
hasAvailableAuthForProvider: hasAvailableAuthForProviderMock,
|
||||
requireApiKey: (auth: { apiKey?: string; mode?: string }, provider: string) => {
|
||||
if (auth?.apiKey) {
|
||||
return auth.apiKey;
|
||||
@ -247,6 +258,7 @@ describe("applyMediaUnderstanding", () => {
|
||||
source: "test",
|
||||
mode: "api-key",
|
||||
});
|
||||
hasAvailableAuthForProviderMock.mockClear();
|
||||
mockedFetchRemoteMedia.mockClear();
|
||||
mockedRunExec.mockReset();
|
||||
mockedFetchRemoteMedia.mockResolvedValue({
|
||||
|
||||
@ -30,27 +30,6 @@ export type {
|
||||
ProviderAuthMethod,
|
||||
ProviderAuthResult,
|
||||
} from "../plugins/types.js";
|
||||
export type {
|
||||
CreateSandboxBackendParams,
|
||||
RemoteShellSandboxHandle,
|
||||
RunSshSandboxCommandParams,
|
||||
SandboxBackendCommandParams,
|
||||
SandboxBackendCommandResult,
|
||||
SandboxBackendExecSpec,
|
||||
SandboxBackendFactory,
|
||||
SandboxFsBridge,
|
||||
SandboxFsStat,
|
||||
SandboxBackendHandle,
|
||||
SandboxBackendId,
|
||||
SandboxBackendManager,
|
||||
SandboxBackendRegistration,
|
||||
SandboxBackendRuntimeInfo,
|
||||
SandboxContext,
|
||||
SandboxResolvedPath,
|
||||
SandboxSshConfig,
|
||||
SshSandboxSession,
|
||||
SshSandboxSettings,
|
||||
} from "../agents/sandbox.js";
|
||||
export type { OpenClawConfig } from "../config/config.js";
|
||||
export type { GatewayRequestHandlerOptions } from "../gateway/server-methods/types.js";
|
||||
export type {
|
||||
@ -66,58 +45,7 @@ export type {
|
||||
} from "./channel-plugin-common.js";
|
||||
|
||||
export { emptyPluginConfigSchema } from "./channel-plugin-common.js";
|
||||
export {
|
||||
buildExecRemoteCommand,
|
||||
buildRemoteCommand,
|
||||
buildSshSandboxArgv,
|
||||
createRemoteShellSandboxFsBridge,
|
||||
createSshSandboxSessionFromConfigText,
|
||||
createSshSandboxSessionFromSettings,
|
||||
disposeSshSandboxSession,
|
||||
getSandboxBackendFactory,
|
||||
getSandboxBackendManager,
|
||||
registerSandboxBackend,
|
||||
runSshSandboxCommand,
|
||||
shellEscape,
|
||||
uploadDirectoryToSshTarget,
|
||||
requireSandboxBackendFactory,
|
||||
} from "../agents/sandbox.js";
|
||||
export { buildOauthProviderAuthResult } from "./provider-auth-result.js";
|
||||
export {
|
||||
applyProviderDefaultModel,
|
||||
configureOpenAICompatibleSelfHostedProviderNonInteractive,
|
||||
discoverOpenAICompatibleSelfHostedProvider,
|
||||
promptAndConfigureOpenAICompatibleSelfHostedProvider,
|
||||
promptAndConfigureOpenAICompatibleSelfHostedProviderAuth,
|
||||
SELF_HOSTED_DEFAULT_CONTEXT_WINDOW,
|
||||
SELF_HOSTED_DEFAULT_COST,
|
||||
SELF_HOSTED_DEFAULT_MAX_TOKENS,
|
||||
} from "../commands/self-hosted-provider-setup.js";
|
||||
export {
|
||||
OLLAMA_DEFAULT_BASE_URL,
|
||||
OLLAMA_DEFAULT_MODEL,
|
||||
configureOllamaNonInteractive,
|
||||
ensureOllamaModelPulled,
|
||||
promptAndConfigureOllama,
|
||||
} from "../commands/ollama-setup.js";
|
||||
export {
|
||||
VLLM_DEFAULT_BASE_URL,
|
||||
VLLM_DEFAULT_CONTEXT_WINDOW,
|
||||
VLLM_DEFAULT_COST,
|
||||
VLLM_DEFAULT_MAX_TOKENS,
|
||||
promptAndConfigureVllm,
|
||||
} from "../commands/vllm-setup.js";
|
||||
export {
|
||||
buildOllamaProvider,
|
||||
buildSglangProvider,
|
||||
buildVllmProvider,
|
||||
} from "../agents/models-config.providers.discovery.js";
|
||||
|
||||
export {
|
||||
approveDevicePairing,
|
||||
listDevicePairing,
|
||||
rejectDevicePairing,
|
||||
} from "../infra/device-pairing.js";
|
||||
export {
|
||||
DEFAULT_SECRET_FILE_MAX_BYTES,
|
||||
loadSecretFileSync,
|
||||
@ -126,13 +54,6 @@ export {
|
||||
} from "../infra/secret-file.js";
|
||||
export type { SecretFileReadOptions, SecretFileReadResult } from "../infra/secret-file.js";
|
||||
|
||||
export {
|
||||
runPluginCommandWithTimeout,
|
||||
type PluginCommandRunOptions,
|
||||
type PluginCommandRunResult,
|
||||
} from "./run-command.js";
|
||||
export { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
|
||||
|
||||
export { resolveGatewayBindUrl } from "../shared/gateway-bind-url.js";
|
||||
export type { GatewayBindUrlResult } from "../shared/gateway-bind-url.js";
|
||||
|
||||
|
||||
37
src/plugin-sdk/provider-setup.ts
Normal file
37
src/plugin-sdk/provider-setup.ts
Normal file
@ -0,0 +1,37 @@
|
||||
export type {
|
||||
OpenClawPluginApi,
|
||||
ProviderAuthContext,
|
||||
ProviderAuthMethodNonInteractiveContext,
|
||||
ProviderAuthResult,
|
||||
ProviderDiscoveryContext,
|
||||
} from "../plugins/types.js";
|
||||
|
||||
export {
|
||||
applyProviderDefaultModel,
|
||||
configureOpenAICompatibleSelfHostedProviderNonInteractive,
|
||||
discoverOpenAICompatibleSelfHostedProvider,
|
||||
promptAndConfigureOpenAICompatibleSelfHostedProvider,
|
||||
promptAndConfigureOpenAICompatibleSelfHostedProviderAuth,
|
||||
SELF_HOSTED_DEFAULT_CONTEXT_WINDOW,
|
||||
SELF_HOSTED_DEFAULT_COST,
|
||||
SELF_HOSTED_DEFAULT_MAX_TOKENS,
|
||||
} from "../commands/self-hosted-provider-setup.js";
|
||||
export {
|
||||
OLLAMA_DEFAULT_BASE_URL,
|
||||
OLLAMA_DEFAULT_MODEL,
|
||||
configureOllamaNonInteractive,
|
||||
ensureOllamaModelPulled,
|
||||
promptAndConfigureOllama,
|
||||
} from "../commands/ollama-setup.js";
|
||||
export {
|
||||
VLLM_DEFAULT_BASE_URL,
|
||||
VLLM_DEFAULT_CONTEXT_WINDOW,
|
||||
VLLM_DEFAULT_COST,
|
||||
VLLM_DEFAULT_MAX_TOKENS,
|
||||
promptAndConfigureVllm,
|
||||
} from "../commands/vllm-setup.js";
|
||||
export {
|
||||
buildOllamaProvider,
|
||||
buildSglangProvider,
|
||||
buildVllmProvider,
|
||||
} from "../agents/models-config.providers.discovery.js";
|
||||
45
src/plugin-sdk/sandbox.ts
Normal file
45
src/plugin-sdk/sandbox.ts
Normal file
@ -0,0 +1,45 @@
|
||||
export type {
|
||||
CreateSandboxBackendParams,
|
||||
RemoteShellSandboxHandle,
|
||||
RunSshSandboxCommandParams,
|
||||
SandboxBackendCommandParams,
|
||||
SandboxBackendCommandResult,
|
||||
SandboxBackendExecSpec,
|
||||
SandboxBackendFactory,
|
||||
SandboxFsBridge,
|
||||
SandboxFsStat,
|
||||
SandboxBackendHandle,
|
||||
SandboxBackendId,
|
||||
SandboxBackendManager,
|
||||
SandboxBackendRegistration,
|
||||
SandboxBackendRuntimeInfo,
|
||||
SandboxContext,
|
||||
SandboxResolvedPath,
|
||||
SandboxSshConfig,
|
||||
SshSandboxSession,
|
||||
SshSandboxSettings,
|
||||
} from "../agents/sandbox.js";
|
||||
|
||||
export {
|
||||
buildExecRemoteCommand,
|
||||
buildRemoteCommand,
|
||||
buildSshSandboxArgv,
|
||||
createRemoteShellSandboxFsBridge,
|
||||
createSshSandboxSessionFromConfigText,
|
||||
createSshSandboxSessionFromSettings,
|
||||
disposeSshSandboxSession,
|
||||
getSandboxBackendFactory,
|
||||
getSandboxBackendManager,
|
||||
registerSandboxBackend,
|
||||
requireSandboxBackendFactory,
|
||||
runSshSandboxCommand,
|
||||
shellEscape,
|
||||
uploadDirectoryToSshTarget,
|
||||
} from "../agents/sandbox.js";
|
||||
|
||||
export {
|
||||
runPluginCommandWithTimeout,
|
||||
type PluginCommandRunOptions,
|
||||
type PluginCommandRunResult,
|
||||
} from "./run-command.js";
|
||||
export { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
|
||||
@ -11,6 +11,8 @@ import * as imessageSdk from "openclaw/plugin-sdk/imessage";
|
||||
import * as lineSdk from "openclaw/plugin-sdk/line";
|
||||
import * as msteamsSdk from "openclaw/plugin-sdk/msteams";
|
||||
import * as nostrSdk from "openclaw/plugin-sdk/nostr";
|
||||
import * as providerSetupSdk from "openclaw/plugin-sdk/provider-setup";
|
||||
import * as sandboxSdk from "openclaw/plugin-sdk/sandbox";
|
||||
import * as signalSdk from "openclaw/plugin-sdk/signal";
|
||||
import * as slackSdk from "openclaw/plugin-sdk/slack";
|
||||
import * as telegramSdk from "openclaw/plugin-sdk/telegram";
|
||||
@ -46,6 +48,24 @@ describe("plugin-sdk subpath exports", () => {
|
||||
expect(typeof coreSdk.resolveThreadSessionKeys).toBe("function");
|
||||
expect(typeof coreSdk.runPassiveAccountLifecycle).toBe("function");
|
||||
expect(typeof coreSdk.createLoggerBackedRuntime).toBe("function");
|
||||
expect("registerSandboxBackend" in asExports(coreSdk)).toBe(false);
|
||||
expect("promptAndConfigureOpenAICompatibleSelfHostedProviderAuth" in asExports(coreSdk)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("exports provider setup helpers from the dedicated subpath", () => {
|
||||
expect(typeof providerSetupSdk.buildVllmProvider).toBe("function");
|
||||
expect(typeof providerSetupSdk.discoverOpenAICompatibleSelfHostedProvider).toBe("function");
|
||||
expect(typeof providerSetupSdk.promptAndConfigureOpenAICompatibleSelfHostedProviderAuth).toBe(
|
||||
"function",
|
||||
);
|
||||
});
|
||||
|
||||
it("exports sandbox helpers from the dedicated subpath", () => {
|
||||
expect(typeof sandboxSdk.registerSandboxBackend).toBe("function");
|
||||
expect(typeof sandboxSdk.runPluginCommandWithTimeout).toBe("function");
|
||||
expect(typeof sandboxSdk.createRemoteShellSandboxFsBridge).toBe("function");
|
||||
});
|
||||
|
||||
it("exports shared core types used by bundled channels", () => {
|
||||
|
||||
@ -136,6 +136,22 @@ describe("registerPluginCommand", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves Telegram topic command bindings without a Telegram registry entry", () => {
|
||||
expect(
|
||||
__testing.resolveBindingConversationFromCommand({
|
||||
channel: "telegram",
|
||||
from: "telegram:group:-100123",
|
||||
to: "telegram:group:-100123:topic:77",
|
||||
accountId: "default",
|
||||
}),
|
||||
).toEqual({
|
||||
channel: "telegram",
|
||||
accountId: "default",
|
||||
conversationId: "-100123",
|
||||
threadId: 77,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not resolve binding conversations for unsupported command channels", () => {
|
||||
expect(
|
||||
__testing.resolveBindingConversationFromCommand({
|
||||
|
||||
@ -22,8 +22,8 @@ vi.mock("../../../extensions/github-copilot/token.js", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/core", async () => {
|
||||
const actual = await vi.importActual<object>("openclaw/plugin-sdk/core");
|
||||
vi.mock("openclaw/plugin-sdk/provider-setup", async () => {
|
||||
const actual = await vi.importActual<object>("openclaw/plugin-sdk/provider-setup");
|
||||
return {
|
||||
...actual,
|
||||
buildOllamaProvider: (...args: unknown[]) => buildOllamaProviderMock(...args),
|
||||
|
||||
@ -2800,6 +2800,7 @@ module.exports = {
|
||||
|
||||
it("preserves runtime reflection semantics when runtime is lazily initialized", () => {
|
||||
useNoBundledPlugins();
|
||||
const stateDir = makeTempDir();
|
||||
const plugin = writePlugin({
|
||||
id: "runtime-introspection",
|
||||
filename: "runtime-introspection.cjs",
|
||||
@ -2818,12 +2819,17 @@ module.exports = {
|
||||
} };`,
|
||||
});
|
||||
|
||||
const registry = loadRegistryFromSinglePlugin({
|
||||
plugin,
|
||||
pluginConfig: {
|
||||
allow: ["runtime-introspection"],
|
||||
},
|
||||
});
|
||||
const registry = withEnv({ OPENCLAW_STATE_DIR: stateDir }, () =>
|
||||
loadRegistryFromSinglePlugin({
|
||||
plugin,
|
||||
pluginConfig: {
|
||||
allow: ["runtime-introspection"],
|
||||
},
|
||||
options: {
|
||||
onlyPluginIds: ["runtime-introspection"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const record = registry.plugins.find((entry) => entry.id === "runtime-introspection");
|
||||
expect(record?.status).toBe("loaded");
|
||||
|
||||
@ -60,6 +60,21 @@ export type PluginLoadOptions = {
|
||||
const MAX_PLUGIN_REGISTRY_CACHE_ENTRIES = 128;
|
||||
const registryCache = new Map<string, PluginRegistry>();
|
||||
const openAllowlistWarningCache = new Set<string>();
|
||||
const LAZY_RUNTIME_REFLECTION_KEYS = [
|
||||
"version",
|
||||
"config",
|
||||
"subagent",
|
||||
"system",
|
||||
"media",
|
||||
"tts",
|
||||
"stt",
|
||||
"tools",
|
||||
"channel",
|
||||
"events",
|
||||
"logging",
|
||||
"state",
|
||||
"modelAuth",
|
||||
] as const satisfies readonly (keyof PluginRuntime)[];
|
||||
|
||||
export function clearPluginLoaderCache(): void {
|
||||
registryCache.clear();
|
||||
@ -870,6 +885,22 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
resolvedRuntime ??= resolveCreatePluginRuntime()(options.runtimeOptions);
|
||||
return resolvedRuntime;
|
||||
};
|
||||
const lazyRuntimeReflectionKeySet = new Set<PropertyKey>(LAZY_RUNTIME_REFLECTION_KEYS);
|
||||
const resolveLazyRuntimeDescriptor = (prop: PropertyKey): PropertyDescriptor | undefined => {
|
||||
if (!lazyRuntimeReflectionKeySet.has(prop)) {
|
||||
return Reflect.getOwnPropertyDescriptor(resolveRuntime() as object, prop);
|
||||
}
|
||||
return {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get() {
|
||||
return Reflect.get(resolveRuntime() as object, prop);
|
||||
},
|
||||
set(value: unknown) {
|
||||
Reflect.set(resolveRuntime() as object, prop, value);
|
||||
},
|
||||
};
|
||||
};
|
||||
const runtime = new Proxy({} as PluginRuntime, {
|
||||
get(_target, prop, receiver) {
|
||||
return Reflect.get(resolveRuntime(), prop, receiver);
|
||||
@ -878,13 +909,13 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
return Reflect.set(resolveRuntime(), prop, value, receiver);
|
||||
},
|
||||
has(_target, prop) {
|
||||
return Reflect.has(resolveRuntime(), prop);
|
||||
return lazyRuntimeReflectionKeySet.has(prop) || Reflect.has(resolveRuntime(), prop);
|
||||
},
|
||||
ownKeys() {
|
||||
return Reflect.ownKeys(resolveRuntime() as object);
|
||||
return [...LAZY_RUNTIME_REFLECTION_KEYS];
|
||||
},
|
||||
getOwnPropertyDescriptor(_target, prop) {
|
||||
return Reflect.getOwnPropertyDescriptor(resolveRuntime() as object, prop);
|
||||
return resolveLazyRuntimeDescriptor(prop);
|
||||
},
|
||||
defineProperty(_target, prop, attributes) {
|
||||
return Reflect.defineProperty(resolveRuntime() as object, prop, attributes);
|
||||
|
||||
@ -58,6 +58,7 @@ describe("provider-runtime", () => {
|
||||
});
|
||||
|
||||
it("matches providers by alias for runtime hook lookup", () => {
|
||||
resolveOwningPluginIdsForProviderMock.mockReturnValue(["openrouter"]);
|
||||
resolvePluginProvidersMock.mockReturnValue([
|
||||
{
|
||||
id: "openrouter",
|
||||
@ -77,13 +78,35 @@ describe("provider-runtime", () => {
|
||||
);
|
||||
expect(resolvePluginProvidersMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: ["openrouter"],
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("skips plugin loading when the provider has no owning plugin", () => {
|
||||
const plugin = resolveProviderRuntimePlugin({ provider: "anthropic" });
|
||||
|
||||
expect(plugin).toBeUndefined();
|
||||
expect(resolveOwningPluginIdsForProviderMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
provider: "anthropic",
|
||||
}),
|
||||
);
|
||||
expect(resolvePluginProvidersMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("dispatches runtime hooks for the matched provider", async () => {
|
||||
resolveOwningPluginIdsForProviderMock.mockImplementation((params: { provider?: string }) => {
|
||||
if (params.provider === "demo") {
|
||||
return ["demo"];
|
||||
}
|
||||
if (params.provider === "openai") {
|
||||
return ["openai"];
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
const prepareDynamicModel = vi.fn(async () => undefined);
|
||||
const prepareRuntimeAuth = vi.fn(async () => ({
|
||||
apiKey: "runtime-token",
|
||||
|
||||
@ -54,14 +54,18 @@ export function resolveProviderRuntimePlugin(params: {
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): ProviderPlugin | undefined {
|
||||
const owningPluginIds = resolveOwningPluginIdsForProvider({
|
||||
provider: params.provider,
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
if (!owningPluginIds || owningPluginIds.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return resolveProviderPluginsForHooks({
|
||||
...params,
|
||||
onlyPluginIds: resolveOwningPluginIdsForProvider({
|
||||
provider: params.provider,
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
}),
|
||||
onlyPluginIds: owningPluginIds,
|
||||
}).find((plugin) => matchesProviderId(plugin, params.provider));
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user