mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:10:45 +00:00
fix: add pricing bootstrap opt-out and sdk compat exports
This commit is contained in:
@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Control UI/WebChat: keep large attachment payloads out of Lit state and optimistic chat messages, using object URL previews plus send-time payload serialization so PDF/image uploads no longer trigger `RangeError: Maximum call stack size exceeded`. Fixes #73360; refs #54378 and #63432. Thanks @hejunhui-73, @Ansub, and @christianhernandez3-afk.
|
||||
- Agents/models: keep per-agent primary models strict when `fallbacks` is omitted, so probe-only custom providers are not tried as hidden fallback candidates unless the agent explicitly opts in. Fixes #73332. Thanks @haumanto.
|
||||
- Gateway/models: add `models.pricing.enabled` so offline or restricted-network installs can skip startup OpenRouter and LiteLLM pricing-catalog fetches while keeping explicit model costs working. Fixes #53639. Thanks @callebtc, @palewire, and @rjdjohnston.
|
||||
- Cron/Telegram: preserve explicit `:topic:` delivery targets over stale session-derived thread IDs when isolated cron announces to Telegram forum topics. Carries forward #59069; refs #49704 and #43808. Thanks @roytong9.
|
||||
- Build/runtime: write the runtime-postbuild stamp after `pnpm build` writes the build stamp, so the next CLI invocation does not re-sync runtime artifacts after a successful build. Fixes #73151. Thanks @bittoby.
|
||||
- CLI/channels: list configured chat channel accounts from read-only setup metadata even when the standalone CLI has not loaded the runtime channel registry, so `openclaw channels list` shows Telegram accounts before auth providers. Fixes #73319 and #73322. Thanks @mlaihk.
|
||||
@@ -26,6 +27,7 @@ Docs: https://docs.openclaw.ai
|
||||
- ACPX: keep generated Codex and Claude ACP wrapper startup paths working when remote or special state filesystems reject chmod, since OpenClaw invokes the wrappers through Node instead of executing them directly. Fixes #73333. Thanks @david-garcia-garcia.
|
||||
- CLI/onboarding: infer image input for common custom-provider vision model IDs, ask only for unknown models, and keep `--custom-image-input`/`--custom-text-input` overrides so vision-capable proxies do not get saved as text-only configs. Fixes #51869. Thanks @Antsoldier1974.
|
||||
- Models/OpenAI Codex: stop listing or resolving unsupported `openai-codex/gpt-5.4-mini` rows through Codex OAuth, keep stale discovery rows suppressed with a clear API-key-route hint, and leave direct `openai/gpt-5.4-mini` available. Fixes #73242. Thanks @0xCyda.
|
||||
- Plugin SDK: restore the root `stringEnum` and `optionalStringEnum` exports on both the published SDK entry and runtime root-alias bridge, so older external plugins can keep building and loading while migrating to focused SDK subpaths. Fixes #68279. Thanks @marzliak.
|
||||
- Plugin SDK: restore the root-alias bridge for `registerContextEngine` and expose missing legacy compat helpers `normalizeAccountId` and `resolvePreferredOpenClawTmpDir` so older external plugins such as `openclaw-weixin` can keep loading while migrating to focused SDK subpaths. Fixes #53497. Thanks @alanxchen85.
|
||||
- Auth profiles: make `openclaw doctor --fix` migrate legacy flat `auth-profiles.json` files such as `{ "ollama-windows": { "apiKey": "ollama-local" } }` to canonical provider default API-key profiles with a backup, so custom Ollama/OpenAI-compatible providers recover cleanly after upgrading. Fixes #59629; supersedes #59642. Thanks @Xsanders555 and @Linux2010.
|
||||
- Memory/Dreaming: retry Dream Diary once with the session default when a configured dreaming model is unavailable, while leaving subagent trust and allowlist errors visible instead of silently masking configuration problems. Refs #67409 and #69209. Thanks @Ghiggins18 and @everySympathy.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
f888e19429506211e4b8b4113594641825d300c0c0a721121092cae2201b721f config-baseline.json
|
||||
481eb68ecf9538d8f6d9808af1a7416b05a3b5d00080552b955a77dbd90819e3 config-baseline.core.json
|
||||
b1d76b9451b21434325e64d5bb531b9b995ba3bbf8f7b1628c09cce18f24c8e2 config-baseline.json
|
||||
58e98b59498060d301104b3772332de5600eb674687b06d0d32a202370709ee0 config-baseline.core.json
|
||||
a9f058ee9616e189dab7fc223e1207a49ae52b8490b8028935c9d0a2b16f81b2 config-baseline.channel.json
|
||||
1f5592bfd141ba1e982ce31763a253c10afb080ab4ea2b6538299b114e29cee1 config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
ffc0924db91ebb9b79c488879bc9938b199047a2577fc469e194af673c9e1303 plugin-sdk-api-baseline.json
|
||||
f2445b07d3ead6c38ab2a37c2e0eccb6414ade36d3fb9eb3dd157e5104f88b0d plugin-sdk-api-baseline.jsonl
|
||||
9a688c953f0108f85f58c173e79c28363d846a592130abec04cafbcabbb22dcc plugin-sdk-api-baseline.json
|
||||
010252e56202abde0816787588239c41b4bfb710b930a5454848a5ae76ad6dae plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -57,6 +57,28 @@ Tool policy, experimental toggles, provider-backed tool config, and custom
|
||||
provider / base-URL setup moved to a dedicated page — see
|
||||
[Configuration — tools and custom providers](/gateway/config-tools).
|
||||
|
||||
## Models
|
||||
|
||||
Provider definitions, model allowlists, and custom provider setup live in
|
||||
[Configuration — tools and custom providers](/gateway/config-tools#custom-providers-and-base-urls).
|
||||
The `models` root also owns global model-catalog behavior.
|
||||
|
||||
```json5
|
||||
{
|
||||
models: {
|
||||
// Optional. Default: true. Requires a Gateway restart when changed.
|
||||
pricing: { enabled: false },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- `models.mode`: provider catalog behavior (`merge` or `replace`).
|
||||
- `models.providers`: custom provider map keyed by provider id.
|
||||
- `models.pricing.enabled`: controls the background pricing bootstrap. When
|
||||
`false`, Gateway startup skips OpenRouter and LiteLLM pricing-catalog fetches;
|
||||
configured `models.providers.*.models[].cost` values still work for local cost
|
||||
estimates.
|
||||
|
||||
## MCP
|
||||
|
||||
OpenClaw-managed MCP server definitions live under `mcp.servers` and are
|
||||
|
||||
@@ -120,6 +120,13 @@ These are **USD per 1M tokens** for `input`, `output`, `cacheRead`, and
|
||||
`cacheWrite`. If pricing is missing, OpenClaw shows tokens only. OAuth tokens
|
||||
never show dollar cost.
|
||||
|
||||
Gateway startup also performs an optional background pricing bootstrap for
|
||||
configured model refs that do not already have local pricing. That bootstrap
|
||||
fetches remote OpenRouter and LiteLLM pricing catalogs. Set
|
||||
`models.pricing.enabled: false` to skip those startup catalog fetches on offline
|
||||
or restricted networks; explicit `models.providers.*.models[].cost` entries
|
||||
continue to drive local cost estimates.
|
||||
|
||||
## Cache TTL and pruning impact
|
||||
|
||||
Provider prompt caching only applies within the cache TTL window. OpenClaw can
|
||||
|
||||
@@ -65,6 +65,28 @@ describe("plugins.slots.contextEngine", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("models.pricing", () => {
|
||||
it("accepts the model pricing bootstrap toggle", () => {
|
||||
for (const enabled of [true, false]) {
|
||||
const result = OpenClawSchema.safeParse({
|
||||
models: {
|
||||
pricing: { enabled },
|
||||
},
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects non-boolean model pricing bootstrap values", () => {
|
||||
const result = OpenClawSchema.safeParse({
|
||||
models: {
|
||||
pricing: { enabled: "false" },
|
||||
},
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("crestodian.rescue", () => {
|
||||
it("accepts documented rescue config", () => {
|
||||
const result = OpenClawSchema.safeParse({
|
||||
|
||||
@@ -3179,6 +3179,21 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
description:
|
||||
"Provider map keyed by provider ID containing connection/auth settings and concrete model definitions. Use stable provider keys so references from agents and tooling remain portable across environments.",
|
||||
},
|
||||
pricing: {
|
||||
type: "object",
|
||||
properties: {
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
title: "Model Pricing Enabled",
|
||||
description:
|
||||
"Enable the background model-pricing bootstrap. Set to false to skip OpenRouter and LiteLLM catalog fetches during Gateway startup; changing this value requires a Gateway restart.",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
title: "Model Pricing",
|
||||
description:
|
||||
"Controls the optional background model-pricing bootstrap that fetches remote per-token cost catalogs.",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
title: "Models",
|
||||
@@ -26592,6 +26607,16 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
help: 'Controls provider catalog behavior: "merge" keeps built-ins and overlays your custom providers, while "replace" uses only your configured providers. In "merge", matching provider IDs preserve non-empty agent models.json baseUrl values, while apiKey values are preserved only when the provider is not SecretRef-managed in current config/auth-profile context; SecretRef-managed providers refresh apiKey from current source markers, and matching model contextWindow/maxTokens use the higher value between explicit and implicit entries.',
|
||||
tags: ["models"],
|
||||
},
|
||||
"models.pricing": {
|
||||
label: "Model Pricing",
|
||||
help: "Controls the optional background model-pricing bootstrap that fetches remote per-token cost catalogs.",
|
||||
tags: ["models"],
|
||||
},
|
||||
"models.pricing.enabled": {
|
||||
label: "Model Pricing Enabled",
|
||||
help: "Enable the background model-pricing bootstrap. Set to false to skip OpenRouter and LiteLLM catalog fetches during Gateway startup; changing this value requires a Gateway restart.",
|
||||
tags: ["models"],
|
||||
},
|
||||
"models.providers": {
|
||||
label: "Model Providers",
|
||||
help: "Provider map keyed by provider ID containing connection/auth settings and concrete model definitions. Use stable provider keys so references from agents and tooling remain portable across environments.",
|
||||
|
||||
@@ -818,6 +818,10 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
'Controls provider catalog behavior: "merge" keeps built-ins and overlays your custom providers, while "replace" uses only your configured providers. In "merge", matching provider IDs preserve non-empty agent models.json baseUrl values, while apiKey values are preserved only when the provider is not SecretRef-managed in current config/auth-profile context; SecretRef-managed providers refresh apiKey from current source markers, and matching model contextWindow/maxTokens use the higher value between explicit and implicit entries.',
|
||||
"models.providers":
|
||||
"Provider map keyed by provider ID containing connection/auth settings and concrete model definitions. Use stable provider keys so references from agents and tooling remain portable across environments.",
|
||||
"models.pricing":
|
||||
"Controls the optional background model-pricing bootstrap that fetches remote per-token cost catalogs.",
|
||||
"models.pricing.enabled":
|
||||
"Enable the background model-pricing bootstrap. Set to false to skip OpenRouter and LiteLLM catalog fetches during Gateway startup; changing this value requires a Gateway restart.",
|
||||
"models.providers.*.baseUrl":
|
||||
"Base URL for the provider endpoint used to serve model requests for that provider entry. Use HTTPS endpoints and keep URLs environment-specific through config templating where needed.",
|
||||
"models.providers.*.apiKey":
|
||||
|
||||
@@ -515,6 +515,8 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"acp.runtime.installCommand": "ACP Runtime Install Command",
|
||||
models: "Models",
|
||||
"models.mode": "Model Catalog Mode",
|
||||
"models.pricing": "Model Pricing",
|
||||
"models.pricing.enabled": "Model Pricing Enabled",
|
||||
"models.providers": "Model Providers",
|
||||
"models.providers.*.baseUrl": "Model Provider Base URL",
|
||||
"models.providers.*.apiKey": "Model Provider API Key", // pragma: allowlist secret
|
||||
|
||||
@@ -143,9 +143,14 @@ export type DiscoveryToggleConfig = {
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export type ModelPricingConfig = {
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export type ModelsConfig = {
|
||||
mode?: "merge" | "replace";
|
||||
providers?: Record<string, ModelProviderConfig>;
|
||||
pricing?: ModelPricingConfig;
|
||||
// Deprecated legacy compat aliases. Kept in the runtime type surface so
|
||||
// doctor/runtime fallbacks can read older configs until migration completes.
|
||||
bedrockDiscovery?: BedrockDiscoveryConfig;
|
||||
|
||||
@@ -381,10 +381,18 @@ export const BedrockDiscoverySchema = z
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
const ModelPricingConfigSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const ModelsConfigSchema = z
|
||||
.object({
|
||||
mode: z.union([z.literal("merge"), z.literal("replace")]).optional(),
|
||||
providers: z.record(z.string(), ModelProviderSchema).optional(),
|
||||
pricing: ModelPricingConfigSchema,
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
@@ -80,6 +80,10 @@ const BASE_RELOAD_RULES: ReloadRule[] = [
|
||||
kind: "hot",
|
||||
actions: ["restart-heartbeat"],
|
||||
},
|
||||
{
|
||||
prefix: "models.pricing",
|
||||
kind: "restart",
|
||||
},
|
||||
{
|
||||
prefix: "models",
|
||||
kind: "hot",
|
||||
|
||||
@@ -245,6 +245,14 @@ describe("buildGatewayReloadPlan", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("requires restart when model pricing bootstrap changes", () => {
|
||||
const plan = buildGatewayReloadPlan(["models.pricing.enabled"]);
|
||||
expect(plan.restartGateway).toBe(true);
|
||||
expect(plan.restartReasons).toContain("models.pricing.enabled");
|
||||
expect(plan.restartHeartbeat).toBe(false);
|
||||
expect(plan.hotReasons).toEqual([]);
|
||||
});
|
||||
|
||||
it("restarts heartbeat when agents.defaults.models allowlist changes", () => {
|
||||
const plan = buildGatewayReloadPlan(["agents.defaults.models"]);
|
||||
expect(plan.restartGateway).toBe(false);
|
||||
|
||||
@@ -761,6 +761,40 @@ describe("model-pricing-cache", () => {
|
||||
stop();
|
||||
});
|
||||
|
||||
it("does not bootstrap remote pricing when pricing is disabled", async () => {
|
||||
const config = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openrouter/moonshotai/kimi-k2.5" },
|
||||
},
|
||||
},
|
||||
models: { pricing: { enabled: false } },
|
||||
} as unknown as OpenClawConfig;
|
||||
const fetchImpl = withFetchPreconnect(vi.fn());
|
||||
|
||||
const stop = startGatewayModelPricingRefresh({ config, fetchImpl });
|
||||
|
||||
await vi.dynamicImportSettled();
|
||||
expect(fetchImpl).not.toHaveBeenCalled();
|
||||
stop();
|
||||
});
|
||||
|
||||
it("does not refresh remote pricing when pricing is disabled", async () => {
|
||||
const config = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openrouter/moonshotai/kimi-k2.5" },
|
||||
},
|
||||
},
|
||||
models: { pricing: { enabled: false } },
|
||||
} as unknown as OpenClawConfig;
|
||||
const fetchImpl = withFetchPreconnect(vi.fn());
|
||||
|
||||
await refreshGatewayModelPricingCache({ config, fetchImpl });
|
||||
|
||||
expect(fetchImpl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("logs configured timeout seconds when pricing fetches time out", async () => {
|
||||
const warnings: string[] = [];
|
||||
loggingState.rawConsole = {
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
type CachedModelPricing,
|
||||
type CachedPricingTier,
|
||||
} from "./model-pricing-cache-state.js";
|
||||
import { isGatewayModelPricingEnabled } from "./model-pricing-config.js";
|
||||
|
||||
type OpenRouterPricingEntry = {
|
||||
id: string;
|
||||
@@ -1105,6 +1106,10 @@ export async function refreshGatewayModelPricingCache(params: {
|
||||
pluginLookUpTable?: Pick<PluginLookUpTable, "index" | "manifestRegistry">;
|
||||
manifestRegistry?: PluginManifestRegistry;
|
||||
}): Promise<void> {
|
||||
if (!isGatewayModelPricingEnabled(params.config)) {
|
||||
clearRefreshTimer();
|
||||
return;
|
||||
}
|
||||
if (inFlightRefresh) {
|
||||
return await inFlightRefresh;
|
||||
}
|
||||
@@ -1250,6 +1255,10 @@ export function startGatewayModelPricingRefresh(params: {
|
||||
pluginLookUpTable?: Pick<PluginLookUpTable, "index" | "manifestRegistry">;
|
||||
manifestRegistry?: PluginManifestRegistry;
|
||||
}): () => void {
|
||||
if (!isGatewayModelPricingEnabled(params.config)) {
|
||||
clearRefreshTimer();
|
||||
return () => {};
|
||||
}
|
||||
let stopped = false;
|
||||
queueMicrotask(() => {
|
||||
if (stopped) {
|
||||
|
||||
5
src/gateway/model-pricing-config.ts
Normal file
5
src/gateway/model-pricing-config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
|
||||
export function isGatewayModelPricingEnabled(config: OpenClawConfig): boolean {
|
||||
return config.models?.pricing?.enabled !== false;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ const hoisted = vi.hoisted(() => {
|
||||
startHeartbeatRunner: vi.fn(() => heartbeatRunner),
|
||||
startChannelHealthMonitor: vi.fn(() => ({ stop: vi.fn() })),
|
||||
startGatewayModelPricingRefresh: vi.fn(() => vi.fn()),
|
||||
loadModelPricingCacheModule: vi.fn(),
|
||||
isVitestRuntimeEnv: vi.fn(() => false),
|
||||
recoverPendingDeliveries: vi.fn(async () => undefined),
|
||||
recoverPendingRestartContinuationDeliveries: vi.fn(async () => undefined),
|
||||
@@ -42,6 +43,10 @@ vi.mock("./channel-health-monitor.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./model-pricing-cache.js", () => ({
|
||||
...(() => {
|
||||
hoisted.loadModelPricingCacheModule();
|
||||
return {};
|
||||
})(),
|
||||
startGatewayModelPricingRefresh: hoisted.startGatewayModelPricingRefresh,
|
||||
}));
|
||||
|
||||
@@ -56,12 +61,31 @@ describe("server-runtime-services", () => {
|
||||
hoisted.startHeartbeatRunner.mockClear();
|
||||
hoisted.startChannelHealthMonitor.mockClear();
|
||||
hoisted.startGatewayModelPricingRefresh.mockClear();
|
||||
hoisted.loadModelPricingCacheModule.mockClear();
|
||||
hoisted.isVitestRuntimeEnv.mockReset().mockReturnValue(false);
|
||||
hoisted.recoverPendingDeliveries.mockClear();
|
||||
hoisted.recoverPendingRestartContinuationDeliveries.mockClear();
|
||||
hoisted.deliverOutboundPayloads.mockClear();
|
||||
});
|
||||
|
||||
it("skips model pricing bootstrap import when pricing is disabled", async () => {
|
||||
startGatewayRuntimeServices({
|
||||
minimalTestGateway: false,
|
||||
cfgAtStart: { models: { pricing: { enabled: false } } } as never,
|
||||
channelManager: {
|
||||
getRuntimeSnapshot: vi.fn(),
|
||||
isHealthMonitorEnabled: vi.fn(),
|
||||
isManuallyStopped: vi.fn(),
|
||||
} as never,
|
||||
log: createLog(),
|
||||
});
|
||||
|
||||
await vi.dynamicImportSettled();
|
||||
|
||||
expect(hoisted.loadModelPricingCacheModule).not.toHaveBeenCalled();
|
||||
expect(hoisted.startGatewayModelPricingRefresh).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps scheduled services inert during initial runtime setup", async () => {
|
||||
const services = startGatewayRuntimeServices({
|
||||
minimalTestGateway: false,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { startHeartbeatRunner, type HeartbeatRunner } from "../infra/heartbeat-r
|
||||
import type { PluginLookUpTable } from "../plugins/plugin-lookup-table.js";
|
||||
import type { ChannelHealthMonitor } from "./channel-health-monitor.js";
|
||||
import { startChannelHealthMonitor } from "./channel-health-monitor.js";
|
||||
import { isGatewayModelPricingEnabled } from "./model-pricing-config.js";
|
||||
|
||||
type GatewayRuntimeServiceLogger = {
|
||||
child: (name: string) => {
|
||||
@@ -93,6 +94,9 @@ function startGatewayModelPricingRefreshOnDemand(params: {
|
||||
pluginLookUpTable?: Pick<PluginLookUpTable, "index" | "manifestRegistry">;
|
||||
log: GatewayRuntimeServiceLogger;
|
||||
}): () => void {
|
||||
if (!isGatewayModelPricingEnabled(params.config)) {
|
||||
return () => {};
|
||||
}
|
||||
let stopped = false;
|
||||
let stopRefresh: (() => void) | undefined;
|
||||
void (async () => {
|
||||
|
||||
@@ -31,6 +31,7 @@ export {
|
||||
export { registerContextEngine } from "../context-engine/registry.js";
|
||||
export type { DiagnosticEventPayload } from "../infra/diagnostic-events.js";
|
||||
export { onDiagnosticEvent } from "../infra/diagnostic-events.js";
|
||||
export { optionalStringEnum, stringEnum } from "../agents/schema/typebox.js";
|
||||
export {
|
||||
applyAuthProfileConfig,
|
||||
buildApiKeyCredential,
|
||||
|
||||
@@ -122,3 +122,4 @@ export {
|
||||
delegateCompactionToRuntime,
|
||||
} from "../context-engine/delegate.js";
|
||||
export { onDiagnosticEvent } from "../infra/diagnostic-events.js";
|
||||
export { optionalStringEnum, stringEnum } from "../agents/schema/typebox.js";
|
||||
|
||||
@@ -101,7 +101,9 @@ describe("plugin-sdk exports", () => {
|
||||
"delegateCompactionToRuntime",
|
||||
"emptyPluginConfigSchema",
|
||||
"onDiagnosticEvent",
|
||||
"optionalStringEnum",
|
||||
"registerContextEngine",
|
||||
"stringEnum",
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -464,6 +464,8 @@ describe("plugin-sdk root alias", () => {
|
||||
expect(typeof rootSdk.delegateCompactionToRuntime).toBe("function");
|
||||
expect(typeof rootSdk.resolveControlCommandGate).toBe("function");
|
||||
expect(typeof rootSdk.onDiagnosticEvent).toBe("function");
|
||||
expect(typeof rootSdk.optionalStringEnum).toBe("function");
|
||||
expect(typeof rootSdk.stringEnum).toBe("function");
|
||||
expect(typeof rootSdk.buildChannelConfigSchema).toBe("function");
|
||||
expect(typeof rootSdk.normalizeAccountId).toBe("function");
|
||||
expect(typeof rootSdk.resolvePreferredOpenClawTmpDir).toBe("function");
|
||||
|
||||
Reference in New Issue
Block a user