From 2c11f11303870a22b4e75c098dba99a77381590f Mon Sep 17 00:00:00 2001 From: Vincent Koc <25068+vincentkoc@users.noreply.github.com> Date: Sun, 5 Jul 2026 22:30:13 -0700 Subject: [PATCH] feat(provider): add Featherless AI integration --- .github/labeler.yml | 5 + CHANGELOG.md | 1 + extensions/featherless/README.md | 12 ++ extensions/featherless/api.ts | 14 ++ .../featherless/featherless.live.test.ts | 114 ++++++++++++++ extensions/featherless/index.test.ts | 147 ++++++++++++++++++ extensions/featherless/index.ts | 105 +++++++++++++ extensions/featherless/models.ts | 40 +++++ extensions/featherless/npm-shrinkwrap.json | 12 ++ extensions/featherless/onboard.ts | 27 ++++ extensions/featherless/openclaw.plugin.json | 82 ++++++++++ extensions/featherless/package.json | 35 +++++ extensions/featherless/provider-catalog.ts | 22 +++ extensions/featherless/tsconfig.json | 16 ++ package.json | 1 + pnpm-lock.yaml | 6 + .../official-external-provider-catalog.json | 50 ++++++ .../official-external-plugin-catalog.test.ts | 11 ++ 18 files changed, 700 insertions(+) create mode 100644 extensions/featherless/README.md create mode 100644 extensions/featherless/api.ts create mode 100644 extensions/featherless/featherless.live.test.ts create mode 100644 extensions/featherless/index.test.ts create mode 100644 extensions/featherless/index.ts create mode 100644 extensions/featherless/models.ts create mode 100644 extensions/featherless/npm-shrinkwrap.json create mode 100644 extensions/featherless/onboard.ts create mode 100644 extensions/featherless/openclaw.plugin.json create mode 100644 extensions/featherless/package.json create mode 100644 extensions/featherless/provider-catalog.ts create mode 100644 extensions/featherless/tsconfig.json diff --git a/.github/labeler.yml b/.github/labeler.yml index 8aa082e3f67e..f9a70b610191 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -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: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6694b803105c..0e7b2b5a2bdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. diff --git a/extensions/featherless/README.md b/extensions/featherless/README.md new file mode 100644 index 000000000000..6f98d318c8bb --- /dev/null +++ b/extensions/featherless/README.md @@ -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 for setup and configuration. diff --git a/extensions/featherless/api.ts b/extensions/featherless/api.ts new file mode 100644 index 000000000000..5ccf73951aec --- /dev/null +++ b/extensions/featherless/api.ts @@ -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"; diff --git a/extensions/featherless/featherless.live.test.ts b/extensions/featherless/featherless.live.test.ts new file mode 100644 index 000000000000..0993963892f6 --- /dev/null +++ b/extensions/featherless/featherless.live.test.ts @@ -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); +}); diff --git a/extensions/featherless/index.test.ts b/extensions/featherless/index.test.ts new file mode 100644 index 000000000000..f7aca4fe472d --- /dev/null +++ b/extensions/featherless/index.test.ts @@ -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"); + }); +}); diff --git a/extensions/featherless/index.ts b/extensions/featherless/index.ts new file mode 100644 index 000000000000..351793d7c1ae --- /dev/null +++ b/extensions/featherless/index.ts @@ -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, + }, +}); diff --git a/extensions/featherless/models.ts b/extensions/featherless/models.ts new file mode 100644 index 000000000000..5792006ea340 --- /dev/null +++ b/extensions/featherless/models.ts @@ -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)); +} diff --git a/extensions/featherless/npm-shrinkwrap.json b/extensions/featherless/npm-shrinkwrap.json new file mode 100644 index 000000000000..804b27d3a1f2 --- /dev/null +++ b/extensions/featherless/npm-shrinkwrap.json @@ -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" + } + } +} diff --git a/extensions/featherless/onboard.ts b/extensions/featherless/onboard.ts new file mode 100644 index 000000000000..68c8e120d9e8 --- /dev/null +++ b/extensions/featherless/onboard.ts @@ -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); +} diff --git a/extensions/featherless/openclaw.plugin.json b/extensions/featherless/openclaw.plugin.json new file mode 100644 index 000000000000..0323e0763304 --- /dev/null +++ b/extensions/featherless/openclaw.plugin.json @@ -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 ", + "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": {} + } +} diff --git a/extensions/featherless/package.json b/extensions/featherless/package.json new file mode 100644 index 000000000000..bf4f394abacb --- /dev/null +++ b/extensions/featherless/package.json @@ -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 + } + } +} diff --git a/extensions/featherless/provider-catalog.ts b/extensions/featherless/provider-catalog.ts new file mode 100644 index 000000000000..999c0e2b9e7b --- /dev/null +++ b/extensions/featherless/provider-catalog.ts @@ -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, + }); +} diff --git a/extensions/featherless/tsconfig.json b/extensions/featherless/tsconfig.json new file mode 100644 index 000000000000..b8a85a99ac3d --- /dev/null +++ b/extensions/featherless/tsconfig.json @@ -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" + ] +} diff --git a/package.json b/package.json index cda09f31c307..289ad4f37958 100644 --- a/package.json +++ b/package.json @@ -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/**", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9a140f8fec9..06e9a50e1fb0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/scripts/lib/official-external-provider-catalog.json b/scripts/lib/official-external-provider-catalog.json index 50aa25ad6b8e..a684997fa6c3 100644 --- a/scripts/lib/official-external-provider-catalog.json +++ b/scripts/lib/official-external-provider-catalog.json @@ -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 ", + "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", diff --git a/src/plugins/official-external-plugin-catalog.test.ts b/src/plugins/official-external-plugin-catalog.test.ts index 00819f43de79..892b5d19100f 100644 --- a/src/plugins/official-external-plugin-catalog.test.ts +++ b/src/plugins/official-external-plugin-catalog.test.ts @@ -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",