openclaw/extensions/featherless/featherless.live.test.ts
2026-07-06 16:17:44 -07:00

114 lines
3.4 KiB
TypeScript

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