refactor: break runtime import cycles

This commit is contained in:
Peter Steinberger
2026-04-09 03:51:31 +01:00
parent 0fbaef799f
commit 0c278bb93c
40 changed files with 217 additions and 154 deletions

View File

@@ -2,7 +2,7 @@ import {
createModelCatalogPresetAppliers,
type OpenClawConfig,
} from "openclaw/plugin-sdk/provider-onboard";
import { ARCEE_BASE_URL } from "./api.js";
import { ARCEE_BASE_URL } from "./models.js";
import {
buildArceeCatalogModels,
buildArceeOpenRouterCatalogModels,

View File

@@ -1,5 +1,5 @@
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
import { buildArceeModelDefinition, ARCEE_BASE_URL, ARCEE_MODEL_CATALOG } from "./api.js";
import { buildArceeModelDefinition, ARCEE_BASE_URL, ARCEE_MODEL_CATALOG } from "./models.js";
export const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";

View File

@@ -6,7 +6,7 @@ import { rawDataToString } from "../infra/ws.js";
import { redactSensitiveText } from "../logging/redact.js";
import { getDirectAgentForCdp, withNoProxyForCdpUrl } from "./cdp-proxy-bypass.js";
import { CDP_HTTP_REQUEST_TIMEOUT_MS, CDP_WS_HANDSHAKE_TIMEOUT_MS } from "./cdp-timeouts.js";
import { resolveBrowserRateLimitMessage } from "./client-fetch.js";
import { resolveBrowserRateLimitMessage } from "./rate-limit-message.js";
export { isLoopbackHost };

View File

@@ -9,6 +9,7 @@ import {
createBrowserControlContext,
startBrowserControlServiceFromConfig,
} from "./control-service.js";
import { resolveBrowserRateLimitMessage } from "./rate-limit-message.js";
import { createBrowserRouteDispatcher } from "./routes/dispatcher.js";
// Application-level error from the browser control service (service is reachable
@@ -104,36 +105,10 @@ const BROWSER_TOOL_MODEL_HINT =
"Do NOT retry the browser tool — it will keep failing. " +
"Use an alternative approach or inform the user that the browser is currently unavailable.";
const BROWSER_SERVICE_RATE_LIMIT_MESSAGE =
"Browser service rate limit reached. " +
"Wait for the current session to complete, or retry later.";
const BROWSERBASE_RATE_LIMIT_MESSAGE =
"Browserbase rate limit reached (max concurrent sessions). " +
"Wait for the current session to complete, or upgrade your plan.";
function isRateLimitStatus(status: number): boolean {
return status === 429;
}
function isBrowserbaseUrl(url: string): boolean {
if (!isAbsoluteHttp(url)) {
return false;
}
try {
const host = normalizeLowercaseStringOrEmpty(new URL(url).hostname);
return host === "browserbase.com" || host.endsWith(".browserbase.com");
} catch {
return false;
}
}
export function resolveBrowserRateLimitMessage(url: string): string {
return isBrowserbaseUrl(url)
? BROWSERBASE_RATE_LIMIT_MESSAGE
: BROWSER_SERVICE_RATE_LIMIT_MESSAGE;
}
function resolveBrowserFetchOperatorHint(url: string): string {
const isLocal = !isAbsoluteHttp(url);
return isLocal

View File

@@ -0,0 +1,31 @@
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
const BROWSER_SERVICE_RATE_LIMIT_MESSAGE =
"Browser service rate limit reached. " +
"Wait for the current session to complete, or retry later.";
const BROWSERBASE_RATE_LIMIT_MESSAGE =
"Browserbase rate limit reached (max concurrent sessions). " +
"Wait for the current session to complete, or upgrade your plan.";
function isAbsoluteHttp(url: string): boolean {
return /^https?:\/\//i.test(url.trim());
}
function isBrowserbaseUrl(url: string): boolean {
if (!isAbsoluteHttp(url)) {
return false;
}
try {
const host = normalizeLowercaseStringOrEmpty(new URL(url).hostname);
return host === "browserbase.com" || host.endsWith(".browserbase.com");
} catch {
return false;
}
}
export function resolveBrowserRateLimitMessage(url: string): string {
return isBrowserbaseUrl(url)
? BROWSERBASE_RATE_LIMIT_MESSAGE
: BROWSER_SERVICE_RATE_LIMIT_MESSAGE;
}

View File

@@ -1,17 +1,19 @@
import fsPromises from "node:fs/promises";
import { loadConfig } from "openclaw/plugin-sdk/browser-config-runtime";
import { withTimeout } from "openclaw/plugin-sdk/browser-node-runtime";
import { detectMime } from "openclaw/plugin-sdk/browser-setup-tools";
import { redactCdpUrl } from "../browser/cdp.helpers.js";
import { resolveBrowserConfig } from "../browser/config.js";
import {
isPersistentBrowserProfileMutation,
normalizeBrowserRequestPath,
resolveRequestedBrowserProfile,
} from "../browser/request-policy.js";
import { createBrowserRouteDispatcher } from "../browser/routes/dispatcher.js";
import {
createBrowserControlContext,
createBrowserRouteDispatcher,
detectMime,
isPersistentBrowserProfileMutation,
loadConfig,
normalizeBrowserRequestPath,
redactCdpUrl,
resolveBrowserConfig,
resolveRequestedBrowserProfile,
startBrowserControlServiceFromConfig,
withTimeout,
} from "../core-api.js";
} from "../control-service.js";
type BrowserProxyParams = {
method?: string;

View File

@@ -5,7 +5,7 @@ import {
BYTEPLUS_CODING_BASE_URL,
BYTEPLUS_CODING_MODEL_CATALOG,
BYTEPLUS_MODEL_CATALOG,
} from "./api.js";
} from "./models.js";
export function buildBytePlusProvider(): ModelProviderConfig {
return {

View File

@@ -8,7 +8,7 @@ import {
CHUTES_DEFAULT_MODEL_REF,
CHUTES_MODEL_CATALOG,
buildChutesModelDefinition,
} from "./api.js";
} from "./models.js";
export { CHUTES_DEFAULT_MODEL_REF };

View File

@@ -4,7 +4,7 @@ import {
CHUTES_MODEL_CATALOG,
buildChutesModelDefinition,
discoverChutesModels,
} from "./api.js";
} from "./models.js";
/**
* Build the Chutes provider with dynamic model discovery.

View File

@@ -1,5 +1,9 @@
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
import { buildDeepSeekModelDefinition, DEEPSEEK_BASE_URL, DEEPSEEK_MODEL_CATALOG } from "./api.js";
import {
buildDeepSeekModelDefinition,
DEEPSEEK_BASE_URL,
DEEPSEEK_MODEL_CATALOG,
} from "./models.js";
export function buildDeepSeekProvider(): ModelProviderConfig {
return {

View File

@@ -1,4 +1,3 @@
import type { EventEmitter } from "node:events";
import type { DiscordGatewayHandle } from "./monitor/gateway-handle.js";
import {
DiscordGatewayEvent,
@@ -6,6 +5,8 @@ import {
DiscordGatewaySupervisor,
} from "./monitor/gateway-supervisor.js";
export { getDiscordGatewayEmitter } from "./monitor/gateway-supervisor.js";
export type WaitForDiscordGatewayStopParams = {
gateway?: DiscordGatewayHandle;
abortSignal?: AbortSignal;
@@ -14,10 +15,6 @@ export type WaitForDiscordGatewayStopParams = {
registerForceStop?: (forceStop: (err: unknown) => void) => void;
};
export function getDiscordGatewayEmitter(gateway?: unknown): EventEmitter | undefined {
return (gateway as { emitter?: EventEmitter } | undefined)?.emitter;
}
export async function waitForDiscordGatewayStop(
params: WaitForDiscordGatewayStopParams,
): Promise<void> {

View File

@@ -2,7 +2,6 @@ import type { EventEmitter } from "node:events";
import { danger } from "openclaw/plugin-sdk/runtime-env";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
import { getDiscordGatewayEmitter } from "../monitor.gateway.js";
export type DiscordGatewayEventType =
| "disallowed-intents"
@@ -29,6 +28,10 @@ export class DiscordGatewayLifecycleError extends Error {
}
}
export function getDiscordGatewayEmitter(gateway?: unknown): EventEmitter | undefined {
return (gateway as { emitter?: EventEmitter } | undefined)?.emitter;
}
export type DiscordGatewaySupervisor = {
emitter?: EventEmitter;
attachLifecycle: (handler: (event: DiscordGatewayEvent) => void) => void;

View File

@@ -6,7 +6,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/channel-entry-contra
function registerSlashCommandRoute(api: OpenClawPluginApi): void {
const register = loadBundledEntryExportSync<(api: OpenClawPluginApi) => void>(import.meta.url, {
specifier: "./runtime-api.js",
specifier: "./slash-route-api.js",
exportName: "registerSlashCommandRoute",
});
register(api);

View File

@@ -86,5 +86,4 @@ export {
resolveChannelMediaMaxBytes,
} from "openclaw/plugin-sdk/media-runtime";
export { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
export { registerSlashCommandRoute } from "./src/mattermost/slash-state.js";
export { setMattermostRuntime } from "./src/runtime.js";

View File

@@ -0,0 +1 @@
export { registerSlashCommandRoute } from "./src/mattermost/slash-state.js";

View File

@@ -16,13 +16,13 @@ import {
import { z } from "openclaw/plugin-sdk/zod";
import {
createFixedWindowRateLimiter,
isBlockedHostnameOrIp,
readJsonBodyWithLimit,
requestBodyErrorToText,
} from "../api.js";
} from "../runtime-api.js";
import { publishNostrProfile, getNostrProfileState } from "./channel.js";
import { NostrProfileSchema, type NostrProfile } from "./config-schema.js";
import { importProfileFromRelays, mergeProfiles } from "./nostr-profile-import.js";
import { validateUrlSafety } from "./nostr-profile-url-safety.js";
// ============================================================================
// Types
@@ -103,30 +103,6 @@ async function withPublishLock<T>(accountId: string, fn: () => Promise<T>): Prom
}
}
// ============================================================================
// SSRF Protection
// ============================================================================
function validateUrlSafety(urlStr: string): { ok: true } | { ok: false; error: string } {
try {
const url = new URL(urlStr);
if (url.protocol !== "https:") {
return { ok: false, error: "URL must use https:// protocol" };
}
const hostname = normalizeLowercaseStringOrEmpty(url.hostname);
if (isBlockedHostnameOrIp(hostname)) {
return { ok: false, error: "URL must not point to private/internal addresses" };
}
return { ok: true };
} catch {
return { ok: false, error: "Invalid URL format" };
}
}
// Export for use in import validation
export { validateUrlSafety };

View File

@@ -7,7 +7,7 @@
import { SimplePool, verifyEvent, type Event } from "nostr-tools";
import type { NostrProfile } from "./config-schema.js";
import { validateUrlSafety } from "./nostr-profile-http.js";
import { validateUrlSafety } from "./nostr-profile-url-safety.js";
import { contentToProfile, type ProfileContent } from "./nostr-profile.js";
// ============================================================================

View File

@@ -0,0 +1,22 @@
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import { isBlockedHostnameOrIp } from "../runtime-api.js";
export function validateUrlSafety(urlStr: string): { ok: true } | { ok: false; error: string } {
try {
const url = new URL(urlStr);
if (url.protocol !== "https:") {
return { ok: false, error: "URL must use https:// protocol" };
}
const hostname = normalizeLowercaseStringOrEmpty(url.hostname);
if (isBlockedHostnameOrIp(hostname)) {
return { ok: false, error: "URL must not point to private/internal addresses" };
}
return { ok: true };
} catch {
return { ok: false, error: "Invalid URL format" };
}
}

View File

@@ -15,5 +15,6 @@ export * from "./src/scenario.js";
export * from "./src/scenario-catalog.js";
export * from "./src/self-check-scenario.js";
export * from "./src/self-check.js";
export * from "./src/self-check-runner.js";
export * from "./src/gateway-child.js";
export * from "./src/suite.js";

View File

@@ -0,0 +1,15 @@
import { startQaLabServer } from "./lab-server.js";
export async function runQaLabSelfCheck(params?: { repoRoot?: string; outputPath?: string }) {
const server = await startQaLabServer({
repoRoot: params?.repoRoot,
outputPath: params?.outputPath,
});
try {
return await server.runSelfCheck();
} finally {
await server.stop();
}
}
export const runQaE2eSelfCheck = runQaLabSelfCheck;

View File

@@ -2,7 +2,6 @@ import fs from "node:fs/promises";
import path from "node:path";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import type { QaBusState } from "./bus-state.js";
import { startQaLabServer } from "./lab-server.js";
import { renderQaMarkdownReport } from "./report.js";
import { runQaScenario, type QaScenarioResult } from "./scenario.js";
import { createQaSelfCheckScenario } from "./self-check-scenario.js";
@@ -87,17 +86,3 @@ export async function runQaSelfCheckAgainstState(params: {
scenarioResult,
};
}
export async function runQaLabSelfCheck(params?: { repoRoot?: string; outputPath?: string }) {
const server = await startQaLabServer({
repoRoot: params?.repoRoot,
outputPath: params?.outputPath,
});
try {
return await server.runSelfCheck();
} finally {
await server.stop();
}
}
export const runQaE2eSelfCheck = runQaLabSelfCheck;

View File

@@ -2,7 +2,11 @@ import {
createModelCatalogPresetAppliers,
type OpenClawConfig,
} from "openclaw/plugin-sdk/provider-onboard";
import { buildTogetherModelDefinition, TOGETHER_BASE_URL, TOGETHER_MODEL_CATALOG } from "./api.js";
import {
buildTogetherModelDefinition,
TOGETHER_BASE_URL,
TOGETHER_MODEL_CATALOG,
} from "./models.js";
export const TOGETHER_DEFAULT_MODEL_REF = "together/moonshotai/Kimi-K2.5";

View File

@@ -1,5 +1,9 @@
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
import { buildTogetherModelDefinition, TOGETHER_BASE_URL, TOGETHER_MODEL_CATALOG } from "./api.js";
import {
buildTogetherModelDefinition,
TOGETHER_BASE_URL,
TOGETHER_MODEL_CATALOG,
} from "./models.js";
export function buildTogetherProvider(): ModelProviderConfig {
return {

View File

@@ -6,6 +6,7 @@
*/
import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
import {
createLoggedPairingApprovalNotifier,
@@ -17,7 +18,6 @@ import {
createDefaultChannelRuntimeState,
} from "openclaw/plugin-sdk/status-helpers";
import type { OpenClawConfig } from "../api.js";
import { buildChannelConfigSchema } from "../api.js";
import { twitchMessageActions } from "./actions.js";
import { removeClientManager } from "./client-manager-registry.js";
import { TwitchConfigSchema } from "./config-schema.js";

View File

@@ -1,5 +1,5 @@
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
import { discoverVeniceModels, VENICE_BASE_URL } from "./api.js";
import { discoverVeniceModels, VENICE_BASE_URL } from "./models.js";
export async function buildVeniceProvider(): Promise<ModelProviderConfig> {
const models = await discoverVeniceModels();

View File

@@ -1,5 +1,5 @@
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
import { discoverVercelAiGatewayModels, VERCEL_AI_GATEWAY_BASE_URL } from "./api.js";
import { discoverVercelAiGatewayModels, VERCEL_AI_GATEWAY_BASE_URL } from "./models.js";
export async function buildVercelAiGatewayProvider(): Promise<ModelProviderConfig> {
return {

View File

@@ -5,7 +5,7 @@ import {
DOUBAO_CODING_BASE_URL,
DOUBAO_CODING_MODEL_CATALOG,
DOUBAO_MODEL_CATALOG,
} from "./api.js";
} from "./models.js";
export function buildDoubaoProvider(): ModelProviderConfig {
return {

View File

@@ -3,8 +3,8 @@ import type {
ProviderRuntimeModel,
} from "openclaw/plugin-sdk/plugin-entry";
import { normalizeModelCompat } from "openclaw/plugin-sdk/provider-model-shared";
import { applyXaiModelCompat } from "openclaw/plugin-sdk/provider-tools";
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
import { applyXaiModelCompat } from "./api.js";
import { resolveXaiCatalogEntry, XAI_BASE_URL } from "./model-definitions.js";
const XAI_MODERN_MODEL_PREFIXES = ["grok-3", "grok-4", "grok-code-fast"] as const;

View File

@@ -7,7 +7,7 @@ import type {
} from "../../context-engine/types.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { emitSessionTranscriptUpdate } from "../../sessions/transcript-events.js";
import { getRawSessionAppendMessage } from "../session-tool-result-guard.js";
import { getRawSessionAppendMessage } from "../session-raw-append-message.js";
import { acquireSessionWriteLock } from "../session-write-lock.js";
import { log } from "./logger.js";

View File

@@ -0,0 +1,24 @@
import type { SessionManager } from "@mariozechner/pi-coding-agent";
const RAW_APPEND_MESSAGE = Symbol("openclaw.session.rawAppendMessage");
export type SessionManagerWithRawAppend = SessionManager & {
[RAW_APPEND_MESSAGE]?: SessionManager["appendMessage"];
};
/**
* Return the unguarded appendMessage implementation for a session manager.
*/
export function getRawSessionAppendMessage(
sessionManager: SessionManager,
): SessionManager["appendMessage"] {
const rawAppend = (sessionManager as SessionManagerWithRawAppend)[RAW_APPEND_MESSAGE];
return rawAppend ?? sessionManager.appendMessage.bind(sessionManager);
}
export function setRawSessionAppendMessage(
sessionManager: SessionManager,
appendMessage: SessionManager["appendMessage"],
): void {
(sessionManager as SessionManagerWithRawAppend)[RAW_APPEND_MESSAGE] = appendMessage;
}

View File

@@ -11,14 +11,13 @@ import {
DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS,
truncateToolResultMessage,
} from "./pi-embedded-runner/tool-result-truncation.js";
import {
getRawSessionAppendMessage,
setRawSessionAppendMessage,
} from "./session-raw-append-message.js";
import { createPendingToolCallState } from "./session-tool-result-state.js";
import { makeMissingToolResult, sanitizeToolCallInputs } from "./session-transcript-repair.js";
import { extractToolCallsFromAssistant, extractToolResultId } from "./tool-call-id.js";
const RAW_APPEND_MESSAGE = Symbol("openclaw.session.rawAppendMessage");
type SessionManagerWithRawAppend = SessionManager & {
[RAW_APPEND_MESSAGE]?: SessionManager["appendMessage"];
};
/**
* Truncate oversized text content blocks in a tool result message.
@@ -63,15 +62,7 @@ function normalizePersistedToolResultName(
return toolResult;
}
/**
* Return the unguarded appendMessage implementation for a session manager.
*/
export function getRawSessionAppendMessage(
sessionManager: SessionManager,
): SessionManager["appendMessage"] {
const rawAppend = (sessionManager as SessionManagerWithRawAppend)[RAW_APPEND_MESSAGE];
return rawAppend ?? sessionManager.appendMessage.bind(sessionManager);
}
export { getRawSessionAppendMessage };
export function installSessionToolResultGuard(
sessionManager: SessionManager,
@@ -115,7 +106,7 @@ export function installSessionToolResultGuard(
getPendingIds: () => string[];
} {
const originalAppend = getRawSessionAppendMessage(sessionManager);
(sessionManager as SessionManagerWithRawAppend)[RAW_APPEND_MESSAGE] = originalAppend;
setRawSessionAppendMessage(sessionManager, originalAppend);
const pendingState = createPendingToolCallState();
const persistMessage = (message: AgentMessage) => {
const transformer = opts?.transformMessageForPersistence;

View File

@@ -11,4 +11,4 @@ export { resolveExternalBestEffortDeliveryTarget } from "../infra/outbound/best-
export { createBoundDeliveryRouter } from "../infra/outbound/bound-delivery-router.js";
export { resolveConversationIdFromTargets } from "../infra/outbound/conversation-id.js";
export { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
export { isEmbeddedPiRunActive, queueEmbeddedPiMessage } from "./pi-embedded.js";
export { isEmbeddedPiRunActive, queueEmbeddedPiMessage } from "./pi-embedded-runner/runs.js";

View File

@@ -10,4 +10,4 @@ export {
isEmbeddedPiRunActive,
queueEmbeddedPiMessage,
waitForEmbeddedPiRunEnd,
} from "./pi-embedded.js";
} from "./pi-embedded-runner/runs.js";

View File

@@ -15,7 +15,7 @@ import { formatErrorMessage } from "../infra/errors.js";
import { isSubagentSessionKey, parseAgentSessionKey } from "../routing/session-key.js";
import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js";
import { AGENT_LANE_SUBAGENT } from "./lanes.js";
import { abortEmbeddedPiRun } from "./pi-embedded.js";
import { abortEmbeddedPiRun } from "./pi-embedded-runner/runs.js";
import {
readLatestAssistantReplySnapshot,
waitForAgentRunAndReadUpdatedAssistantReply,

View File

@@ -2,13 +2,15 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js";
const mocks = vi.hoisted(() => ({
applyProviderConfigDefaultsWithPlugin: vi.fn(),
applyProviderConfigDefaultsForConfig: vi.fn(),
}));
vi.mock("../plugins/provider-runtime.js", () => ({
applyProviderConfigDefaultsWithPlugin: (
...args: Parameters<typeof mocks.applyProviderConfigDefaultsWithPlugin>
) => mocks.applyProviderConfigDefaultsWithPlugin(...args),
vi.mock("./provider-policy.js", () => ({
applyProviderConfigDefaultsForConfig: (
...args: Parameters<typeof mocks.applyProviderConfigDefaultsForConfig>
) => mocks.applyProviderConfigDefaultsForConfig(...args),
normalizeProviderConfigForConfigDefaults: (_params: { providerConfig: unknown }) =>
_params.providerConfig,
}));
let applyContextPruningDefaults: typeof import("./defaults.js").applyContextPruningDefaults;
@@ -20,7 +22,7 @@ describe("config defaults", () => {
vi.resetModules();
({ applyAgentDefaults, applyContextPruningDefaults, applyMessageDefaults } =
await import("./defaults.js"));
mocks.applyProviderConfigDefaultsWithPlugin.mockReset();
mocks.applyProviderConfigDefaultsForConfig.mockReset();
});
it("skips provider defaults when agent defaults are absent", () => {
@@ -35,7 +37,7 @@ describe("config defaults", () => {
};
expect(applyContextPruningDefaults(cfg as never)).toBe(cfg);
expect(mocks.applyProviderConfigDefaultsWithPlugin).not.toHaveBeenCalled();
expect(mocks.applyProviderConfigDefaultsForConfig).not.toHaveBeenCalled();
});
it("uses anthropic provider defaults when agent defaults exist", () => {
@@ -53,10 +55,10 @@ describe("config defaults", () => {
},
},
};
mocks.applyProviderConfigDefaultsWithPlugin.mockReturnValue(nextCfg);
mocks.applyProviderConfigDefaultsForConfig.mockReturnValue(nextCfg);
expect(applyContextPruningDefaults(cfg as never)).toBe(nextCfg);
expect(mocks.applyProviderConfigDefaultsWithPlugin).toHaveBeenCalledTimes(1);
expect(mocks.applyProviderConfigDefaultsForConfig).toHaveBeenCalledTimes(1);
});
it("defaults ackReactionScope without deriving other message fields", () => {

View File

@@ -1,8 +1,10 @@
import { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js";
import { normalizeProviderSpecificConfig } from "../agents/models-config.providers.policy.js";
import { normalizeProviderId } from "../agents/provider-id.js";
import { applyProviderConfigDefaultsWithPlugin } from "../plugins/provider-runtime.js";
import { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js";
import {
applyProviderConfigDefaultsForConfig,
normalizeProviderConfigForConfigDefaults,
} from "./provider-policy.js";
import { normalizeTalkConfig } from "./talk.js";
import type { OpenClawConfig } from "./types.js";
import type { ModelDefinitionConfig } from "./types.models.js";
@@ -139,7 +141,10 @@ export function applyModelDefaults(cfg: OpenClawConfig): OpenClawConfig {
if (providerConfig) {
const nextProviders = { ...providerConfig };
for (const [providerId, provider] of Object.entries(providerConfig)) {
const normalizedProvider = normalizeProviderSpecificConfig(providerId, provider);
const normalizedProvider = normalizeProviderConfigForConfigDefaults({
provider: providerId,
providerConfig: provider,
});
const models = normalizedProvider.models;
if (!Array.isArray(models) || models.length === 0) {
if (normalizedProvider !== provider) {
@@ -339,13 +344,10 @@ export function applyContextPruningDefaults(cfg: OpenClawConfig): OpenClawConfig
return cfg;
}
return (
applyProviderConfigDefaultsWithPlugin({
applyProviderConfigDefaultsForConfig({
provider: "anthropic",
context: {
provider: "anthropic",
config: cfg,
env: process.env,
},
config: cfg,
env: process.env,
}) ?? cfg
);
}

View File

@@ -1,21 +1,8 @@
import { describe, expect, it, vi } from "vitest";
import { describe, expect, it } from "vitest";
import { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js";
import { applyModelDefaults } from "./defaults.js";
import type { OpenClawConfig } from "./types.js";
const { normalizeProviderSpecificConfigMock } = vi.hoisted(() => ({
normalizeProviderSpecificConfigMock: vi.fn((providerKey: string, provider: unknown) => {
if (providerKey !== "anthropic" || !provider || typeof provider !== "object") {
return provider;
}
return { ...(provider as Record<string, unknown>), api: "anthropic-messages" };
}),
}));
vi.mock("../agents/models-config.providers.policy.js", () => ({
normalizeProviderSpecificConfig: normalizeProviderSpecificConfigMock,
}));
describe("applyModelDefaults", () => {
function buildProxyProviderConfig(overrides?: { contextWindow?: number; maxTokens?: number }) {
return {

View File

@@ -0,0 +1,27 @@
import { resolveBundledProviderPolicySurface } from "../plugins/provider-public-artifacts.js";
import type { ModelProviderConfig, OpenClawConfig } from "./types.js";
export function normalizeProviderConfigForConfigDefaults(params: {
provider: string;
providerConfig: ModelProviderConfig;
}): ModelProviderConfig {
const normalized = resolveBundledProviderPolicySurface(params.provider)?.normalizeConfig?.({
provider: params.provider,
providerConfig: params.providerConfig,
});
return normalized && normalized !== params.providerConfig ? normalized : params.providerConfig;
}
export function applyProviderConfigDefaultsForConfig(params: {
provider: string;
config: OpenClawConfig;
env: NodeJS.ProcessEnv;
}): OpenClawConfig {
return (
resolveBundledProviderPolicySurface(params.provider)?.applyConfigDefaults?.({
provider: params.provider,
config: params.config,
env: params.env,
}) ?? params.config
);
}

View File

@@ -5,7 +5,6 @@ import {
type TaskAuditSeverity,
type TaskAuditSummary,
} from "./task-registry.audit.shared.js";
import { reconcileInspectableTasks } from "./task-registry.reconcile.js";
import type { TaskRecord } from "./task-registry.types.js";
export type TaskAuditOptions = {
@@ -20,6 +19,12 @@ const DEFAULT_STALE_RUNNING_MS = 30 * 60_000;
export { createEmptyTaskAuditSummary };
export type { TaskAuditCode, TaskAuditFinding, TaskAuditSeverity, TaskAuditSummary };
let taskAuditTaskProvider: () => TaskRecord[] = () => [];
export function configureTaskAuditTaskProvider(provider: () => TaskRecord[]): void {
taskAuditTaskProvider = provider;
}
function createFinding(params: {
severity: TaskAuditSeverity;
code: TaskAuditCode;
@@ -83,7 +88,7 @@ function compareFindings(left: TaskAuditFinding, right: TaskAuditFinding): numbe
}
export function listTaskAuditFindings(options: TaskAuditOptions = {}): TaskAuditFinding[] {
const tasks = options.tasks ?? reconcileInspectableTasks();
const tasks = options.tasks ?? taskAuditTaskProvider();
const now = options.now ?? Date.now();
const staleQueuedMs = options.staleQueuedMs ?? DEFAULT_STALE_QUEUED_MS;
const staleRunningMs = options.staleRunningMs ?? DEFAULT_STALE_RUNNING_MS;

View File

@@ -15,7 +15,11 @@ import {
resolveTaskForLookupToken,
setTaskCleanupAfterById,
} from "./runtime-internal.js";
import { listTaskAuditFindings, summarizeTaskAuditFindings } from "./task-registry.audit.js";
import {
configureTaskAuditTaskProvider,
listTaskAuditFindings,
summarizeTaskAuditFindings,
} from "./task-registry.audit.js";
import type { TaskAuditSummary } from "./task-registry.audit.js";
import { summarizeTaskRecords } from "./task-registry.summary.js";
import type { TaskRecord, TaskRegistrySummary } from "./task-registry.types.js";
@@ -229,6 +233,8 @@ export function reconcileInspectableTasks(): TaskRecord[] {
.map((task) => reconcileTaskRecordForOperatorInspection(task));
}
configureTaskAuditTaskProvider(reconcileInspectableTasks);
export function getInspectableTaskRegistrySummary(): TaskRegistrySummary {
return summarizeTaskRecords(reconcileInspectableTasks());
}