mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
389 lines
11 KiB
TypeScript
389 lines
11 KiB
TypeScript
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
|
import type { Context, Model } from "@mariozechner/pi-ai";
|
|
import { registerSingleProviderPlugin } from "openclaw/plugin-sdk/plugin-test-runtime";
|
|
import { buildOpenAICompletionsParams } from "openclaw/plugin-sdk/provider-transport-runtime";
|
|
import { describe, expect, it } from "vitest";
|
|
import plugin from "./index.js";
|
|
|
|
function createGlm47Template() {
|
|
return {
|
|
id: "glm-4.7",
|
|
name: "GLM-4.7",
|
|
provider: "zai",
|
|
api: "openai-completions",
|
|
baseUrl: "https://api.z.ai/api/paas/v4",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0.6, output: 2.2, cacheRead: 0.11, cacheWrite: 0 },
|
|
contextWindow: 204800,
|
|
maxTokens: 131072,
|
|
};
|
|
}
|
|
|
|
describe("zai provider plugin", () => {
|
|
it("owns replay policy for OpenAI-compatible Z.ai transports", async () => {
|
|
const provider = await registerSingleProviderPlugin(plugin);
|
|
|
|
expect(
|
|
provider.buildReplayPolicy?.({
|
|
provider: "zai",
|
|
modelApi: "openai-completions",
|
|
modelId: "glm-5.1",
|
|
} as never),
|
|
).toMatchObject({
|
|
sanitizeToolCallIds: true,
|
|
toolCallIdMode: "strict",
|
|
applyAssistantFirstOrderingFix: true,
|
|
validateGeminiTurns: true,
|
|
validateAnthropicTurns: true,
|
|
});
|
|
|
|
expect(
|
|
provider.buildReplayPolicy?.({
|
|
provider: "zai",
|
|
modelApi: "openai-responses",
|
|
modelId: "glm-5.1",
|
|
} as never),
|
|
).toMatchObject({
|
|
sanitizeToolCallIds: true,
|
|
toolCallIdMode: "strict",
|
|
applyAssistantFirstOrderingFix: false,
|
|
validateGeminiTurns: false,
|
|
validateAnthropicTurns: false,
|
|
});
|
|
});
|
|
|
|
it("resolves persisted GLM-5 family models with provider-owned metadata", async () => {
|
|
const provider = await registerSingleProviderPlugin(plugin);
|
|
const template = createGlm47Template();
|
|
|
|
const cases = [
|
|
{
|
|
modelId: "glm-5.1",
|
|
expected: {
|
|
input: ["text"],
|
|
reasoning: true,
|
|
contextWindow: 202800,
|
|
maxTokens: 131100,
|
|
},
|
|
},
|
|
{
|
|
modelId: "glm-5v-turbo",
|
|
expected: {
|
|
input: ["text", "image"],
|
|
reasoning: true,
|
|
contextWindow: 202800,
|
|
maxTokens: 131100,
|
|
},
|
|
},
|
|
] as const;
|
|
|
|
for (const testCase of cases) {
|
|
expect(
|
|
provider.resolveDynamicModel?.({
|
|
provider: "zai",
|
|
modelId: testCase.modelId,
|
|
modelRegistry: {
|
|
find: (_provider: string, modelId: string) => (modelId === "glm-4.7" ? template : null),
|
|
},
|
|
} as never),
|
|
).toMatchObject({
|
|
provider: "zai",
|
|
api: "openai-completions",
|
|
baseUrl: "https://api.z.ai/api/paas/v4",
|
|
id: testCase.modelId,
|
|
...testCase.expected,
|
|
});
|
|
}
|
|
});
|
|
|
|
it("returns an already-registered GLM-5 variant as-is", async () => {
|
|
const provider = await registerSingleProviderPlugin(plugin);
|
|
const registered = {
|
|
id: "glm-5-turbo",
|
|
name: "GLM-5-Turbo",
|
|
provider: "zai",
|
|
api: "openai-completions",
|
|
baseUrl: "https://api.z.ai/api/paas/v4",
|
|
reasoning: false,
|
|
input: ["text"],
|
|
cost: { input: 0.1, output: 0.2, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 123456,
|
|
maxTokens: 54321,
|
|
};
|
|
const template = createGlm47Template();
|
|
|
|
expect(
|
|
provider.resolveDynamicModel?.({
|
|
provider: "zai",
|
|
modelId: "glm-5-turbo",
|
|
modelRegistry: {
|
|
find: (_provider: string, modelId: string) =>
|
|
modelId === "glm-5-turbo" ? registered : modelId === "glm-4.7" ? template : null,
|
|
},
|
|
} as never),
|
|
).toEqual(registered);
|
|
});
|
|
|
|
it("still synthesizes unknown GLM-5 variants from the GLM-4.7 template", async () => {
|
|
const provider = await registerSingleProviderPlugin(plugin);
|
|
const template = createGlm47Template();
|
|
|
|
expect(
|
|
provider.resolveDynamicModel?.({
|
|
provider: "zai",
|
|
modelId: "glm-5-turbo",
|
|
modelRegistry: {
|
|
find: (_provider: string, modelId: string) => (modelId === "glm-4.7" ? template : null),
|
|
},
|
|
} as never),
|
|
).toMatchObject({
|
|
id: "glm-5-turbo",
|
|
name: "GLM-5 Turbo",
|
|
provider: "zai",
|
|
api: "openai-completions",
|
|
baseUrl: "https://api.z.ai/api/paas/v4",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
});
|
|
});
|
|
|
|
it("wires tool-stream defaults through the shared stream family hook", async () => {
|
|
const provider = await registerSingleProviderPlugin(plugin);
|
|
let capturedPayload: Record<string, unknown> | undefined;
|
|
const baseStreamFn: StreamFn = (model, _context, options) => {
|
|
const payload: Record<string, unknown> = {};
|
|
options?.onPayload?.(payload as never, model as never);
|
|
capturedPayload = payload;
|
|
return {} as ReturnType<StreamFn>;
|
|
};
|
|
|
|
const defaultWrapped = provider.wrapStreamFn?.({
|
|
provider: "zai",
|
|
modelId: "glm-5.1",
|
|
extraParams: {},
|
|
streamFn: baseStreamFn,
|
|
} as never);
|
|
|
|
void defaultWrapped?.(
|
|
{
|
|
api: "openai-completions",
|
|
provider: "zai",
|
|
id: "glm-5.1",
|
|
} as Model<"openai-completions">,
|
|
{ messages: [] } as Context,
|
|
{},
|
|
);
|
|
|
|
expect(capturedPayload).toMatchObject({
|
|
tool_stream: true,
|
|
});
|
|
|
|
const disabledWrapped = provider.wrapStreamFn?.({
|
|
provider: "zai",
|
|
modelId: "glm-5.1",
|
|
extraParams: { tool_stream: false },
|
|
streamFn: baseStreamFn,
|
|
} as never);
|
|
|
|
void disabledWrapped?.(
|
|
{
|
|
api: "openai-completions",
|
|
provider: "zai",
|
|
id: "glm-5.1",
|
|
} as Model<"openai-completions">,
|
|
{ messages: [] } as Context,
|
|
{},
|
|
);
|
|
|
|
expect(capturedPayload).not.toHaveProperty("tool_stream");
|
|
});
|
|
|
|
it("maps thinking off to Z.AI thinking disabled", async () => {
|
|
const provider = await registerSingleProviderPlugin(plugin);
|
|
let capturedPayload: Record<string, unknown> | undefined;
|
|
const baseStreamFn: StreamFn = (model, _context, options) => {
|
|
const payload: Record<string, unknown> = {};
|
|
options?.onPayload?.(payload as never, model as never);
|
|
capturedPayload = payload;
|
|
return {} as ReturnType<StreamFn>;
|
|
};
|
|
|
|
const wrapped = provider.wrapStreamFn?.({
|
|
provider: "zai",
|
|
modelId: "glm-5.1",
|
|
extraParams: {},
|
|
thinkingLevel: "off",
|
|
streamFn: baseStreamFn,
|
|
} as never);
|
|
|
|
void wrapped?.(
|
|
{
|
|
api: "openai-completions",
|
|
provider: "zai",
|
|
id: "glm-5.1",
|
|
} as Model<"openai-completions">,
|
|
{ messages: [] } as Context,
|
|
{},
|
|
);
|
|
|
|
expect(capturedPayload).toMatchObject({
|
|
tool_stream: true,
|
|
thinking: { type: "disabled" },
|
|
});
|
|
});
|
|
|
|
it("enables Z.AI preserved thinking only when requested", async () => {
|
|
const provider = await registerSingleProviderPlugin(plugin);
|
|
let capturedPayload: Record<string, unknown> | undefined;
|
|
const baseStreamFn: StreamFn = (model, _context, options) => {
|
|
const payload: Record<string, unknown> = {};
|
|
options?.onPayload?.(payload as never, model as never);
|
|
capturedPayload = payload;
|
|
return {} as ReturnType<StreamFn>;
|
|
};
|
|
|
|
const wrappedWithoutPreserve = provider.wrapStreamFn?.({
|
|
provider: "zai",
|
|
modelId: "glm-5.1",
|
|
extraParams: {},
|
|
thinkingLevel: "low",
|
|
streamFn: baseStreamFn,
|
|
} as never);
|
|
|
|
void wrappedWithoutPreserve?.(
|
|
{
|
|
api: "openai-completions",
|
|
provider: "zai",
|
|
id: "glm-5.1",
|
|
} as Model<"openai-completions">,
|
|
{ messages: [] } as Context,
|
|
{},
|
|
);
|
|
|
|
expect(capturedPayload).toMatchObject({ tool_stream: true });
|
|
expect(capturedPayload).not.toHaveProperty("thinking");
|
|
|
|
const wrappedWithPreserve = provider.wrapStreamFn?.({
|
|
provider: "zai",
|
|
modelId: "glm-5.1",
|
|
extraParams: { preserveThinking: true },
|
|
thinkingLevel: "low",
|
|
streamFn: baseStreamFn,
|
|
} as never);
|
|
|
|
void wrappedWithPreserve?.(
|
|
{
|
|
api: "openai-completions",
|
|
provider: "zai",
|
|
id: "glm-5.1",
|
|
} as Model<"openai-completions">,
|
|
{ messages: [] } as Context,
|
|
{},
|
|
);
|
|
|
|
expect(capturedPayload).toMatchObject({
|
|
tool_stream: true,
|
|
thinking: { type: "enabled", clear_thinking: false },
|
|
});
|
|
});
|
|
|
|
it("preserves replayed reasoning_content for Z.AI preserved thinking", async () => {
|
|
const provider = await registerSingleProviderPlugin(plugin);
|
|
let capturedPayload: Record<string, unknown> | undefined;
|
|
const model = {
|
|
provider: "zai",
|
|
id: "glm-5.1",
|
|
name: "GLM 5.1",
|
|
api: "openai-completions",
|
|
baseUrl: "https://api.z.ai/api/paas/v4",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 200_000,
|
|
maxTokens: 131_072,
|
|
} as Model<"openai-completions">;
|
|
const context = {
|
|
messages: [
|
|
{ role: "user", content: "hi", timestamp: 1 },
|
|
{
|
|
role: "assistant",
|
|
api: "openai-completions",
|
|
provider: "zai",
|
|
model: "glm-5.1",
|
|
content: [
|
|
{
|
|
type: "thinking",
|
|
thinking: "prior reasoning",
|
|
thinkingSignature: "reasoning_content",
|
|
},
|
|
{ type: "text", text: "visible reply" },
|
|
],
|
|
usage: {
|
|
input: 0,
|
|
output: 0,
|
|
cacheRead: 0,
|
|
cacheWrite: 0,
|
|
totalTokens: 0,
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
},
|
|
stopReason: "stop",
|
|
timestamp: 2,
|
|
},
|
|
{ role: "user", content: "continue", timestamp: 3 },
|
|
],
|
|
} as Context;
|
|
const baseStreamFn: StreamFn = (streamModel, streamContext, options) => {
|
|
const payload = buildOpenAICompletionsParams(streamModel as never, streamContext, {
|
|
reasoning: "high",
|
|
} as never);
|
|
options?.onPayload?.(payload as never, streamModel as never);
|
|
capturedPayload = payload;
|
|
return {} as ReturnType<StreamFn>;
|
|
};
|
|
|
|
const wrapped = provider.wrapStreamFn?.({
|
|
provider: "zai",
|
|
modelId: "glm-5.1",
|
|
extraParams: { preserve_thinking: true },
|
|
thinkingLevel: "low",
|
|
streamFn: baseStreamFn,
|
|
} as never);
|
|
|
|
void wrapped?.(model, context, {});
|
|
|
|
expect(capturedPayload).toMatchObject({
|
|
thinking: { type: "enabled", clear_thinking: false },
|
|
});
|
|
expect((capturedPayload?.messages as Array<Record<string, unknown>>)[1]).toMatchObject({
|
|
role: "assistant",
|
|
content: "visible reply",
|
|
reasoning_content: "prior reasoning",
|
|
});
|
|
});
|
|
|
|
it("defaults tool_stream extra params but preserves explicit values", async () => {
|
|
const provider = await registerSingleProviderPlugin(plugin);
|
|
|
|
expect(
|
|
provider.prepareExtraParams?.({
|
|
provider: "zai",
|
|
modelId: "glm-4.7",
|
|
extraParams: { endpoint: "global" },
|
|
} as never),
|
|
).toEqual({
|
|
endpoint: "global",
|
|
tool_stream: true,
|
|
});
|
|
|
|
const explicit = { endpoint: "global", tool_stream: false };
|
|
expect(
|
|
provider.prepareExtraParams?.({
|
|
provider: "zai",
|
|
modelId: "glm-4.7",
|
|
extraParams: explicit,
|
|
} as never),
|
|
).toBe(explicit);
|
|
});
|
|
});
|