mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
refactor: move provider endpoint metadata into manifests
This commit is contained in:
@@ -128,12 +128,12 @@
|
||||
- Mac packaging (dev): `scripts/package-mac-app.sh` defaults to current arch.
|
||||
- Type-check/build: `pnpm build`
|
||||
- TypeScript checks are split by architecture boundary:
|
||||
- `pnpm tsgo` / `pnpm tsgo:core`: core production graph (`src/`, `ui/`, `packages/`; no `extensions/`).
|
||||
- `pnpm tsgo` / `pnpm tsgo:core`: core production roots (`src/`, `ui/`, `packages/`; no `extensions/` include roots).
|
||||
- `pnpm tsgo:core:test`: core colocated tests.
|
||||
- `pnpm tsgo:extensions`: bundled extension production graph.
|
||||
- `pnpm tsgo:extensions:test`: bundled extension colocated tests.
|
||||
- `pnpm tsgo:all`: every TypeScript graph above; this is what `pnpm check` runs.
|
||||
- `pnpm tsgo:profile [core-test|extensions-test|--all]`: profile fresh graph cost into `.artifacts/tsgo-profile/`.
|
||||
- `pnpm tsgo:profile [core-test|extensions-test|--all]`: profile fresh graph cost into `.artifacts/tsgo-profile/`; if a core graph lists `extensions/<id>/`, treat that as boundary/perf debt from imports (usually plugin-sdk facades or shared helpers pulling extension sources).
|
||||
- Narrow aliases remain for local loops: `pnpm tsgo:test:src`, `pnpm tsgo:test:ui`, `pnpm tsgo:test:packages`.
|
||||
- Do not add `tsc --noEmit`, `typecheck`, or `check:types` lanes for repo type checking. Use `tsgo` graphs. `tsc` is allowed only when emitting declaration/package-boundary compatibility artifacts that `tsgo` does not replace.
|
||||
- Boundary rule: core must not know extension implementation details. Extensions hook into core through manifests, registries, capabilities, and public `openclaw/plugin-sdk/*` contracts. If you find core production code naming a specific extension, or a core test that is really testing extension-owned behavior, call it out and prefer moving coverage/logic to the owning extension or a generic contract test.
|
||||
|
||||
@@ -95,6 +95,12 @@ Those belong in your plugin code and `package.json`.
|
||||
"modelSupport": {
|
||||
"modelPrefixes": ["router-"]
|
||||
},
|
||||
"providerEndpoints": [
|
||||
{
|
||||
"endpointClass": "xai-native",
|
||||
"hosts": ["api.x.ai"]
|
||||
}
|
||||
],
|
||||
"cliBackends": ["openrouter-cli"],
|
||||
"syntheticAuthRefs": ["openrouter-cli"],
|
||||
"providerAuthEnvVars": {
|
||||
@@ -153,6 +159,7 @@ Those belong in your plugin code and `package.json`.
|
||||
| `channels` | No | `string[]` | Channel ids owned by this plugin. Used for discovery and config validation. |
|
||||
| `providers` | No | `string[]` | Provider ids owned by this plugin. |
|
||||
| `modelSupport` | No | `object` | Manifest-owned shorthand model-family metadata used to auto-load the plugin before runtime. |
|
||||
| `providerEndpoints` | No | `object[]` | Manifest-owned endpoint host/baseUrl metadata for provider routes that core must classify before provider runtime loads. |
|
||||
| `cliBackends` | No | `string[]` | CLI inference backend ids owned by this plugin. Used for startup auto-activation from explicit config refs. |
|
||||
| `syntheticAuthRefs` | No | `string[]` | Provider or CLI backend refs whose plugin-owned synthetic auth hook should be probed during cold model discovery before runtime loads. |
|
||||
| `nonSecretAuthMarkers` | No | `string[]` | Bundled-plugin-owned placeholder API key values that represent non-secret local, OAuth, or ambient credential state. |
|
||||
@@ -602,6 +609,9 @@ See [Configuration reference](/gateway/configuration) for the full `plugins.*` s
|
||||
- `providerAuthAliases` lets provider variants reuse another provider's auth
|
||||
env vars, auth profiles, config-backed auth, and API-key onboarding choice
|
||||
without hardcoding that relationship in core.
|
||||
- `providerEndpoints` lets provider plugins own simple endpoint host/baseUrl
|
||||
matching metadata. Use it only for endpoint classes core already supports;
|
||||
the plugin still owns runtime behavior.
|
||||
- `syntheticAuthRefs` is the cheap metadata path for provider-owned synthetic
|
||||
auth hooks that must be visible to cold model discovery before the runtime
|
||||
registry exists. Only list refs whose runtime provider or CLI backend actually
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
getModelProviderHint,
|
||||
normalizeNativeXaiModelId,
|
||||
normalizeProviderId,
|
||||
resolveProviderEndpoint,
|
||||
} from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import {
|
||||
applyXaiModelCompat,
|
||||
@@ -30,9 +29,19 @@ export {
|
||||
resolveXaiModelCompatPatch,
|
||||
} from "openclaw/plugin-sdk/provider-tools";
|
||||
|
||||
const XAI_NATIVE_ENDPOINT_HOSTS = new Set(["api.x.ai", "api.grok.x.ai"]);
|
||||
|
||||
function resolveHostname(value: string): string | undefined {
|
||||
try {
|
||||
return new URL(value).hostname.toLowerCase();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function isXaiNativeEndpoint(baseUrl: unknown): boolean {
|
||||
return (
|
||||
typeof baseUrl === "string" && resolveProviderEndpoint(baseUrl).endpointClass === "xai-native"
|
||||
typeof baseUrl === "string" && XAI_NATIVE_ENDPOINT_HOSTS.has(resolveHostname(baseUrl) ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
"id": "xai",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["xai"],
|
||||
"providerEndpoints": [
|
||||
{
|
||||
"endpointClass": "xai-native",
|
||||
"hosts": ["api.x.ai", "api.grok.x.ai"]
|
||||
}
|
||||
],
|
||||
"syntheticAuthRefs": ["xai"],
|
||||
"providerAuthEnvVars": {
|
||||
"xai": ["XAI_API_KEY"]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import {
|
||||
normalizeOptionalLowercaseString,
|
||||
normalizeOptionalString,
|
||||
@@ -131,6 +132,7 @@ const OPENAI_RESPONSES_APIS = new Set([
|
||||
]);
|
||||
const OPENAI_RESPONSES_PROVIDERS = new Set(["openai", "azure-openai", "azure-openai-responses"]);
|
||||
const MOONSHOT_COMPAT_PROVIDERS = new Set(["moonshot", "kimi"]);
|
||||
const MANIFEST_PROVIDER_ENDPOINT_CLASSES = new Set<ProviderEndpointClass>(["xai-native"]);
|
||||
|
||||
function formatOpenClawUserAgent(version: string): string {
|
||||
return `${OPENCLAW_ATTRIBUTION_ORIGINATOR}/${version}`;
|
||||
@@ -186,6 +188,36 @@ function normalizeComparableBaseUrl(value: string): string | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
function isManifestProviderEndpointClass(value: string): value is ProviderEndpointClass {
|
||||
return MANIFEST_PROVIDER_ENDPOINT_CLASSES.has(value as ProviderEndpointClass);
|
||||
}
|
||||
|
||||
function resolveManifestProviderEndpoint(params: {
|
||||
host: string;
|
||||
normalizedBaseUrl?: string;
|
||||
}): ProviderEndpointResolution | undefined {
|
||||
const registry = loadPluginManifestRegistry({ cache: true });
|
||||
for (const plugin of registry.plugins) {
|
||||
for (const endpoint of plugin.providerEndpoints ?? []) {
|
||||
if (!isManifestProviderEndpointClass(endpoint.endpointClass)) {
|
||||
continue;
|
||||
}
|
||||
if (endpoint.hosts?.some((host) => host.toLowerCase() === params.host)) {
|
||||
return { endpointClass: endpoint.endpointClass, hostname: params.host };
|
||||
}
|
||||
if (
|
||||
params.normalizedBaseUrl &&
|
||||
endpoint.baseUrls?.some(
|
||||
(baseUrl) => normalizeComparableBaseUrl(baseUrl) === params.normalizedBaseUrl,
|
||||
)
|
||||
) {
|
||||
return { endpointClass: endpoint.endpointClass, hostname: params.host };
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isLocalEndpointHost(host: string): boolean {
|
||||
return (
|
||||
LOCAL_ENDPOINT_HOSTS.has(host) ||
|
||||
@@ -246,9 +278,6 @@ export function resolveProviderEndpoint(
|
||||
if (host === "openrouter.ai" || host.endsWith(".openrouter.ai")) {
|
||||
return { endpointClass: "openrouter", hostname: host };
|
||||
}
|
||||
if (host === "api.x.ai" || host === "api.grok.x.ai") {
|
||||
return { endpointClass: "xai-native", hostname: host };
|
||||
}
|
||||
if (host === "api.z.ai") {
|
||||
return { endpointClass: "zai-native", hostname: host };
|
||||
}
|
||||
@@ -273,6 +302,10 @@ export function resolveProviderEndpoint(
|
||||
googleVertexRegion: googleVertexHost[1],
|
||||
};
|
||||
}
|
||||
const manifestEndpoint = resolveManifestProviderEndpoint({ host, normalizedBaseUrl });
|
||||
if (manifestEndpoint) {
|
||||
return manifestEndpoint;
|
||||
}
|
||||
if (isLocalEndpointHost(host)) {
|
||||
return { endpointClass: "local", hostname: host };
|
||||
}
|
||||
|
||||
@@ -60,27 +60,34 @@ vi.mock("../plugins/provider-oauth-flow.js", () => ({
|
||||
createVpsAwareOAuthHandlers,
|
||||
}));
|
||||
|
||||
const LOCAL_PROVIDER_ID = "local-provider";
|
||||
const LOCAL_PROVIDER_LABEL = "Local Provider";
|
||||
const LOCAL_AUTH_METHOD_ID = "local";
|
||||
const LOCAL_PROFILE_ID = `${LOCAL_PROVIDER_ID}:default`;
|
||||
const LOCAL_API_KEY = "local-provider-key";
|
||||
const LOCAL_DEFAULT_MODEL = `${LOCAL_PROVIDER_ID}/demo-model`;
|
||||
|
||||
function buildProvider(): ProviderPlugin {
|
||||
return {
|
||||
id: "ollama",
|
||||
label: "Ollama",
|
||||
id: LOCAL_PROVIDER_ID,
|
||||
label: LOCAL_PROVIDER_LABEL,
|
||||
auth: [
|
||||
{
|
||||
id: "local",
|
||||
label: "Ollama",
|
||||
id: LOCAL_AUTH_METHOD_ID,
|
||||
label: LOCAL_PROVIDER_LABEL,
|
||||
kind: "custom",
|
||||
run: async () => ({
|
||||
profiles: [
|
||||
{
|
||||
profileId: "ollama:default",
|
||||
profileId: LOCAL_PROFILE_ID,
|
||||
credential: {
|
||||
type: "api_key",
|
||||
provider: "ollama",
|
||||
key: "ollama-local",
|
||||
provider: LOCAL_PROVIDER_ID,
|
||||
key: LOCAL_API_KEY,
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultModel: "ollama/qwen3:4b",
|
||||
defaultModel: LOCAL_DEFAULT_MODEL,
|
||||
}),
|
||||
},
|
||||
],
|
||||
@@ -89,7 +96,7 @@ function buildProvider(): ProviderPlugin {
|
||||
|
||||
function buildParams(overrides: Partial<ApplyAuthChoiceParams> = {}): ApplyAuthChoiceParams {
|
||||
return {
|
||||
authChoice: "ollama",
|
||||
authChoice: LOCAL_PROVIDER_ID,
|
||||
config: {},
|
||||
prompter: {
|
||||
note: vi.fn(async () => {}),
|
||||
@@ -122,41 +129,41 @@ describe("applyAuthChoiceLoadedPluginProvider", () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
config: {},
|
||||
agentModelOverride: "ollama/qwen3:4b",
|
||||
agentModelOverride: LOCAL_DEFAULT_MODEL,
|
||||
});
|
||||
expect(runProviderModelSelectedHook).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps provider config patches when default model application is deferred", async () => {
|
||||
const provider: ProviderPlugin = {
|
||||
id: "moonshot",
|
||||
label: "Moonshot",
|
||||
id: "remote-alpha",
|
||||
label: "Remote Alpha",
|
||||
auth: [
|
||||
{
|
||||
id: "api-key-cn",
|
||||
label: "Moonshot API key (.cn)",
|
||||
id: "api-key",
|
||||
label: "Remote Alpha API key",
|
||||
kind: "api_key",
|
||||
run: async () => ({
|
||||
profiles: [
|
||||
{
|
||||
profileId: "moonshot:default",
|
||||
profileId: "remote-alpha:default",
|
||||
credential: {
|
||||
type: "api_key",
|
||||
provider: "moonshot",
|
||||
key: "sk-moonshot-cn-test",
|
||||
provider: "remote-alpha",
|
||||
key: "sk-remote-alpha-test",
|
||||
},
|
||||
},
|
||||
],
|
||||
configPatch: {
|
||||
models: {
|
||||
providers: {
|
||||
moonshot: {
|
||||
"remote-alpha": {
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://api.moonshot.cn/v1",
|
||||
baseUrl: "https://api.remote-alpha.example/v1",
|
||||
models: [
|
||||
{
|
||||
id: "kimi-k2.5",
|
||||
name: "kimi-k2.5",
|
||||
id: "alpha-large",
|
||||
name: "alpha-large",
|
||||
input: ["text", "image"],
|
||||
reasoning: true,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
@@ -168,7 +175,7 @@ describe("applyAuthChoiceLoadedPluginProvider", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultModel: "moonshot/kimi-k2.5",
|
||||
defaultModel: "remote-alpha/alpha-large",
|
||||
}),
|
||||
},
|
||||
],
|
||||
@@ -192,18 +199,22 @@ describe("applyAuthChoiceLoadedPluginProvider", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result?.agentModelOverride).toBe("moonshot/kimi-k2.5");
|
||||
expect(result?.agentModelOverride).toBe("remote-alpha/alpha-large");
|
||||
expect(result?.config.agents?.defaults?.model).toEqual({
|
||||
primary: "anthropic/claude-opus-4-6",
|
||||
});
|
||||
expect(result?.config.models?.providers?.moonshot?.baseUrl).toBe("https://api.moonshot.cn/v1");
|
||||
expect(result?.config.models?.providers?.moonshot?.models?.[0]?.input).toContain("image");
|
||||
expect(result?.config.models?.providers?.["remote-alpha"]?.baseUrl).toBe(
|
||||
"https://api.remote-alpha.example/v1",
|
||||
);
|
||||
expect(result?.config.models?.providers?.["remote-alpha"]?.models?.[0]?.input).toContain(
|
||||
"image",
|
||||
);
|
||||
expect(upsertAuthProfile).toHaveBeenCalledWith({
|
||||
profileId: "moonshot:default",
|
||||
profileId: "remote-alpha:default",
|
||||
credential: {
|
||||
type: "api_key",
|
||||
provider: "moonshot",
|
||||
key: "sk-moonshot-cn-test",
|
||||
provider: "remote-alpha",
|
||||
key: "sk-remote-alpha-test",
|
||||
},
|
||||
agentDir: "/tmp/agent",
|
||||
});
|
||||
@@ -221,20 +232,20 @@ describe("applyAuthChoiceLoadedPluginProvider", () => {
|
||||
const result = await applyAuthChoiceLoadedPluginProvider(buildParams());
|
||||
|
||||
expect(result?.config.agents?.defaults?.model).toEqual({
|
||||
primary: "ollama/qwen3:4b",
|
||||
primary: LOCAL_DEFAULT_MODEL,
|
||||
});
|
||||
expect(upsertAuthProfile).toHaveBeenCalledWith({
|
||||
profileId: "ollama:default",
|
||||
profileId: LOCAL_PROFILE_ID,
|
||||
credential: {
|
||||
type: "api_key",
|
||||
provider: "ollama",
|
||||
key: "ollama-local",
|
||||
provider: LOCAL_PROVIDER_ID,
|
||||
key: LOCAL_API_KEY,
|
||||
},
|
||||
agentDir: "/tmp/agent",
|
||||
});
|
||||
expect(runProviderModelSelectedHook).toHaveBeenCalledWith({
|
||||
config: result?.config,
|
||||
model: "ollama/qwen3:4b",
|
||||
model: LOCAL_DEFAULT_MODEL,
|
||||
prompter: expect.objectContaining({ note: expect.any(Function) }),
|
||||
agentDir: undefined,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
@@ -270,27 +281,27 @@ describe("applyAuthChoiceLoadedPluginProvider", () => {
|
||||
run: async () => ({
|
||||
profiles: [
|
||||
{
|
||||
profileId: "ollama:default",
|
||||
profileId: LOCAL_PROFILE_ID,
|
||||
credential: {
|
||||
type: "api_key",
|
||||
provider: "ollama",
|
||||
key: "ollama-local",
|
||||
provider: LOCAL_PROVIDER_ID,
|
||||
key: LOCAL_API_KEY,
|
||||
},
|
||||
},
|
||||
],
|
||||
configPatch: {
|
||||
models: {
|
||||
providers: {
|
||||
ollama: {
|
||||
api: "ollama",
|
||||
baseUrl: "http://127.0.0.1:11434",
|
||||
[LOCAL_PROVIDER_ID]: {
|
||||
api: "openai-completions",
|
||||
baseUrl: "http://127.0.0.1:4000/v1",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultModel: "ollama/qwen3:4b",
|
||||
notes: ["Detected local Ollama runtime.", "Pulled model metadata."],
|
||||
defaultModel: LOCAL_DEFAULT_MODEL,
|
||||
notes: ["Detected local provider runtime.", "Pulled model metadata."],
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -309,18 +320,18 @@ describe("applyAuthChoiceLoadedPluginProvider", () => {
|
||||
method,
|
||||
});
|
||||
|
||||
expect(result.defaultModel).toBe("ollama/qwen3:4b");
|
||||
expect(result.config.models?.providers?.ollama).toEqual({
|
||||
api: "ollama",
|
||||
baseUrl: "http://127.0.0.1:11434",
|
||||
expect(result.defaultModel).toBe(LOCAL_DEFAULT_MODEL);
|
||||
expect(result.config.models?.providers?.[LOCAL_PROVIDER_ID]).toEqual({
|
||||
api: "openai-completions",
|
||||
baseUrl: "http://127.0.0.1:4000/v1",
|
||||
models: [],
|
||||
});
|
||||
expect(result.config.auth?.profiles?.["ollama:default"]).toEqual({
|
||||
provider: "ollama",
|
||||
expect(result.config.auth?.profiles?.[LOCAL_PROFILE_ID]).toEqual({
|
||||
provider: LOCAL_PROVIDER_ID,
|
||||
mode: "api_key",
|
||||
});
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
"Detected local Ollama runtime.\nPulled model metadata.",
|
||||
"Detected local provider runtime.\nPulled model metadata.",
|
||||
"Provider notes",
|
||||
);
|
||||
});
|
||||
@@ -392,7 +403,7 @@ describe("applyAuthChoiceLoadedPluginProvider", () => {
|
||||
const note = vi.fn(async () => {});
|
||||
const result = await applyAuthChoicePluginProvider(
|
||||
buildParams({
|
||||
authChoice: "provider-plugin:ollama:local",
|
||||
authChoice: `provider-plugin:${LOCAL_PROVIDER_ID}:${LOCAL_AUTH_METHOD_ID}`,
|
||||
agentId: "worker",
|
||||
setDefaultModel: false,
|
||||
prompter: {
|
||||
@@ -400,25 +411,25 @@ describe("applyAuthChoiceLoadedPluginProvider", () => {
|
||||
} as unknown as ApplyAuthChoiceParams["prompter"],
|
||||
}),
|
||||
{
|
||||
authChoice: "provider-plugin:ollama:local",
|
||||
pluginId: "ollama",
|
||||
providerId: "ollama",
|
||||
methodId: "local",
|
||||
label: "Ollama",
|
||||
authChoice: `provider-plugin:${LOCAL_PROVIDER_ID}:${LOCAL_AUTH_METHOD_ID}`,
|
||||
pluginId: LOCAL_PROVIDER_ID,
|
||||
providerId: LOCAL_PROVIDER_ID,
|
||||
methodId: LOCAL_AUTH_METHOD_ID,
|
||||
label: LOCAL_PROVIDER_LABEL,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result?.agentModelOverride).toBe("ollama/qwen3:4b");
|
||||
expect(result?.agentModelOverride).toBe(LOCAL_DEFAULT_MODEL);
|
||||
expect(result?.config.plugins).toEqual({
|
||||
entries: {
|
||||
ollama: {
|
||||
[LOCAL_PROVIDER_ID]: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(runProviderModelSelectedHook).not.toHaveBeenCalled();
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
'Default model set to ollama/qwen3:4b for agent "worker".',
|
||||
`Default model set to ${LOCAL_DEFAULT_MODEL} for agent "worker".`,
|
||||
"Model configured",
|
||||
);
|
||||
});
|
||||
@@ -438,10 +449,10 @@ describe("applyAuthChoiceLoadedPluginProvider", () => {
|
||||
} as unknown as ApplyAuthChoiceParams["prompter"],
|
||||
}),
|
||||
{
|
||||
authChoice: "ollama",
|
||||
pluginId: "ollama",
|
||||
providerId: "ollama",
|
||||
label: "Ollama",
|
||||
authChoice: LOCAL_PROVIDER_ID,
|
||||
pluginId: LOCAL_PROVIDER_ID,
|
||||
providerId: LOCAL_PROVIDER_ID,
|
||||
label: LOCAL_PROVIDER_LABEL,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -453,6 +464,9 @@ describe("applyAuthChoiceLoadedPluginProvider", () => {
|
||||
},
|
||||
});
|
||||
expect(resolvePluginProviders).not.toHaveBeenCalled();
|
||||
expect(note).toHaveBeenCalledWith("Ollama plugin is disabled (plugins disabled).", "Ollama");
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
"Local Provider plugin is disabled (plugins disabled).",
|
||||
LOCAL_PROVIDER_LABEL,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,17 @@ const forbiddenOllamaFacadeFiles = [
|
||||
"src/plugin-sdk/ollama.ts",
|
||||
"src/plugin-sdk/ollama-runtime.ts",
|
||||
] as const;
|
||||
const genericCoreFixtureFiles = [
|
||||
"src/commands/auth-choice.apply.plugin-provider.test.ts",
|
||||
"src/plugins/contracts/memory-embedding-provider.contract.test.ts",
|
||||
"src/plugins/discovery.test.ts",
|
||||
"test/helpers/plugins/tts-contract-suites.ts",
|
||||
] as const;
|
||||
const forbiddenGenericFixtureTerms = [
|
||||
/\bOllama\b|\bollama\b/u,
|
||||
/\bMoonshot\b|\bmoonshot\b/u,
|
||||
/\bxAI\b|\bxai\b|\bx-ai\b/u,
|
||||
] as const;
|
||||
const importSpecifierPattern =
|
||||
/\b(?:import|export)\s+(?:type\s+)?(?:[^'"]*?\s+from\s+)?["']([^"']+)["']|import\(\s*["']([^"']+)["']\s*\)/g;
|
||||
|
||||
@@ -54,4 +65,18 @@ describe("core extension facade boundary", () => {
|
||||
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps generic core fixtures free of bundled provider names", () => {
|
||||
const violations: string[] = [];
|
||||
for (const file of genericCoreFixtureFiles) {
|
||||
const source = fs.readFileSync(path.join(repoRoot, file), "utf8");
|
||||
for (const pattern of forbiddenGenericFixtureTerms) {
|
||||
if (pattern.test(source)) {
|
||||
violations.push(`${file} matches ${String(pattern)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,22 +40,22 @@ describe("memory embedding provider registration", () => {
|
||||
registerVirtualTestPlugin({
|
||||
registry,
|
||||
config,
|
||||
id: "ollama",
|
||||
name: "Ollama",
|
||||
id: "external-vector",
|
||||
name: "External Vector",
|
||||
contracts: {
|
||||
memoryEmbeddingProviders: ["ollama"],
|
||||
memoryEmbeddingProviders: ["external-vector"],
|
||||
},
|
||||
register(api) {
|
||||
api.registerMemoryEmbeddingProvider({
|
||||
id: "ollama",
|
||||
id: "external-vector",
|
||||
create: async () => ({ provider: null }),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
expect(getRegisteredMemoryEmbeddingProvider("ollama")).toEqual({
|
||||
adapter: expect.objectContaining({ id: "ollama" }),
|
||||
ownerPluginId: "ollama",
|
||||
expect(getRegisteredMemoryEmbeddingProvider("external-vector")).toEqual({
|
||||
adapter: expect.objectContaining({ id: "external-vector" }),
|
||||
ownerPluginId: "external-vector",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -497,17 +497,17 @@ describe("discoverOpenClawPlugins", () => {
|
||||
{
|
||||
name: "strips provider suffixes from package-derived ids",
|
||||
setup: (stateDir: string) => {
|
||||
const packageDir = path.join(stateDir, "extensions", "ollama-provider-pack");
|
||||
const packageDir = path.join(stateDir, "extensions", "local-provider-pack");
|
||||
createPackagePluginWithEntry({
|
||||
packageDir,
|
||||
packageName: "@openclaw/ollama-provider",
|
||||
pluginId: "ollama",
|
||||
packageName: "@example/local-provider",
|
||||
pluginId: "local",
|
||||
entryPath: "src/index.ts",
|
||||
});
|
||||
return {};
|
||||
},
|
||||
includes: ["ollama"],
|
||||
excludes: ["ollama-provider"],
|
||||
includes: ["local"],
|
||||
excludes: ["local-provider"],
|
||||
},
|
||||
{
|
||||
name: "normalizes bundled speech package ids to canonical plugin ids",
|
||||
|
||||
@@ -382,6 +382,13 @@ describe("loadPluginManifestRegistry", () => {
|
||||
providerAuthEnvVars: {
|
||||
openai: ["OPENAI_API_KEY"],
|
||||
},
|
||||
providerEndpoints: [
|
||||
{
|
||||
endpointClass: "openai-public",
|
||||
hosts: ["API.OPENAI.COM", ""],
|
||||
baseUrls: ["https://api.openai.com/v1"],
|
||||
},
|
||||
],
|
||||
syntheticAuthRefs: ["openai-cli"],
|
||||
nonSecretAuthMarkers: ["openai-cli"],
|
||||
providerAuthAliases: {
|
||||
@@ -409,6 +416,13 @@ describe("loadPluginManifestRegistry", () => {
|
||||
expect(registry.plugins[0]?.providerAuthEnvVars).toEqual({
|
||||
openai: ["OPENAI_API_KEY"],
|
||||
});
|
||||
expect(registry.plugins[0]?.providerEndpoints).toEqual([
|
||||
{
|
||||
endpointClass: "openai-public",
|
||||
hosts: ["api.openai.com"],
|
||||
baseUrls: ["https://api.openai.com/v1"],
|
||||
},
|
||||
]);
|
||||
expect(registry.plugins[0]?.syntheticAuthRefs).toEqual(["openai-cli"]);
|
||||
expect(registry.plugins[0]?.nonSecretAuthMarkers).toEqual(["openai-cli"]);
|
||||
expect(registry.plugins[0]?.providerAuthAliases).toEqual({
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
type PluginManifestChannelConfig,
|
||||
type PluginManifestContracts,
|
||||
type PluginManifestModelSupport,
|
||||
type PluginManifestProviderEndpoint,
|
||||
type PluginManifestQaRunner,
|
||||
type PluginManifestSetup,
|
||||
} from "./manifest.js";
|
||||
@@ -85,6 +86,7 @@ export type PluginManifestRecord = {
|
||||
providers: string[];
|
||||
providerDiscoverySource?: string;
|
||||
modelSupport?: PluginManifestModelSupport;
|
||||
providerEndpoints?: PluginManifestProviderEndpoint[];
|
||||
cliBackends: string[];
|
||||
syntheticAuthRefs?: string[];
|
||||
nonSecretAuthMarkers?: string[];
|
||||
@@ -329,6 +331,7 @@ function buildRecord(params: {
|
||||
? path.resolve(params.candidate.rootDir, params.manifest.providerDiscoveryEntry)
|
||||
: undefined,
|
||||
modelSupport: params.manifest.modelSupport,
|
||||
providerEndpoints: params.manifest.providerEndpoints,
|
||||
cliBackends: params.manifest.cliBackends ?? [],
|
||||
syntheticAuthRefs: params.manifest.syntheticAuthRefs ?? [],
|
||||
nonSecretAuthMarkers: params.manifest.nonSecretAuthMarkers ?? [],
|
||||
|
||||
@@ -39,6 +39,18 @@ export type PluginManifestModelSupport = {
|
||||
modelPatterns?: string[];
|
||||
};
|
||||
|
||||
export type PluginManifestProviderEndpoint = {
|
||||
/**
|
||||
* Core endpoint class this plugin-owned endpoint should map to. Core must
|
||||
* already know the class; manifests own host/baseUrl matching metadata.
|
||||
*/
|
||||
endpointClass: string;
|
||||
/** Hostnames that should resolve to this endpoint class. */
|
||||
hosts?: string[];
|
||||
/** Exact normalized base URLs that should resolve to this endpoint class. */
|
||||
baseUrls?: string[];
|
||||
};
|
||||
|
||||
export type PluginManifestActivationCapability = "provider" | "channel" | "tool" | "hook";
|
||||
|
||||
export type PluginManifestActivation = {
|
||||
@@ -161,6 +173,8 @@ export type PluginManifest = {
|
||||
* Use this for shorthand model refs that omit an explicit provider prefix.
|
||||
*/
|
||||
modelSupport?: PluginManifestModelSupport;
|
||||
/** Cheap provider endpoint metadata used before provider runtime loads. */
|
||||
providerEndpoints?: PluginManifestProviderEndpoint[];
|
||||
/** Cheap startup activation lookup for plugin-owned CLI inference backends. */
|
||||
cliBackends?: string[];
|
||||
/**
|
||||
@@ -433,6 +447,37 @@ function normalizeManifestModelSupport(value: unknown): PluginManifestModelSuppo
|
||||
return Object.keys(modelSupport).length > 0 ? modelSupport : undefined;
|
||||
}
|
||||
|
||||
function normalizeManifestProviderEndpoints(
|
||||
value: unknown,
|
||||
): PluginManifestProviderEndpoint[] | undefined {
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const endpoints: PluginManifestProviderEndpoint[] = [];
|
||||
for (const rawEndpoint of value) {
|
||||
if (!isRecord(rawEndpoint)) {
|
||||
continue;
|
||||
}
|
||||
const endpointClass = normalizeOptionalString(rawEndpoint.endpointClass);
|
||||
if (!endpointClass) {
|
||||
continue;
|
||||
}
|
||||
const hosts = normalizeTrimmedStringList(rawEndpoint.hosts).map((host) => host.toLowerCase());
|
||||
const baseUrls = normalizeTrimmedStringList(rawEndpoint.baseUrls);
|
||||
if (hosts.length === 0 && baseUrls.length === 0) {
|
||||
continue;
|
||||
}
|
||||
endpoints.push({
|
||||
endpointClass,
|
||||
...(hosts.length > 0 ? { hosts } : {}),
|
||||
...(baseUrls.length > 0 ? { baseUrls } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
return endpoints.length > 0 ? endpoints : undefined;
|
||||
}
|
||||
|
||||
function normalizeManifestActivation(value: unknown): PluginManifestActivation | undefined {
|
||||
if (!isRecord(value)) {
|
||||
return undefined;
|
||||
@@ -710,6 +755,7 @@ export function loadPluginManifest(
|
||||
const providers = normalizeTrimmedStringList(raw.providers);
|
||||
const providerDiscoveryEntry = normalizeOptionalString(raw.providerDiscoveryEntry);
|
||||
const modelSupport = normalizeManifestModelSupport(raw.modelSupport);
|
||||
const providerEndpoints = normalizeManifestProviderEndpoints(raw.providerEndpoints);
|
||||
const cliBackends = normalizeTrimmedStringList(raw.cliBackends);
|
||||
const syntheticAuthRefs = normalizeTrimmedStringList(raw.syntheticAuthRefs);
|
||||
const nonSecretAuthMarkers = normalizeTrimmedStringList(raw.nonSecretAuthMarkers);
|
||||
@@ -746,6 +792,7 @@ export function loadPluginManifest(
|
||||
providers,
|
||||
providerDiscoveryEntry,
|
||||
modelSupport,
|
||||
providerEndpoints,
|
||||
cliBackends,
|
||||
syntheticAuthRefs,
|
||||
nonSecretAuthMarkers,
|
||||
|
||||
@@ -899,18 +899,18 @@ export function describeTtsSummarizationContract() {
|
||||
expect(resolveModelAsyncMock).toHaveBeenCalledWith("openai", "gpt-4.1-mini", undefined, cfg);
|
||||
});
|
||||
|
||||
it("keeps the Ollama api for direct summarization", async () => {
|
||||
it("keeps native completion APIs for direct summarization", async () => {
|
||||
vi.mocked(resolveModelAsyncMock).mockResolvedValue({
|
||||
...createResolvedModel("ollama", "qwen3:8b", "ollama"),
|
||||
...createResolvedModel("local-summary", "demo-model", "openai-completions"),
|
||||
model: {
|
||||
...createResolvedModel("ollama", "qwen3:8b", "ollama").model,
|
||||
baseUrl: "http://127.0.0.1:11434",
|
||||
...createResolvedModel("local-summary", "demo-model", "openai-completions").model,
|
||||
baseUrl: "http://127.0.0.1:4000/v1",
|
||||
},
|
||||
} as never);
|
||||
|
||||
await runSummarizeText();
|
||||
|
||||
expect(vi.mocked(completeSimple).mock.calls[0]?.[0]?.api).toBe("ollama");
|
||||
expect(vi.mocked(completeSimple).mock.calls[0]?.[0]?.api).toBe("openai-completions");
|
||||
expect(ensureCustomApiRegisteredMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user