mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-10 00:11:19 +00:00
170 lines
5.8 KiB
TypeScript
170 lines
5.8 KiB
TypeScript
// 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("applies provider compat to configured models without overriding explicit values", async () => {
|
|
const provider = await registerSingleProviderPlugin(featherlessPlugin);
|
|
const normalized = provider.normalizeResolvedModel?.({
|
|
provider: "featherless",
|
|
modelId: "google/gemma-3-27b-it",
|
|
model: {
|
|
...createDefaultRuntimeModel(),
|
|
id: "google/gemma-3-27b-it",
|
|
name: "Gemma 3 27B",
|
|
compat: {
|
|
supportsStore: true,
|
|
thinkingFormat: "deepseek",
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(normalized?.compat).toMatchObject({
|
|
...FEATHERLESS_DYNAMIC_COMPAT,
|
|
supportsStore: true,
|
|
thinkingFormat: "deepseek",
|
|
});
|
|
});
|
|
|
|
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");
|
|
});
|
|
});
|