Files
openclaw/extensions/groq/index.test.ts
Peter Steinberger eec2c0cc26 refactor(providers): unify lifecycle-safe manifest registration (#114381)
* refactor(providers): canonicalize manifest-driven registration

* fix(zai): eliminate type-only SDK import side effects
2026-07-27 03:20:02 -04:00

379 lines
12 KiB
TypeScript

import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
import {
createAssistantMessageEventStream,
type SimpleStreamOptions,
} from "openclaw/plugin-sdk/llm";
// Groq tests cover index plugin behavior.
import { capturePluginRegistration } from "openclaw/plugin-sdk/plugin-test-runtime";
import { buildManifestModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-shared";
import { describe, expect, it } from "vitest";
import { resolveGroqReasoningCompatPatch } from "./api.js";
import plugin from "./index.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
describe("groq provider compat", () => {
it("recovers only matching implicit-budget rejections without changing normal tools", async () => {
const [provider] = capturePluginRegistration(plugin).providers;
const capturePayloads = async (
extraParams: Record<string, unknown> | undefined,
firstError?: string,
initialMaxTokens = 32_768,
onPayload?: SimpleStreamOptions["onPayload"],
streamMaxTokens?: number,
withTools = true,
modelParams?: Record<string, unknown>,
maxTokensSource: "configured" | "discovered" | null = "discovered",
) => {
const payloads: Array<Record<string, unknown>> = [];
const baseStreamFn: StreamFn = async (model, _context, options) => {
const payload: Record<string, unknown> = {
max_completion_tokens: initialMaxTokens,
};
if (withTools) {
payload.tools = [{ type: "function", function: { name: "read" } }];
payload.tool_choice = "auto";
}
const replacement = await options?.onPayload?.(payload, model);
const finalPayload =
replacement && typeof replacement === "object"
? (replacement as Record<string, unknown>)
: payload;
payloads.push(finalPayload);
const stream = createAssistantMessageEventStream();
if (payloads.length === 1 && firstError) {
stream.push({
type: "error",
reason: "error",
error: { stopReason: "error", errorMessage: firstError } as never,
});
} else {
stream.push({
type: "done",
reason: "stop",
message: { stopReason: "stop" } as never,
});
}
stream.end();
return stream;
};
const streamFn = provider?.wrapStreamFn?.({
provider: "groq",
modelId: "llama-3.3-70b-versatile",
extraParams,
model: {
maxTokens: initialMaxTokens,
...(maxTokensSource ? { maxTokensSource } : {}),
...(modelParams ? { params: modelParams } : {}),
} as never,
streamFn: baseStreamFn,
} as never);
const streamOptions: SimpleStreamOptions = {};
if (onPayload) {
streamOptions.onPayload = onPayload;
}
if (streamMaxTokens !== undefined) {
streamOptions.maxTokens = streamMaxTokens;
}
const stream = streamFn?.(
{} as never,
{ messages: [], ...(withTools ? { tools: [{ name: "read" }] } : {}) } as never,
streamOptions,
);
if (stream) {
const resolvedStream = await stream;
for await (const event of resolvedStream) {
void event;
// Drain the wrapper so a matching error can trigger its fallback attempt.
}
}
return payloads;
};
const successfulDefault = await capturePayloads(undefined);
expect(successfulDefault).toHaveLength(1);
expect(successfulDefault[0]).toMatchObject({
max_completion_tokens: 32_768,
tools: [{ type: "function", function: { name: "read" } }],
});
const matchingError =
"413 Request too large for model `llama-3.3-70b-versatile` on tokens per minute (TPM): Limit 12000, Requested 30000";
let payloadHookCalls = 0;
const recovered = await capturePayloads(undefined, matchingError, 32_768, (payload) => {
const record = payload as Record<string, unknown>;
payloadHookCalls += 1;
return payloadHookCalls === 1
? payload
: {
...record,
max_tokens: 65_536,
max_completion_tokens: 32_768,
tools: [{ type: "function", function: { name: "restored" } }],
tool_choice: "required",
};
});
expect(payloadHookCalls).toBe(2);
expect(recovered).toHaveLength(2);
expect(recovered[0]).toMatchObject({
max_completion_tokens: 32_768,
tools: [{ type: "function", function: { name: "read" } }],
});
expect(recovered[1]).toEqual({ max_completion_tokens: 1_024 });
const noTools = await capturePayloads(
undefined,
matchingError,
32_768,
undefined,
undefined,
false,
);
expect(noTools).toEqual([{ max_completion_tokens: 32_768 }, { max_completion_tokens: 1_024 }]);
payloadHookCalls = 0;
const alternateAlias = await capturePayloads(undefined, matchingError, 32_768, (payload) => {
const record = payload as Record<string, unknown>;
payloadHookCalls += 1;
return payloadHookCalls === 1
? payload
: {
...record,
max_tokens: 65_536,
tools: [{ type: "function", function: { name: "restored" } }],
};
});
expect(alternateAlias[1]).toEqual({ max_completion_tokens: 1_024 });
payloadHookCalls = 0;
const parallelToolAliasCollision = await capturePayloads(
undefined,
matchingError,
32_768,
(payload) => {
const record = payload as Record<string, unknown>;
payloadHookCalls += 1;
return payloadHookCalls === 1
? payload
: {
...record,
parallel_tool_calls: true,
parallelToolCalls: false,
};
},
);
expect(payloadHookCalls).toBe(2);
expect(parallelToolAliasCollision[1]).toEqual({ max_completion_tokens: 1_024 });
for (const unrelatedMessage of [
"401 Unauthorized",
"400 malformed request",
"413 context length exceeded",
"429 Rate limit reached on tokens per minute (TPM)",
]) {
const unrelatedError = await capturePayloads(undefined, unrelatedMessage);
expect(unrelatedError).toHaveLength(1);
expect(unrelatedError[0]).toHaveProperty("tools");
}
const explicit = { max_completion_tokens: 2_048 };
const explicitPayloads = await capturePayloads(explicit, matchingError, 2_048);
expect(explicitPayloads).toHaveLength(1);
expect(explicitPayloads[0]).toMatchObject({
max_completion_tokens: 2_048,
tools: [{ type: "function", function: { name: "read" } }],
tool_choice: "auto",
});
for (const modelParams of [
{ maxTokens: 4_096 },
{ max_completion_tokens: 4_096 },
{ max_tokens: 4_096 },
{ extra_body: { max_completion_tokens: 4_096 } },
{ extra_body: { max_tokens: 4_096 } },
{ extraBody: { max_completion_tokens: 4_096 } },
{ extraBody: { max_tokens: 4_096 } },
]) {
const resolvedModel = await capturePayloads(
undefined,
matchingError,
4_096,
undefined,
undefined,
true,
modelParams,
);
expect(resolvedModel).toHaveLength(1);
expect(resolvedModel[0]).toHaveProperty("max_completion_tokens", 4_096);
}
const configuredModelBudget = await capturePayloads(
undefined,
matchingError,
4_096,
undefined,
undefined,
true,
undefined,
"configured",
);
expect(configuredModelBudget).toHaveLength(1);
expect(configuredModelBudget[0]).toMatchObject({
max_completion_tokens: 4_096,
tools: [{ type: "function", function: { name: "read" } }],
});
const unknownModelBudget = await capturePayloads(
undefined,
matchingError,
4_096,
undefined,
undefined,
true,
undefined,
null,
);
expect(unknownModelBudget).toHaveLength(1);
expect(unknownModelBudget[0]).toMatchObject({
max_completion_tokens: 4_096,
tools: [{ type: "function", function: { name: "read" } }],
});
const requestScoped = await capturePayloads(undefined, matchingError, 4_096, undefined, 4_096);
expect(requestScoped).toHaveLength(1);
expect(requestScoped[0]).toMatchObject({
max_completion_tokens: 4_096,
tools: [{ type: "function", function: { name: "read" } }],
});
});
it("preserves synchronous throws and complete asynchronous error metadata", async () => {
const [provider] = capturePluginRegistration(plugin).providers;
if (!provider) {
throw new Error("expected Groq provider registration");
}
const model = {
api: "openai-completions",
provider: "groq",
id: "llama-3.3-70b-versatile",
} as never;
const context = { messages: [], tools: [{ name: "read" }] } as never;
const wrap = (streamFn: StreamFn) =>
provider.wrapStreamFn?.({
provider: "groq",
modelId: "llama-3.3-70b-versatile",
model: { maxTokensSource: "discovered" } as never,
streamFn,
} as never);
const synchronous = wrap(() => {
throw new Error("synchronous failure");
});
expect(() => synchronous?.(model, context, {})).toThrow("synchronous failure");
const asynchronous = wrap(async () => {
throw new Error("asynchronous failure");
});
const events = [];
const stream = asynchronous?.(model, context, {});
if (stream) {
const resolvedStream = await stream;
for await (const event of resolvedStream) {
events.push(event);
}
}
expect(events).toEqual([
{
type: "error",
reason: "error",
error: expect.objectContaining({
api: "openai-completions",
provider: "groq",
model: "llama-3.3-70b-versatile",
stopReason: "error",
errorMessage: "asynchronous failure",
timestamp: expect.any(Number),
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
}),
},
]);
});
it("maps Groq Qwen 3 reasoning to provider-native none/default values", () => {
expect(resolveGroqReasoningCompatPatch("qwen/qwen3.6-27b")).toEqual({
supportsReasoningEffort: true,
supportedReasoningEfforts: ["none", "default"],
reasoningEffortMap: {
adaptive: "default",
high: "default",
off: "none",
none: "none",
minimal: "default",
low: "default",
medium: "default",
max: "default",
xhigh: "default",
},
});
});
it("keeps GPT-OSS reasoning on the Groq low/medium/high contract", () => {
expect(resolveGroqReasoningCompatPatch("openai/gpt-oss-120b")).toEqual({
supportsReasoningEffort: true,
supportedReasoningEfforts: ["low", "medium", "high"],
});
});
it("registers Groq model and media providers", async () => {
const captured = capturePluginRegistration(plugin);
const [provider] = captured.providers;
if (!provider) {
throw new Error("Expected Groq provider");
}
expect(provider).toMatchObject({
docsPath: "/providers/groq",
envVars: ["GROQ_API_KEY"],
id: "groq",
label: "Groq",
});
expect(provider.auth).toHaveLength(1);
expect(provider.auth[0]).toMatchObject({
id: "api-key",
kind: "api_key",
label: "Groq API key",
starterModel: `groq/${manifest.modelCatalog.providers.groq.defaultModel}`,
wizard: {
choiceId: "groq-api-key",
groupId: "groq",
},
});
expect(await provider.staticCatalog?.run({} as never)).toEqual({
provider: buildManifestModelProviderConfig({
providerId: "groq",
catalog: manifest.modelCatalog.providers.groq,
}),
});
expect(captured.modelCatalogProviders.map((entry) => entry.kinds)).toEqual([["text"]]);
expect(captured.mediaUnderstandingProviders).toHaveLength(1);
const [mediaProvider] = captured.mediaUnderstandingProviders;
if (!mediaProvider) {
throw new Error("Expected Groq media understanding provider");
}
const { transcribeAudio, ...mediaProviderMetadata } = mediaProvider;
expect(mediaProviderMetadata).toEqual({
autoPriority: { audio: 20 },
capabilities: ["audio"],
defaultModels: { audio: "whisper-large-v3-turbo" },
id: "groq",
});
expect(transcribeAudio).toBeTypeOf("function");
});
});