mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-01 12:21:25 +00:00
refactor: decouple bundled plugin sdk surfaces
This commit is contained in:
@@ -270,6 +270,13 @@ my-plugin/
|
||||
`./runtime-api.ts`. The SDK path is the external contract only.
|
||||
</Warning>
|
||||
|
||||
<Warning>
|
||||
Extension production code should also avoid `openclaw/plugin-sdk/<other-plugin>`
|
||||
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.
|
||||
</Warning>
|
||||
|
||||
## Related
|
||||
|
||||
- [Entry Points](/plugins/sdk-entrypoints) — `definePluginEntry` and `defineChannelPluginEntry` options
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<T extends { compat?: unknown }>(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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
export { redactSensitiveText } from "../logging/redact.js";
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user