diff --git a/CHANGELOG.md b/CHANGELOG.md index 05806adb4f9..4084a8a4384 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index d89e8c0da0e..7e9f4a75492 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -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 diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index a9dc066c4bb..92c1fcae9f1 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -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 diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index e0a0269ea05..d5693d1f5e8 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -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 diff --git a/docs/reference/token-use.md b/docs/reference/token-use.md index cee0d86ed3c..ab233f5ffda 100644 --- a/docs/reference/token-use.md +++ b/docs/reference/token-use.md @@ -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 diff --git a/src/config/config-misc.test.ts b/src/config/config-misc.test.ts index 217d9995435..4d33ee6abf8 100644 --- a/src/config/config-misc.test.ts +++ b/src/config/config-misc.test.ts @@ -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({ diff --git a/src/config/schema.base.generated.ts b/src/config/schema.base.generated.ts index 455a437ab9a..4610b8ab118 100644 --- a/src/config/schema.base.generated.ts +++ b/src/config/schema.base.generated.ts @@ -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.", diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index d3b4553ae22..9393915c08b 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -818,6 +818,10 @@ export const FIELD_HELP: Record = { '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": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index 4177cd861b1..3d2600a5904 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -515,6 +515,8 @@ export const FIELD_LABELS: Record = { "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 diff --git a/src/config/types.models.ts b/src/config/types.models.ts index 9d33bbe333f..cc7ccb8814e 100644 --- a/src/config/types.models.ts +++ b/src/config/types.models.ts @@ -143,9 +143,14 @@ export type DiscoveryToggleConfig = { enabled?: boolean; }; +export type ModelPricingConfig = { + enabled?: boolean; +}; + export type ModelsConfig = { mode?: "merge" | "replace"; providers?: Record; + 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; diff --git a/src/config/zod-schema.core.ts b/src/config/zod-schema.core.ts index 2fce4021288..3a0fee2450e 100644 --- a/src/config/zod-schema.core.ts +++ b/src/config/zod-schema.core.ts @@ -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(); diff --git a/src/gateway/config-reload-plan.ts b/src/gateway/config-reload-plan.ts index 60ccaaf812c..d3f1563638e 100644 --- a/src/gateway/config-reload-plan.ts +++ b/src/gateway/config-reload-plan.ts @@ -80,6 +80,10 @@ const BASE_RELOAD_RULES: ReloadRule[] = [ kind: "hot", actions: ["restart-heartbeat"], }, + { + prefix: "models.pricing", + kind: "restart", + }, { prefix: "models", kind: "hot", diff --git a/src/gateway/config-reload.test.ts b/src/gateway/config-reload.test.ts index 5fe5368e562..629f469f657 100644 --- a/src/gateway/config-reload.test.ts +++ b/src/gateway/config-reload.test.ts @@ -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); diff --git a/src/gateway/model-pricing-cache.test.ts b/src/gateway/model-pricing-cache.test.ts index c8c2ea43b2c..affa9143f59 100644 --- a/src/gateway/model-pricing-cache.test.ts +++ b/src/gateway/model-pricing-cache.test.ts @@ -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 = { diff --git a/src/gateway/model-pricing-cache.ts b/src/gateway/model-pricing-cache.ts index 4451a39f035..d8d3756d27e 100644 --- a/src/gateway/model-pricing-cache.ts +++ b/src/gateway/model-pricing-cache.ts @@ -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; manifestRegistry?: PluginManifestRegistry; }): Promise { + if (!isGatewayModelPricingEnabled(params.config)) { + clearRefreshTimer(); + return; + } if (inFlightRefresh) { return await inFlightRefresh; } @@ -1250,6 +1255,10 @@ export function startGatewayModelPricingRefresh(params: { pluginLookUpTable?: Pick; manifestRegistry?: PluginManifestRegistry; }): () => void { + if (!isGatewayModelPricingEnabled(params.config)) { + clearRefreshTimer(); + return () => {}; + } let stopped = false; queueMicrotask(() => { if (stopped) { diff --git a/src/gateway/model-pricing-config.ts b/src/gateway/model-pricing-config.ts new file mode 100644 index 00000000000..10a2ad63a21 --- /dev/null +++ b/src/gateway/model-pricing-config.ts @@ -0,0 +1,5 @@ +import type { OpenClawConfig } from "../config/types.openclaw.js"; + +export function isGatewayModelPricingEnabled(config: OpenClawConfig): boolean { + return config.models?.pricing?.enabled !== false; +} diff --git a/src/gateway/server-runtime-services.test.ts b/src/gateway/server-runtime-services.test.ts index 04dcc5e56c6..006921f28ea 100644 --- a/src/gateway/server-runtime-services.test.ts +++ b/src/gateway/server-runtime-services.test.ts @@ -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, diff --git a/src/gateway/server-runtime-services.ts b/src/gateway/server-runtime-services.ts index 230a4f30fe8..1e388304b7d 100644 --- a/src/gateway/server-runtime-services.ts +++ b/src/gateway/server-runtime-services.ts @@ -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; log: GatewayRuntimeServiceLogger; }): () => void { + if (!isGatewayModelPricingEnabled(params.config)) { + return () => {}; + } let stopped = false; let stopRefresh: (() => void) | undefined; void (async () => { diff --git a/src/plugin-sdk/compat.ts b/src/plugin-sdk/compat.ts index 41751a45667..95354b78f56 100644 --- a/src/plugin-sdk/compat.ts +++ b/src/plugin-sdk/compat.ts @@ -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, diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index 1bbb3ebdd1c..9469ca4b4fc 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -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"; diff --git a/src/plugins/contracts/plugin-sdk-index.test.ts b/src/plugins/contracts/plugin-sdk-index.test.ts index ba6c82e250e..c9fde3b6cde 100644 --- a/src/plugins/contracts/plugin-sdk-index.test.ts +++ b/src/plugins/contracts/plugin-sdk-index.test.ts @@ -101,7 +101,9 @@ describe("plugin-sdk exports", () => { "delegateCompactionToRuntime", "emptyPluginConfigSchema", "onDiagnosticEvent", + "optionalStringEnum", "registerContextEngine", + "stringEnum", ]); }); diff --git a/src/plugins/contracts/plugin-sdk-root-alias.test.ts b/src/plugins/contracts/plugin-sdk-root-alias.test.ts index 8e28fff0d74..fa20417892f 100644 --- a/src/plugins/contracts/plugin-sdk-root-alias.test.ts +++ b/src/plugins/contracts/plugin-sdk-root-alias.test.ts @@ -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");