fix(providers): publish Meta provider (#103070)

This commit is contained in:
Vincent Koc
2026-07-09 12:11:56 -07:00
committed by GitHub
parent 11fa0cf2d5
commit 266ca5b3a2
36 changed files with 387 additions and 266 deletions
+55
View File
@@ -0,0 +1,55 @@
# Meta provider
Bundled OpenClaw provider plugin for the **Meta API** — an OpenAI-compatible
**Responses API** endpoint (`POST /v1/responses`).
- **Base URL:** `https://api.ai.meta.com/v1`
- **Auth:** `Authorization: Bearer $MODEL_API_KEY`
- **Model:** `muse-spark-1.1` (reasoning model; use `muse-spark` for smoke tests until 1.1 ships)
- Context window: 1,048,576 tokens (input + output share the budget)
- Reasoning effort: `minimal | low | medium | high | xhigh` (default: `high`)
- Vision: image input in `user` messages
- Tool calling + streaming
- Stateless encrypted reasoning replay (`store: false`)
## Usage
Set the API key and select the model:
```bash
export MODEL_API_KEY=<key>
```
```json5
// ~/.openclaw/openclaw.json
{
agents: {
defaults: {
model: { primary: "meta/muse-spark-1.1" },
},
},
}
```
Or run onboarding and choose **Meta**.
## Thinking / reasoning
`--thinking <level>` and `/think <level>` map to Responses API `reasoning.effort`.
Default thinking level is `high`. `off` maps to `minimal` because Muse Spark does
not accept `none`.
## Docs
See `docs/providers/meta.md` for setup, onboarding, and smoke tests.
## Live test
```bash
export MODEL_API_KEY=<key>
export OPENCLAW_LIVE_TEST=1
export META_LIVE_TEST=1
pnpm test extensions/meta/meta.live.test.ts
```
Live tests call `muse-spark` on `/v1/responses`.
+11
View File
@@ -0,0 +1,11 @@
/**
* Public Meta provider plugin API exports.
*/
export {
buildMetaCatalogModels,
buildMetaModelDefinition,
META_BASE_URL,
META_MODEL_CATALOG,
} from "./models.js";
export { buildMetaProvider } from "./provider-catalog.js";
export { applyMetaConfig, META_DEFAULT_MODEL_REF } from "./onboard.js";
+70
View File
@@ -0,0 +1,70 @@
// Meta tests cover plugin registration and catalog shape.
import { capturePluginRegistration } from "openclaw/plugin-sdk/plugin-test-runtime";
import { describe, expect, it } from "vitest";
import { buildMetaProvider } from "./api.js";
import plugin from "./index.js";
function requireThinkingProfileResolver(
provider: ReturnType<typeof capturePluginRegistration>["providers"][number],
) {
if (!provider.resolveThinkingProfile) {
throw new Error("Expected resolveThinkingProfile on Meta provider");
}
return provider.resolveThinkingProfile;
}
describe("meta provider", () => {
it("registers the Meta provider with api-key auth", () => {
const captured = capturePluginRegistration(plugin);
const [provider] = captured.providers;
if (!provider) {
throw new Error("Expected Meta provider");
}
expect(provider).toMatchObject({
id: "meta",
label: "Meta",
docsPath: "/providers/meta",
});
expect(provider.auth).toHaveLength(1);
expect(provider.auth[0]).toMatchObject({
id: "api-key",
kind: "api_key",
label: "Meta API key",
});
});
it("builds the muse-spark-1.1 catalog entry over openai-responses", () => {
const providerConfig = buildMetaProvider();
expect(providerConfig.baseUrl).toBe("https://api.ai.meta.com/v1");
expect(providerConfig.api).toBe("openai-responses");
const model = providerConfig.models.find((m) => m.id === "muse-spark-1.1");
if (!model) {
throw new Error("Expected muse-spark-1.1 model");
}
expect(model.contextWindow).toBe(1048576);
expect(model.reasoning).toBe(true);
expect(model.input).toContain("image");
});
it("advertises a high default thinking profile for muse-spark models", () => {
const captured = capturePluginRegistration(plugin);
const [provider] = captured.providers;
if (!provider) {
throw new Error("Expected Meta provider");
}
const resolveThinkingProfile = requireThinkingProfileResolver(provider);
const profile = resolveThinkingProfile({
provider: "meta",
modelId: "muse-spark-1.1",
} as never);
expect(profile?.defaultLevel).toBe("high");
expect(profile?.levels.map((level) => level.id)).toEqual([
"off",
"minimal",
"low",
"medium",
"high",
"xhigh",
]);
});
});
+47
View File
@@ -0,0 +1,47 @@
/**
* Meta provider plugin entrypoint.
*/
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { OPENAI_COMPATIBLE_REPLAY_HOOKS } from "openclaw/plugin-sdk/provider-model-shared";
import { applyMetaConfig, META_DEFAULT_MODEL_REF } from "./onboard.js";
import { buildMetaProvider } from "./provider-catalog.js";
import { wrapMetaProviderStream } from "./stream.js";
import { resolveMetaThinkingProfile } from "./thinking.js";
const PROVIDER_ID = "meta";
export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "Meta Provider",
description: "Bundled Meta provider plugin",
provider: {
label: "Meta",
docsPath: "/providers/meta",
auth: [
{
methodId: "api-key",
label: "Meta API key",
hint: "Meta (Responses API)",
optionKey: "metaApiKey",
flagName: "--meta-api-key",
envVar: "MODEL_API_KEY",
promptMessage: "Enter Meta API key",
defaultModel: META_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applyMetaConfig(cfg),
noteMessage: ["Meta provides Responses API inference."].join("\n"),
noteTitle: "Meta",
wizard: {
groupLabel: "Meta",
groupHint: "Meta (Responses API)",
},
},
],
catalog: {
buildProvider: buildMetaProvider,
buildStaticProvider: buildMetaProvider,
},
...OPENAI_COMPATIBLE_REPLAY_HOOKS,
wrapStreamFn: wrapMetaProviderStream,
resolveThinkingProfile: ({ modelId }) => resolveMetaThinkingProfile(modelId),
},
});
+90
View File
@@ -0,0 +1,90 @@
// Meta live tests prove muse-spark auth and Responses API completion.
import { streamSimple, type Model } from "openclaw/plugin-sdk/llm";
import { extractNonEmptyAssistantText, isLiveTestEnabled } from "openclaw/plugin-sdk/test-env";
import { describe, expect, it } from "vitest";
import { buildMetaProvider } from "./provider-catalog.js";
import { wrapMetaProviderStream } from "./stream.js";
const MODEL_API_KEY = process.env.MODEL_API_KEY?.trim() ?? "";
const LIVE_MODEL_ID = "muse-spark";
const LIVE =
isLiveTestEnabled(["META_LIVE_TEST", "MODEL_API_LIVE_TEST"]) &&
MODEL_API_KEY.length > 0;
const describeLive = LIVE ? describe : describe.skip;
function resolveLiveModel(): Model<"openai-responses"> {
const provider = buildMetaProvider();
const catalogModel = provider.models?.find((entry) => entry.id === "muse-spark-1.1");
if (!catalogModel) {
throw new Error("Meta catalog does not include muse-spark-1.1");
}
return {
provider: "meta",
baseUrl: provider.baseUrl,
...catalogModel,
id: LIVE_MODEL_ID,
api: "openai-responses",
} as Model<"openai-responses">;
}
function resolveLiveStreamFn() {
const model = resolveLiveModel();
return (
wrapMetaProviderStream({
provider: "meta",
modelId: model.id,
model,
streamFn: streamSimple,
}) ?? streamSimple
);
}
describeLive("meta plugin live", () => {
it("lists muse-spark via the /models endpoint", async () => {
const response = await fetch("https://api.ai.meta.com/v1/models", {
headers: { Authorization: `Bearer ${MODEL_API_KEY}` },
});
expect(response.ok).toBe(true);
const body = (await response.json()) as { data?: Array<{ id: string }> };
const ids = (body.data ?? []).map((entry) => entry.id);
expect(ids).toContain(LIVE_MODEL_ID);
}, 30_000);
it("completes a muse-spark Responses API turn with high reasoning effort", async () => {
const model = resolveLiveModel();
let capturedPayload: Record<string, unknown> | undefined;
const stream = await resolveLiveStreamFn()(
model,
{
messages: [
{
role: "user",
content: "Reply with exactly: PATCH_OK",
timestamp: Date.now(),
},
],
},
{
apiKey: MODEL_API_KEY,
maxTokens: 4000, // fix: high reasoning needs ~300 tokens
reasoning: "high",
onPayload: (payload) => {
capturedPayload = payload as Record<string, unknown>;
},
},
);
const result = await stream.result();
if (result.stopReason === "error") {
throw new Error(result.errorMessage || "Meta returned an error");
}
expect(capturedPayload?.store).toBe(false);
expect(capturedPayload?.include).toEqual(
expect.arrayContaining(["reasoning.encrypted_content"]),
);
const reasoning = capturedPayload?.reasoning as { effort?: string } | undefined;
expect(reasoning?.effort).toBe("high");
expect(extractNonEmptyAssistantText(result.content)).toMatch(/PATCH_OK/i);
}, 120_000);
});
+32
View File
@@ -0,0 +1,32 @@
/**
* Meta model catalog helpers derived from the plugin manifest.
*/
import { buildManifestModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-shared";
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
import manifest from "./openclaw.plugin.json" with { type: "json" };
const META_MANIFEST_CATALOG = manifest.modelCatalog.providers["meta"];
/** Base URL for Meta OpenAI-compatible inference. */
export const META_BASE_URL = META_MANIFEST_CATALOG.baseUrl;
/** Meta model catalog entries from the plugin manifest. */
export const META_MODEL_CATALOG = META_MANIFEST_CATALOG.models;
/** Builds normalized Meta catalog model definitions. */
export function buildMetaCatalogModels(): ModelDefinitionConfig[] {
return buildManifestModelProviderConfig({
providerId: "meta",
catalog: META_MANIFEST_CATALOG,
}).models;
}
/** Builds one normalized Meta model definition from a manifest entry. */
export function buildMetaModelDefinition(
model: (typeof META_MODEL_CATALOG)[number],
): ModelDefinitionConfig {
const providerConfig = buildManifestModelProviderConfig({
providerId: "meta",
catalog: { ...META_MANIFEST_CATALOG, models: [model] },
});
return providerConfig.models[0];
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "@openclaw/meta-provider",
"version": "2026.6.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/meta-provider",
"version": "2026.6.11"
}
}
}
+31
View File
@@ -0,0 +1,31 @@
/**
* Meta onboarding config helpers.
*/
import {
createModelCatalogPresetAppliers,
type OpenClawConfig,
} from "openclaw/plugin-sdk/provider-onboard";
import {
buildMetaModelDefinition,
META_BASE_URL,
META_MODEL_CATALOG,
} from "./models.js";
/** Default Meta model reference used after onboarding. */
export const META_DEFAULT_MODEL_REF = "meta/muse-spark-1.1";
const metaPresetAppliers = createModelCatalogPresetAppliers({
primaryModelRef: META_DEFAULT_MODEL_REF,
resolveParams: (_cfg: OpenClawConfig) => ({
providerId: "meta",
api: "openai-responses",
baseUrl: META_BASE_URL,
catalogModels: META_MODEL_CATALOG.map(buildMetaModelDefinition),
aliases: [{ modelRef: META_DEFAULT_MODEL_REF, alias: "Muse Spark 1.1" }],
}),
});
/** Applies Meta provider/catalog config and default model aliases. */
export function applyMetaConfig(cfg: OpenClawConfig): OpenClawConfig {
return metaPresetAppliers.applyConfig(cfg);
}
+99
View File
@@ -0,0 +1,99 @@
{
"id": "meta",
"activation": {
"onStartup": false
},
"enabledByDefault": true,
"providers": ["meta"],
"providerEndpoints": [
{
"endpointClass": "meta-native",
"hosts": ["api.ai.meta.com"]
}
],
"providerRequest": {
"providers": {
"meta": {
"family": "meta"
}
}
},
"modelCatalog": {
"providers": {
"meta": {
"baseUrl": "https://api.ai.meta.com/v1",
"api": "openai-responses",
"models": [
{
"id": "muse-spark-1.1",
"name": "Muse Spark 1.1",
"reasoning": true,
"input": ["text", "image"],
"contextWindow": 1048576,
"maxTokens": 128000,
"thinkingLevelMap": {
"off": "minimal",
"minimal": "minimal",
"low": "low",
"medium": "medium",
"high": "high",
"xhigh": "xhigh"
},
"compat": {
"supportsTools": true,
"supportsReasoningEffort": true,
"supportedReasoningEfforts": [
"minimal",
"low",
"medium",
"high",
"xhigh"
]
},
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
}
}
]
}
},
"discovery": {
"meta": "static"
}
},
"setup": {
"providers": [
{
"id": "meta",
"authMethods": ["api-key"],
"envVars": ["MODEL_API_KEY"]
}
]
},
"providerAuthChoices": [
{
"provider": "meta",
"method": "api-key",
"choiceId": "meta-api-key",
"appGuidedSecret": true,
"choiceLabel": "Meta API key",
"choiceHint": "Meta (Responses API)",
"groupId": "meta",
"groupLabel": "Meta",
"groupHint": "Meta (Responses API)",
"onboardingFeatured": true,
"optionKey": "metaApiKey",
"cliFlag": "--meta-api-key",
"cliOption": "--meta-api-key <key>",
"cliDescription": "Meta API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}
+35
View File
@@ -0,0 +1,35 @@
{
"name": "@openclaw/meta-provider",
"version": "2026.6.11",
"description": "OpenClaw Meta provider plugin.",
"repository": {
"type": "git",
"url": "https://github.com/openclaw/openclaw"
},
"type": "module",
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {
"extensions": [
"./index.ts"
],
"install": {
"clawhubSpec": "clawhub:@openclaw/meta-provider",
"npmSpec": "@openclaw/meta-provider",
"defaultChoice": "npm",
"minHostVersion": ">=2026.6.11"
},
"compat": {
"pluginApi": ">=2026.6.11"
},
"build": {
"openclawVersion": "2026.6.11",
"bundledDist": true
},
"release": {
"publishToClawHub": true,
"publishToNpm": true
}
}
}
+14
View File
@@ -0,0 +1,14 @@
/**
* Meta model provider builder.
*/
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
import { buildMetaCatalogModels, META_BASE_URL } from "./models.js";
/** Builds the Meta OpenAI-compatible model provider config. */
export function buildMetaProvider(): ModelProviderConfig {
return {
baseUrl: META_BASE_URL,
api: "openai-responses",
models: buildMetaCatalogModels(),
};
}
+42
View File
@@ -0,0 +1,42 @@
// Meta plugin module implements stream behavior.
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
import { streamSimple } from "openclaw/plugin-sdk/llm";
import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry";
import { streamWithPayloadPatch } from "openclaw/plugin-sdk/provider-stream-shared";
const META_REASONING_ENCRYPTED_CONTENT_INCLUDE = "reasoning.encrypted_content";
function ensureMetaResponsesReplayFields(payloadObj: Record<string, unknown>): void {
const existing = payloadObj.include;
const include = Array.isArray(existing)
? existing.filter((entry): entry is string => typeof entry === "string")
: [];
if (!include.includes(META_REASONING_ENCRYPTED_CONTENT_INCLUDE)) {
include.push(META_REASONING_ENCRYPTED_CONTENT_INCLUDE);
}
payloadObj.include = include;
payloadObj.store = false;
}
export function createMetaResponsesWrapper(baseStreamFn: StreamFn | undefined): StreamFn {
const underlying = baseStreamFn ?? streamSimple;
return (model, context, options) =>
streamWithPayloadPatch(underlying, model, context, options, (payloadObj) => {
if (model.provider !== "meta" || model.api !== "openai-responses") {
return;
}
if (!model.reasoning) {
return;
}
ensureMetaResponsesReplayFields(payloadObj);
});
}
export function wrapMetaProviderStream(
ctx: ProviderWrapStreamFnContext,
): StreamFn | undefined {
if (ctx.provider !== "meta" || ctx.model?.api !== "openai-responses") {
return undefined;
}
return createMetaResponsesWrapper(ctx.streamFn);
}
+21
View File
@@ -0,0 +1,21 @@
// Meta plugin module implements thinking behavior.
import type { ProviderThinkingProfile } from "openclaw/plugin-sdk/plugin-entry";
const META_REASONING_MODEL_IDS = new Set(["muse-spark", "muse-spark-1.1"]);
function isMetaReasoningModelId(modelId: string): boolean {
return META_REASONING_MODEL_IDS.has(modelId.toLowerCase());
}
const META_THINKING_LEVEL_IDS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
const META_THINKING_PROFILE = {
levels: META_THINKING_LEVEL_IDS.map((id) => ({ id })),
defaultLevel: "high",
} satisfies ProviderThinkingProfile;
export function resolveMetaThinkingProfile(
modelId: string,
): ProviderThinkingProfile | undefined {
return isMetaReasoningModelId(modelId) ? META_THINKING_PROFILE : undefined;
}
+16
View File
@@ -0,0 +1,16 @@
{
"extends": "../tsconfig.package-boundary.base.json",
"compilerOptions": {
"rootDir": "."
},
"include": ["./*.ts", "./src/**/*.ts"],
"exclude": [
"./**/*.test.ts",
"./dist/**",
"./node_modules/**",
"./src/test-support/**",
"./src/**/*test-helpers.ts",
"./src/**/*test-harness.ts",
"./src/**/*test-support.ts"
]
}