openclaw/extensions/featherless/index.test.ts

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");
});
});