diff --git a/docs/plugins/sdk-overview.md b/docs/plugins/sdk-overview.md
index 26e57b1169b..eb5f05c3c59 100644
--- a/docs/plugins/sdk-overview.md
+++ b/docs/plugins/sdk-overview.md
@@ -270,6 +270,13 @@ my-plugin/
`./runtime-api.ts`. The SDK path is the external contract only.
+
+ Extension production code should also avoid `openclaw/plugin-sdk/`
+ imports. If a helper is truly shared, promote it to a neutral SDK subpath
+ such as `openclaw/plugin-sdk/speech`, `.../provider-model-shared`, or another
+ capability-oriented surface instead of coupling two plugins together.
+
+
## Related
- [Entry Points](/plugins/sdk-entrypoints) — `definePluginEntry` and `defineChannelPluginEntry` options
diff --git a/extensions/elevenlabs/speech-provider.ts b/extensions/elevenlabs/speech-provider.ts
index f6ce0e1436b..6bc47072e98 100644
--- a/extensions/elevenlabs/speech-provider.ts
+++ b/extensions/elevenlabs/speech-provider.ts
@@ -5,13 +5,13 @@ import type {
SpeechProviderOverrides,
SpeechProviderPlugin,
SpeechVoiceOption,
-} from "openclaw/plugin-sdk/speech-core";
+} from "openclaw/plugin-sdk/speech";
import {
normalizeApplyTextNormalization,
normalizeLanguageCode,
normalizeSeed,
requireInRange,
-} from "openclaw/plugin-sdk/speech-core";
+} from "openclaw/plugin-sdk/speech";
import { elevenLabsTTS } from "./tts.js";
const DEFAULT_ELEVENLABS_BASE_URL = "https://api.elevenlabs.io";
diff --git a/extensions/elevenlabs/tts.ts b/extensions/elevenlabs/tts.ts
index dff127efbbc..ef69c1c6587 100644
--- a/extensions/elevenlabs/tts.ts
+++ b/extensions/elevenlabs/tts.ts
@@ -3,7 +3,7 @@ import {
normalizeLanguageCode,
normalizeSeed,
requireInRange,
-} from "openclaw/plugin-sdk/speech-core";
+} from "openclaw/plugin-sdk/speech";
const DEFAULT_ELEVENLABS_BASE_URL = "https://api.elevenlabs.io";
diff --git a/extensions/google/image-generation-provider.test.ts b/extensions/google/image-generation-provider.test.ts
index 75716fe1382..16d480d6d0a 100644
--- a/extensions/google/image-generation-provider.test.ts
+++ b/extensions/google/image-generation-provider.test.ts
@@ -1,10 +1,10 @@
-import * as imageGenerationCore from "openclaw/plugin-sdk/image-generation-core";
+import * as providerAuthRuntime from "openclaw/plugin-sdk/provider-auth-runtime";
import { afterEach, describe, expect, it, vi } from "vitest";
import { buildGoogleImageGenerationProvider } from "./image-generation-provider.js";
import { __testing as geminiWebSearchTesting } from "./src/gemini-web-search-provider.js";
function mockGoogleApiKeyAuth() {
- vi.spyOn(imageGenerationCore, "resolveApiKeyForProvider").mockResolvedValue({
+ vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({
apiKey: "google-test-key",
source: "env",
mode: "api-key",
@@ -48,7 +48,7 @@ describe("Google image-generation provider", () => {
});
it("generates image buffers from the Gemini generateContent API", async () => {
- vi.spyOn(imageGenerationCore, "resolveApiKeyForProvider").mockResolvedValue({
+ vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({
apiKey: "google-test-key",
source: "env",
mode: "api-key",
@@ -118,7 +118,7 @@ describe("Google image-generation provider", () => {
});
it("accepts OAuth JSON auth and inline_data responses", async () => {
- vi.spyOn(imageGenerationCore, "resolveApiKeyForProvider").mockResolvedValue({
+ vi.spyOn(providerAuthRuntime, "resolveApiKeyForProvider").mockResolvedValue({
apiKey: JSON.stringify({ token: "oauth-token" }),
source: "profile",
mode: "token",
diff --git a/extensions/google/image-generation-provider.ts b/extensions/google/image-generation-provider.ts
index 33616abc4b8..ae3898d3b32 100644
--- a/extensions/google/image-generation-provider.ts
+++ b/extensions/google/image-generation-provider.ts
@@ -1,5 +1,5 @@
import type { ImageGenerationProvider } from "openclaw/plugin-sdk/image-generation";
-import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/image-generation-core";
+import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
import {
assertOkOrThrowHttpError,
normalizeBaseUrl,
diff --git a/extensions/matrix/src/matrix/sdk/logger.ts b/extensions/matrix/src/matrix/sdk/logger.ts
index 758b0c1e85e..91765122163 100644
--- a/extensions/matrix/src/matrix/sdk/logger.ts
+++ b/extensions/matrix/src/matrix/sdk/logger.ts
@@ -1,5 +1,5 @@
import { format } from "node:util";
-import { redactSensitiveText } from "openclaw/plugin-sdk/diagnostics-otel";
+import { redactSensitiveText } from "openclaw/plugin-sdk/logging-core";
import type { RuntimeLogger } from "openclaw/plugin-sdk/plugin-runtime";
import { getMatrixRuntime } from "../../runtime.js";
diff --git a/extensions/microsoft/speech-provider.ts b/extensions/microsoft/speech-provider.ts
index d4fe6dc74d0..9e0d6db57fa 100644
--- a/extensions/microsoft/speech-provider.ts
+++ b/extensions/microsoft/speech-provider.ts
@@ -5,13 +5,13 @@ import {
TRUSTED_CLIENT_TOKEN,
generateSecMsGecToken,
} from "node-edge-tts/dist/drm.js";
-import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/llm-task";
import { isVoiceCompatibleAudio } from "openclaw/plugin-sdk/media-runtime";
import type {
SpeechProviderConfig,
SpeechProviderPlugin,
SpeechVoiceOption,
-} from "openclaw/plugin-sdk/speech-core";
+} from "openclaw/plugin-sdk/speech";
+import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
import { edgeTTS, inferEdgeExtension } from "./tts.js";
const DEFAULT_EDGE_VOICE = "en-US-MichelleNeural";
diff --git a/extensions/openai/speech-provider.ts b/extensions/openai/speech-provider.ts
index 42a9f835e66..686a669a876 100644
--- a/extensions/openai/speech-provider.ts
+++ b/extensions/openai/speech-provider.ts
@@ -4,7 +4,7 @@ import type {
SpeechProviderConfig,
SpeechProviderOverrides,
SpeechProviderPlugin,
-} from "openclaw/plugin-sdk/speech-core";
+} from "openclaw/plugin-sdk/speech";
import {
DEFAULT_OPENAI_BASE_URL,
isValidOpenAIModel,
diff --git a/extensions/opencode/index.ts b/extensions/opencode/index.ts
index 6be8e688c9a..779c346cb46 100644
--- a/extensions/opencode/index.ts
+++ b/extensions/opencode/index.ts
@@ -1,16 +1,17 @@
-import { isMiniMaxModernModelId } from "openclaw/plugin-sdk/minimax";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
+import { matchesExactOrPrefix } from "openclaw/plugin-sdk/provider-model-shared";
import { applyOpencodeZenConfig, OPENCODE_ZEN_DEFAULT_MODEL } from "./api.js";
const PROVIDER_ID = "opencode";
+const MINIMAX_MODERN_MODEL_MATCHERS = ["minimax-m2.7"] as const;
function isModernOpencodeModel(modelId: string): boolean {
const lower = modelId.trim().toLowerCase();
if (lower.endsWith("-free") || lower === "alpha-glm-4.7") {
return false;
}
- return !isMiniMaxModernModelId(lower);
+ return !matchesExactOrPrefix(lower, MINIMAX_MODERN_MODEL_MATCHERS);
}
export default definePluginEntry({
diff --git a/extensions/venice/index.ts b/extensions/venice/index.ts
index 76edef80b80..6c46ee66ea4 100644
--- a/extensions/venice/index.ts
+++ b/extensions/venice/index.ts
@@ -1,14 +1,34 @@
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
-import { applyXaiModelCompat } from "openclaw/plugin-sdk/xai";
+import { applyModelCompatPatch } from "openclaw/plugin-sdk/provider-model-shared";
+import type { ModelCompatConfig } from "openclaw/plugin-sdk/provider-model-shared";
+import { XAI_UNSUPPORTED_SCHEMA_KEYWORDS } from "openclaw/plugin-sdk/provider-tools";
import { applyVeniceConfig, VENICE_DEFAULT_MODEL_REF } from "./onboard.js";
import { buildVeniceProvider } from "./provider-catalog.js";
const PROVIDER_ID = "venice";
+const XAI_TOOL_SCHEMA_PROFILE = "xai";
+const HTML_ENTITY_TOOL_CALL_ARGUMENTS_ENCODING = "html-entities";
function isXaiBackedVeniceModel(modelId: string): boolean {
return modelId.trim().toLowerCase().includes("grok");
}
+function resolveXaiCompatPatch(): ModelCompatConfig {
+ return {
+ toolSchemaProfile: XAI_TOOL_SCHEMA_PROFILE,
+ unsupportedToolSchemaKeywords: Array.from(XAI_UNSUPPORTED_SCHEMA_KEYWORDS),
+ nativeWebSearchTool: true,
+ toolCallArgumentsEncoding: HTML_ENTITY_TOOL_CALL_ARGUMENTS_ENCODING,
+ };
+}
+
+function applyXaiCompat(model: T): T {
+ return applyModelCompatPatch(
+ model as T & { compat?: ModelCompatConfig },
+ resolveXaiCompatPatch(),
+ ) as T;
+}
+
export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "Venice Provider",
@@ -42,6 +62,6 @@ export default defineSingleProviderPluginEntry({
buildProvider: buildVeniceProvider,
},
normalizeResolvedModel: ({ modelId, model }) =>
- isXaiBackedVeniceModel(modelId) ? applyXaiModelCompat(model) : undefined,
+ isXaiBackedVeniceModel(modelId) ? applyXaiCompat(model) : undefined,
},
});
diff --git a/src/plugin-sdk/channel-import-guardrails.test.ts b/src/plugin-sdk/channel-import-guardrails.test.ts
index 2290da560f0..c6c9d4e0289 100644
--- a/src/plugin-sdk/channel-import-guardrails.test.ts
+++ b/src/plugin-sdk/channel-import-guardrails.test.ts
@@ -13,6 +13,11 @@ const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const REPO_ROOT = resolve(ROOT_DIR, "..");
const ALLOWED_EXTENSION_PUBLIC_SURFACES = new Set(GUARDED_EXTENSION_PUBLIC_SURFACE_BASENAMES);
ALLOWED_EXTENSION_PUBLIC_SURFACES.add("test-api.js");
+const BUNDLED_EXTENSION_IDS = new Set(
+ readdirSync(resolve(REPO_ROOT, "extensions"), { withFileTypes: true })
+ .filter((entry) => entry.isDirectory() && entry.name !== "shared")
+ .map((entry) => entry.name),
+);
const GUARDED_CHANNEL_EXTENSIONS = new Set([
"bluebubbles",
"discord",
@@ -455,6 +460,27 @@ function expectNoSiblingExtensionPrivateSrcImports(file: string, imports: string
}
}
+function expectNoCrossPluginSdkFacadeImports(file: string, imports: string[]): void {
+ const normalizedFile = file.replaceAll("\\", "/");
+ const currentExtensionId =
+ normalizedFile.match(new RegExp(`/${BUNDLED_PLUGIN_ROOT_DIR}/([^/]+)/`))?.[1] ?? null;
+ if (!currentExtensionId) {
+ return;
+ }
+ for (const specifier of imports) {
+ if (!specifier.startsWith("openclaw/plugin-sdk/")) {
+ continue;
+ }
+ const targetSubpath = specifier.slice("openclaw/plugin-sdk/".length);
+ if (!BUNDLED_EXTENSION_IDS.has(targetSubpath) || targetSubpath === currentExtensionId) {
+ continue;
+ }
+ expect.fail(
+ `${file} should not import another bundled plugin facade, got ${specifier}. Promote shared helpers to a neutral plugin-sdk subpath instead.`,
+ );
+ }
+}
+
describe("channel import guardrails", () => {
it("keeps channel helper modules off their own SDK barrels", () => {
for (const source of SAME_CHANNEL_SDK_GUARDS) {
@@ -522,6 +548,12 @@ describe("channel import guardrails", () => {
}
});
+ it("keeps extension production files off other bundled plugin sdk facades", () => {
+ for (const file of collectExtensionSourceFiles()) {
+ expectNoCrossPluginSdkFacadeImports(file, getSourceAnalysis(file).importSpecifiers);
+ }
+ });
+
it("keeps core extension imports limited to approved public surfaces", () => {
for (const file of collectCoreSourceFiles()) {
expectOnlyApprovedExtensionSeams(file, getSourceAnalysis(file).extensionImports);
diff --git a/src/plugin-sdk/logging-core.ts b/src/plugin-sdk/logging-core.ts
index 023c41f73bd..e9a9ec93abf 100644
--- a/src/plugin-sdk/logging-core.ts
+++ b/src/plugin-sdk/logging-core.ts
@@ -1 +1,2 @@
export { createSubsystemLogger } from "../logging/subsystem.js";
+export { redactSensitiveText } from "../logging/redact.js";
diff --git a/src/plugin-sdk/speech.ts b/src/plugin-sdk/speech.ts
index 9033a70af8f..8628da81d16 100644
--- a/src/plugin-sdk/speech.ts
+++ b/src/plugin-sdk/speech.ts
@@ -1,9 +1,40 @@
// Public speech helpers for bundled or third-party plugins.
+//
+// Keep this surface neutral. Provider plugins should not need to know about the
+// bundled `speech-core` plugin id just to consume shared speech types/helpers.
-export { parseTtsDirectives } from "../tts/directives.js";
+export type { SpeechProviderPlugin } from "../plugins/types.js";
export type {
+ SpeechDirectiveTokenParseContext,
+ SpeechDirectiveTokenParseResult,
+ SpeechListVoicesRequest,
SpeechModelOverridePolicy,
+ SpeechProviderConfig,
+ SpeechProviderConfiguredContext,
+ SpeechProviderResolveConfigContext,
+ SpeechProviderResolveTalkConfigContext,
+ SpeechProviderResolveTalkOverridesContext,
+ SpeechProviderOverrides,
+ SpeechSynthesisRequest,
+ SpeechTelephonySynthesisRequest,
SpeechVoiceOption,
TtsDirectiveOverrides,
TtsDirectiveParseResult,
} from "../tts/provider-types.js";
+
+export {
+ scheduleCleanup,
+ summarizeText,
+ normalizeApplyTextNormalization,
+ normalizeLanguageCode,
+ normalizeSeed,
+ requireInRange,
+} from "../tts/tts-core.js";
+export { parseTtsDirectives } from "../tts/directives.js";
+export {
+ canonicalizeSpeechProviderId,
+ getSpeechProvider,
+ listSpeechProviders,
+ normalizeSpeechProviderId,
+} from "../tts/provider-registry.js";
+export { normalizeTtsAutoMode, TTS_AUTO_MODES } from "../tts/tts-auto-mode.js";