mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
feat(provider): add Featherless AI integration
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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/**",
|
||||
|
||||
Generated
+6
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user