feat(provider): add Featherless AI integration

This commit is contained in:
Vincent Koc
2026-07-06 16:17:44 -07:00
committed by Vincent Koc
parent 317aa89af5
commit 2c11f11303
18 changed files with 700 additions and 0 deletions
+5
View File
@@ -378,6 +378,11 @@
- any-glob-to-any-file:
- "extensions/deepinfra/**"
- "docs/providers/deepinfra.md"
"extensions: featherless":
- changed-files:
- any-glob-to-any-file:
- "extensions/featherless/**"
- "docs/providers/featherless.md"
"extensions: gmi":
- changed-files:
- any-glob-to-any-file:
+1
View File
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
### Changes
- **Featherless AI provider:** add an official external provider plugin with API-key onboarding, a tool-capable Qwen3 default, and support for arbitrary Featherless model ids.
- **Gateway host status:** show the connected Gateway's host, network address, OS, runtime, uptime, CPU, memory, and disk details in Control UI Settings. (#100478)
- **iOS offline chat:** pre-paint recent sessions and canonical transcripts from a protected, bounded per-gateway cache, keep sending disabled offline, and purge cached conversation text when pairing is reset. (#100194)
- **Slack progress indicators:** use Slack's native assistant thread status and rotating loading messages by default while keeping acknowledgement reactions static; lifecycle reaction updates now require `messages.statusReactions.enabled: true`.
+12
View File
@@ -0,0 +1,12 @@
# OpenClaw Featherless AI Provider
Official OpenClaw provider plugin for Featherless AI's OpenAI-compatible API.
Install from OpenClaw:
```bash
openclaw plugins install @openclaw/featherless-provider
openclaw gateway restart
```
See <https://docs.openclaw.ai/providers/featherless> for setup and configuration.
+14
View File
@@ -0,0 +1,14 @@
// Public Featherless provider plugin API exports.
export {
FEATHERLESS_BASE_URL,
FEATHERLESS_DEFAULT_CONTEXT_WINDOW,
FEATHERLESS_DEFAULT_MAX_TOKENS,
FEATHERLESS_DEFAULT_MODEL_ID,
FEATHERLESS_DEFAULT_MODEL_REF,
FEATHERLESS_DYNAMIC_CONTEXT_WINDOW,
FEATHERLESS_DYNAMIC_MAX_TOKENS,
buildFeatherlessCatalogModels,
isFeatherlessCatalogModelId,
} from "./models.js";
export { applyFeatherlessConfig } from "./onboard.js";
export { buildFeatherlessProvider } from "./provider-catalog.js";
@@ -0,0 +1,114 @@
// Featherless live tests prove text generation and a complete tool-call round trip.
import {
completeSimple,
type AssistantMessage,
type Model,
type Tool,
} from "openclaw/plugin-sdk/llm";
import { extractNonEmptyAssistantText, isLiveTestEnabled } from "openclaw/plugin-sdk/test-env";
import { Type } from "typebox";
import { describe, expect, it } from "vitest";
import { FEATHERLESS_DEFAULT_MODEL_ID } from "./models.js";
import { buildFeatherlessProvider } from "./provider-catalog.js";
const FEATHERLESS_API_KEY = process.env.FEATHERLESS_API_KEY?.trim() ?? "";
const LIVE = isLiveTestEnabled(["FEATHERLESS_LIVE_TEST"]) && FEATHERLESS_API_KEY.length > 0;
const describeLive = LIVE ? describe : describe.skip;
function resolveLiveModel(): Model<"openai-completions"> {
const provider = buildFeatherlessProvider();
const model = provider.models?.find((entry) => entry.id === FEATHERLESS_DEFAULT_MODEL_ID);
if (!model) {
throw new Error(`Featherless catalog does not include ${FEATHERLESS_DEFAULT_MODEL_ID}`);
}
return {
provider: "featherless",
baseUrl: provider.baseUrl,
...model,
api: "openai-completions",
} as Model<"openai-completions">;
}
function liveEchoTool(): Tool {
return {
name: "live_echo",
description: "Return the supplied value.",
parameters: Type.Object(
{
value: Type.String(),
},
{ additionalProperties: false },
),
};
}
function requireToolCall(message: AssistantMessage) {
const toolCall = message.content.find((block) => block.type === "toolCall");
if (toolCall?.type !== "toolCall") {
throw new Error(`Featherless live model did not call a tool: ${message.stopReason}`);
}
return toolCall;
}
describeLive("featherless plugin live", () => {
it("completes a tool-call round trip with the default model", async () => {
const model = resolveLiveModel();
const tool = liveEchoTool();
const userPrompt = {
role: "user" as const,
content: "Call live_echo with value featherless. Do not answer directly.",
timestamp: Date.now() - 3,
};
const first = await completeSimple(
model,
{
messages: [userPrompt],
tools: [tool],
},
{
apiKey: FEATHERLESS_API_KEY,
maxTokens: 256,
},
);
if (first.stopReason === "error") {
throw new Error(first.errorMessage || "Featherless first turn returned an error");
}
const toolCall = requireToolCall(first);
expect(toolCall.name).toBe("live_echo");
expect(toolCall.arguments).toEqual({ value: "featherless" });
const second = await completeSimple(
model,
{
messages: [
userPrompt,
first,
{
role: "toolResult",
toolCallId: toolCall.id,
toolName: toolCall.name,
content: [{ type: "text", text: "ok" }],
isError: false,
timestamp: Date.now() - 1,
},
{
role: "user",
content: "Reply with exactly: ok",
timestamp: Date.now(),
},
],
tools: [tool],
},
{
apiKey: FEATHERLESS_API_KEY,
maxTokens: 64,
},
);
if (second.stopReason === "error") {
throw new Error(second.errorMessage || "Featherless second turn returned an error");
}
expect(extractNonEmptyAssistantText(second.content)).toMatch(/^ok[.!]?$/i);
}, 120_000);
});
+147
View File
@@ -0,0 +1,147 @@
// Featherless tests cover provider registration, catalog, and dynamic model behavior.
import type { ProviderRuntimeModel } from "openclaw/plugin-sdk/plugin-entry";
import {
registerSingleProviderPlugin,
resolveProviderPluginChoice,
} from "openclaw/plugin-sdk/plugin-test-runtime";
import { resolveAgentModelPrimaryValue } from "openclaw/plugin-sdk/provider-onboard";
import { describe, expect, it } from "vitest";
import { createProviderDynamicModelContext } from "../test-support/provider-model-test-helpers.js";
import featherlessPlugin from "./index.js";
import {
FEATHERLESS_BASE_URL,
FEATHERLESS_DEFAULT_CONTEXT_WINDOW,
FEATHERLESS_DEFAULT_MAX_TOKENS,
FEATHERLESS_DEFAULT_MODEL_ID,
FEATHERLESS_DEFAULT_MODEL_REF,
FEATHERLESS_DYNAMIC_COMPAT,
FEATHERLESS_DYNAMIC_CONTEXT_WINDOW,
FEATHERLESS_DYNAMIC_MAX_TOKENS,
} from "./models.js";
import { applyFeatherlessConfig } from "./onboard.js";
function createDefaultRuntimeModel(): ProviderRuntimeModel {
return {
id: FEATHERLESS_DEFAULT_MODEL_ID,
name: "Qwen3 32B",
provider: "featherless",
api: "openai-completions",
baseUrl: FEATHERLESS_BASE_URL,
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: FEATHERLESS_DEFAULT_CONTEXT_WINDOW,
maxTokens: FEATHERLESS_DEFAULT_MAX_TOKENS,
compat: { thinkingFormat: "qwen-chat-template" },
};
}
describe("featherless provider plugin", () => {
it("registers Featherless AI with api-key auth metadata", async () => {
const provider = await registerSingleProviderPlugin(featherlessPlugin);
const resolved = resolveProviderPluginChoice({
providers: [provider],
choice: "featherless-api-key",
});
expect(provider.id).toBe("featherless");
expect(provider.label).toBe("Featherless AI");
expect(provider.envVars).toEqual(["FEATHERLESS_API_KEY"]);
expect(provider.auth).toHaveLength(1);
expect(provider.normalizeToolSchemas).toEqual(expect.any(Function));
expect(resolved?.provider.id).toBe("featherless");
expect(resolved?.method.id).toBe("api-key");
});
it("applies the curated default during onboarding", () => {
const config = applyFeatherlessConfig({});
expect(resolveAgentModelPrimaryValue(config.agents?.defaults?.model)).toBe(
FEATHERLESS_DEFAULT_MODEL_REF,
);
expect(config.agents?.defaults?.models?.[FEATHERLESS_DEFAULT_MODEL_REF]?.alias).toBe(
"Qwen3 32B",
);
});
it("builds the curated Featherless catalog", async () => {
const provider = await registerSingleProviderPlugin(featherlessPlugin);
const result = await provider.staticCatalog?.run({
config: {},
env: {},
resolveProviderApiKey: () => ({}),
} as never);
if (!result || !("provider" in result)) {
throw new Error("expected Featherless static catalog");
}
expect(result.provider.baseUrl).toBe(FEATHERLESS_BASE_URL);
expect(result.provider.api).toBe("openai-completions");
expect(result.provider.models).toEqual([
expect.objectContaining({
id: FEATHERLESS_DEFAULT_MODEL_ID,
reasoning: true,
input: ["text"],
contextWindow: FEATHERLESS_DEFAULT_CONTEXT_WINDOW,
maxTokens: FEATHERLESS_DEFAULT_MAX_TOKENS,
compat: expect.objectContaining({
maxTokensField: "max_tokens",
thinkingFormat: "qwen-chat-template",
}),
}),
]);
});
it("resolves arbitrary Featherless model ids from conservative text defaults", async () => {
const provider = await registerSingleProviderPlugin(featherlessPlugin);
const resolved = provider.resolveDynamicModel?.(
createProviderDynamicModelContext({
provider: "featherless",
modelId: "moonshotai/Kimi-K2-Instruct",
models: [createDefaultRuntimeModel()],
}),
);
expect(resolved).toMatchObject({
id: "moonshotai/Kimi-K2-Instruct",
provider: "featherless",
api: "openai-completions",
baseUrl: FEATHERLESS_BASE_URL,
reasoning: false,
input: ["text"],
contextWindow: FEATHERLESS_DYNAMIC_CONTEXT_WINDOW,
maxTokens: FEATHERLESS_DYNAMIC_MAX_TOKENS,
compat: FEATHERLESS_DYNAMIC_COMPAT,
});
});
it("defers the curated model to static catalog resolution", async () => {
const provider = await registerSingleProviderPlugin(featherlessPlugin);
const resolved = provider.resolveDynamicModel?.(
createProviderDynamicModelContext({
provider: "featherless",
modelId: FEATHERLESS_DEFAULT_MODEL_ID,
models: [createDefaultRuntimeModel()],
}),
);
expect(resolved).toBeUndefined();
});
it("uses the shared OpenAI-compatible replay policy", async () => {
const provider = await registerSingleProviderPlugin(featherlessPlugin);
const policy = provider.buildReplayPolicy?.({
provider: "featherless",
modelApi: "openai-completions",
modelId: FEATHERLESS_DEFAULT_MODEL_ID,
});
expect(policy).toMatchObject({
sanitizeToolCallIds: true,
applyAssistantFirstOrderingFix: true,
validateGeminiTurns: true,
validateAnthropicTurns: true,
});
expect(policy).not.toHaveProperty("dropReasoningFromHistory");
});
});
+105
View File
@@ -0,0 +1,105 @@
// Featherless plugin entrypoint registers its OpenClaw integration.
import type { ProviderResolveDynamicModelContext } from "openclaw/plugin-sdk/plugin-entry";
import { readConfiguredProviderCatalogEntries } from "openclaw/plugin-sdk/provider-catalog-shared";
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import {
buildProviderReplayFamilyHooks,
cloneFirstTemplateModel,
normalizeModelCompat,
} from "openclaw/plugin-sdk/provider-model-shared";
import { buildProviderToolCompatFamilyHooks } from "openclaw/plugin-sdk/provider-tools";
import { applyFeatherlessConfig, FEATHERLESS_DEFAULT_MODEL_REF } from "./onboard.js";
import {
buildFeatherlessProvider,
FEATHERLESS_BASE_URL,
FEATHERLESS_DEFAULT_MODEL_ID,
FEATHERLESS_DYNAMIC_COMPAT,
FEATHERLESS_DYNAMIC_CONTEXT_WINDOW,
FEATHERLESS_DYNAMIC_MAX_TOKENS,
isFeatherlessCatalogModelId,
} from "./provider-catalog.js";
const PROVIDER_ID = "featherless";
function resolveFeatherlessDynamicModel(ctx: ProviderResolveDynamicModelContext) {
const modelId = ctx.modelId.trim();
if (!modelId || isFeatherlessCatalogModelId(modelId)) {
return undefined;
}
return (
cloneFirstTemplateModel({
providerId: PROVIDER_ID,
modelId,
templateIds: [FEATHERLESS_DEFAULT_MODEL_ID],
ctx,
patch: {
provider: PROVIDER_ID,
reasoning: false,
input: ["text"],
contextWindow: FEATHERLESS_DYNAMIC_CONTEXT_WINDOW,
maxTokens: FEATHERLESS_DYNAMIC_MAX_TOKENS,
compat: FEATHERLESS_DYNAMIC_COMPAT,
},
}) ??
normalizeModelCompat({
id: modelId,
name: modelId,
provider: PROVIDER_ID,
api: "openai-completions",
baseUrl: FEATHERLESS_BASE_URL,
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: FEATHERLESS_DYNAMIC_CONTEXT_WINDOW,
maxTokens: FEATHERLESS_DYNAMIC_MAX_TOKENS,
compat: FEATHERLESS_DYNAMIC_COMPAT,
})
);
}
export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "Featherless AI Provider",
description: "Featherless AI provider plugin",
provider: {
label: "Featherless AI",
docsPath: "/providers/featherless",
envVars: ["FEATHERLESS_API_KEY"],
auth: [
{
methodId: "api-key",
label: "Featherless AI API key",
hint: "OpenAI-compatible access to open models",
optionKey: "featherlessApiKey",
flagName: "--featherless-api-key",
envVar: "FEATHERLESS_API_KEY",
promptMessage: "Enter Featherless AI API key",
defaultModel: FEATHERLESS_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applyFeatherlessConfig(cfg),
noteTitle: "Featherless AI",
noteMessage: [
"Featherless AI serves open models through an OpenAI-compatible API.",
"Create an API key at: https://featherless.ai/account/api-keys",
].join("\n"),
},
],
catalog: {
buildProvider: buildFeatherlessProvider,
buildStaticProvider: buildFeatherlessProvider,
allowExplicitBaseUrl: true,
},
augmentModelCatalog: ({ config }) =>
readConfiguredProviderCatalogEntries({
config,
providerId: PROVIDER_ID,
}),
...buildProviderReplayFamilyHooks({
family: "openai-compatible",
dropReasoningFromHistory: false,
}),
...buildProviderToolCompatFamilyHooks("openai"),
resolveDynamicModel: (ctx) => resolveFeatherlessDynamicModel(ctx),
isModernModelRef: () => true,
},
});
+40
View File
@@ -0,0 +1,40 @@
// Featherless model catalog helpers derive their values 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 FEATHERLESS_MANIFEST_PROVIDER = buildManifestModelProviderConfig({
providerId: "featherless",
catalog: manifest.modelCatalog.providers.featherless,
});
export const FEATHERLESS_BASE_URL = FEATHERLESS_MANIFEST_PROVIDER.baseUrl;
export const FEATHERLESS_DEFAULT_MODEL_ID = "Qwen/Qwen3-32B";
export const FEATHERLESS_DEFAULT_MODEL_REF = `featherless/${FEATHERLESS_DEFAULT_MODEL_ID}` as const;
export const FEATHERLESS_DYNAMIC_CONTEXT_WINDOW = 4096;
export const FEATHERLESS_DYNAMIC_MAX_TOKENS = 1024;
function requireFeatherlessManifestModel(id: string): ModelDefinitionConfig {
const model = FEATHERLESS_MANIFEST_PROVIDER.models.find((entry) => entry.id === id);
if (!model) {
throw new Error(`Missing Featherless modelCatalog row ${id}`);
}
return model;
}
const FEATHERLESS_DEFAULT_MODEL = requireFeatherlessManifestModel(FEATHERLESS_DEFAULT_MODEL_ID);
export const FEATHERLESS_DEFAULT_CONTEXT_WINDOW = FEATHERLESS_DEFAULT_MODEL.contextWindow;
export const FEATHERLESS_DEFAULT_MAX_TOKENS = FEATHERLESS_DEFAULT_MODEL.maxTokens;
export const FEATHERLESS_DYNAMIC_COMPAT = {
...FEATHERLESS_DEFAULT_MODEL.compat,
thinkingFormat: "openai",
} as const;
export function isFeatherlessCatalogModelId(modelId: string): boolean {
return FEATHERLESS_MANIFEST_PROVIDER.models.some((model) => model.id === modelId);
}
export function buildFeatherlessCatalogModels(): ModelDefinitionConfig[] {
return FEATHERLESS_MANIFEST_PROVIDER.models.map((model) => structuredClone(model));
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "@openclaw/featherless-provider",
"version": "2026.6.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/featherless-provider",
"version": "2026.6.11"
}
}
}
+27
View File
@@ -0,0 +1,27 @@
// Featherless onboarding applies the curated model catalog and default.
import {
createModelCatalogPresetAppliers,
type OpenClawConfig,
} from "openclaw/plugin-sdk/provider-onboard";
import {
buildFeatherlessCatalogModels,
FEATHERLESS_BASE_URL,
FEATHERLESS_DEFAULT_MODEL_REF,
} from "./models.js";
export { FEATHERLESS_DEFAULT_MODEL_REF } from "./models.js";
const featherlessPresetAppliers = createModelCatalogPresetAppliers({
primaryModelRef: FEATHERLESS_DEFAULT_MODEL_REF,
resolveParams: (_cfg: OpenClawConfig) => ({
providerId: "featherless",
api: "openai-completions",
baseUrl: FEATHERLESS_BASE_URL,
catalogModels: buildFeatherlessCatalogModels(),
aliases: [{ modelRef: FEATHERLESS_DEFAULT_MODEL_REF, alias: "Qwen3 32B" }],
}),
});
export function applyFeatherlessConfig(cfg: OpenClawConfig): OpenClawConfig {
return featherlessPresetAppliers.applyConfig(cfg);
}
@@ -0,0 +1,82 @@
{
"id": "featherless",
"name": "Featherless AI",
"description": "OpenClaw Featherless AI provider plugin.",
"activation": {
"onStartup": false
},
"enabledByDefault": true,
"providers": ["featherless"],
"providerRequest": {
"providers": {
"featherless": {
"family": "featherless"
}
}
},
"setup": {
"providers": [
{
"id": "featherless",
"envVars": ["FEATHERLESS_API_KEY"]
}
]
},
"providerAuthChoices": [
{
"provider": "featherless",
"method": "api-key",
"choiceId": "featherless-api-key",
"choiceLabel": "Featherless AI API key",
"choiceHint": "OpenAI-compatible access to open models",
"groupId": "featherless",
"groupLabel": "Featherless AI",
"groupHint": "OpenAI-compatible access to open models",
"optionKey": "featherlessApiKey",
"cliFlag": "--featherless-api-key",
"cliOption": "--featherless-api-key <key>",
"cliDescription": "Featherless AI API key"
}
],
"modelCatalog": {
"providers": {
"featherless": {
"baseUrl": "https://api.featherless.ai/v1",
"api": "openai-completions",
"models": [
{
"id": "Qwen/Qwen3-32B",
"name": "Qwen3 32B",
"reasoning": true,
"input": ["text"],
"contextWindow": 32768,
"maxTokens": 4096,
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
},
"compat": {
"supportsStore": false,
"supportsDeveloperRole": false,
"supportsReasoningEffort": false,
"supportsUsageInStreaming": false,
"maxTokensField": "max_tokens",
"thinkingFormat": "qwen-chat-template",
"supportsStrictMode": false
}
}
]
}
},
"discovery": {
"featherless": "static"
}
},
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}
+35
View File
@@ -0,0 +1,35 @@
{
"name": "@openclaw/featherless-provider",
"version": "2026.6.11",
"description": "OpenClaw Featherless AI 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/featherless-provider",
"npmSpec": "@openclaw/featherless-provider",
"defaultChoice": "npm",
"minHostVersion": ">=2026.6.11"
},
"compat": {
"pluginApi": ">=2026.6.11"
},
"build": {
"openclawVersion": "2026.6.11",
"bundledDist": false
},
"release": {
"publishToClawHub": true,
"publishToNpm": true
}
}
}
@@ -0,0 +1,22 @@
// Featherless provider catalog exposes the curated setup model.
import { buildManifestModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-shared";
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
import manifest from "./openclaw.plugin.json" with { type: "json" };
export {
FEATHERLESS_BASE_URL,
FEATHERLESS_DEFAULT_CONTEXT_WINDOW,
FEATHERLESS_DEFAULT_MAX_TOKENS,
FEATHERLESS_DEFAULT_MODEL_ID,
FEATHERLESS_DYNAMIC_COMPAT,
FEATHERLESS_DYNAMIC_CONTEXT_WINDOW,
FEATHERLESS_DYNAMIC_MAX_TOKENS,
isFeatherlessCatalogModelId,
} from "./models.js";
export function buildFeatherlessProvider(): ModelProviderConfig {
return buildManifestModelProviderConfig({
providerId: "featherless",
catalog: manifest.modelCatalog.providers.featherless,
});
}
+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"
]
}
+1
View File
@@ -98,6 +98,7 @@
"!dist/extensions/discord/**",
"!dist/extensions/exa/**",
"!dist/extensions/feishu/**",
"!dist/extensions/featherless/**",
"!dist/extensions/firecrawl/**",
"!dist/extensions/fireworks/**",
"!dist/extensions/google-meet/**",
+6
View File
@@ -783,6 +783,12 @@ importers:
specifier: workspace:*
version: link:../../packages/plugin-sdk
extensions/featherless:
devDependencies:
'@openclaw/plugin-sdk':
specifier: workspace:*
version: link:../../packages/plugin-sdk
extensions/firecrawl:
dependencies:
typebox:
@@ -538,6 +538,56 @@
}
}
},
{
"name": "@openclaw/featherless-provider",
"description": "OpenClaw Featherless AI provider plugin.",
"source": "official",
"kind": "provider",
"openclaw": {
"plugin": {
"id": "featherless",
"label": "Featherless AI"
},
"providers": [
{
"id": "featherless",
"name": "Featherless AI",
"docs": "/providers/featherless",
"categories": [
"cloud",
"llm"
],
"envVars": [
"FEATHERLESS_API_KEY"
],
"authChoices": [
{
"method": "api-key",
"choiceId": "featherless-api-key",
"choiceLabel": "Featherless AI API key",
"choiceHint": "OpenAI-compatible access to open models",
"groupId": "featherless",
"groupLabel": "Featherless AI",
"groupHint": "OpenAI-compatible access to open models",
"optionKey": "featherlessApiKey",
"cliFlag": "--featherless-api-key",
"cliOption": "--featherless-api-key <key>",
"cliDescription": "Featherless AI API key",
"onboardingScopes": [
"text-inference"
]
}
]
}
],
"install": {
"clawhubSpec": "clawhub:@openclaw/featherless-provider",
"npmSpec": "@openclaw/featherless-provider",
"defaultChoice": "npm",
"minHostVersion": ">=2026.6.11"
}
}
},
{
"name": "@openclaw/gmi-provider",
"description": "OpenClaw GMI Cloud provider plugin",
@@ -1661,6 +1661,7 @@ describe("official external plugin catalog", () => {
["vercel-ai-gateway", "@openclaw/vercel-ai-gateway-provider"],
["zai", "@openclaw/zai-provider"],
] as const;
const currentExternalized = [["featherless", "@openclaw/featherless-provider"]] as const;
for (const [id, npmSpec] of [...providers, ...plugins]) {
expect(resolveOfficialExternalPluginInstall(expectCatalogEntry(id))).toEqual({
@@ -1678,6 +1679,14 @@ describe("official external plugin catalog", () => {
minHostVersion: ">=2026.6.9",
});
}
for (const [id, npmSpec] of currentExternalized) {
expect(resolveOfficialExternalPluginInstall(expectCatalogEntry(id))).toEqual({
clawhubSpec: `clawhub:${npmSpec}`,
npmSpec,
defaultChoice: "npm",
minHostVersion: ">=2026.6.11",
});
}
});
it("advertises StepFun with its ClawHub package and plugin API floor", () => {
@@ -1827,6 +1836,7 @@ describe("official external plugin catalog", () => {
CLOUDFLARE_AI_GATEWAY_API_KEY: "cloudflare-key",
DEEPINFRA_API_KEY: "deepinfra-key",
DEEPSEEK_API_KEY: "deepseek-key",
FEATHERLESS_API_KEY: "featherless-key",
GROQ_API_KEY: "groq-key",
LONGCAT_API_KEY: "longcat-key",
KILOCODE_API_KEY: "kilocode-key",
@@ -1850,6 +1860,7 @@ describe("official external plugin catalog", () => {
"cloudflare-ai-gateway",
"deepinfra",
"deepseek",
"featherless",
"fireworks",
"groq",
"kilocode",