Files
openclaw/extensions/zai/index.test.ts
Peter Steinberger bb46b79d3c refactor: internalize OpenClaw agent runtime (#85341)
* refactor: extract agent core package

Introduce packages/agent-core as the OpenClaw-owned home for reusable agent loop, harness, session, prompt, and runtime dependency contracts.

* refactor: extract shared llm runtime

Move provider model registries, stream wrappers, OAuth helpers, and LLM utilities into src/llm with plugin-sdk barrels instead of depending on the old embedded runtime layout.

* refactor: remove pi runtime internals

Rename remaining Pi-shaped agent surfaces to OpenClaw agent runtime names, delete obsolete Pi docs and package graph checks, and add the third-party notice for incorporated code.

* refactor: tighten agent session runtime

Make agent-core/runtime dependencies explicit, consolidate compaction and session transcript helpers, and move model/session helpers behind OpenClaw-owned contracts.

* refactor: remove static model and pi auth paths

Drop static model catalogs and Pi auth bridges, move model/provider facts to manifest-owned runtime contracts, and harden internal embedded-agent utilities.

* refactor: remove legacy provider compat paths

* docs: remove agent parity notes

* fix: skip provider wildcard metadata parsing

* refactor: share session extension sdk loading

* refactor: inline acpx proxy error formatter

* refactor: fold edit recovery into edit tool

* fix: accept extension batch separator

* test: align startup provider plugin expectations

* fix: restore provider-scoped release discovery

* test: align static asset packaging expectations

* fix: run static provider catalogs during scoped discovery

* fix: add provider entry catalogs for scoped live discovery

* fix: load lightweight provider catalog entries

* fix: refresh provider-scoped plugin metadata

* fix: keep provider catalog entries on release live path

* fix: keep static manifest models in release live checks

* fix: harden release model discovery

* fix: reduce OpenAI live cache probe reasoning

* fix: disable OpenAI cache probe reasoning

* ci: extend OpenAI gateway live timeout

* fix: extend live gateway model budget

* fix: stabilize release validation regressions

* fix: honor provider aliases in model rows

* fix: stabilize release validation lanes

* fix: stabilize release memory qa

* ci: stabilize release validation lanes

* ci: prefer ipv4 for live docker node calls

* fix: restore shared tool-call stream wrapper

* ci: remove legacy pi test shard alias

* fix: clean up embedded agent test drift

* fix: stabilize runtime alias status

* fix: clean up embedded agent ci drift

* fix: restore release ci invariants

* fix: clean up post-rebase runtime drift

* fix: restore release ci checks

* fix: restore release ci after rebase

* fix: remove stale pi runtime path

* test: align compaction runtime expectations

* test: update plugin prerelease expectations

* fix: handle claude live tool approvals

* fix: stabilize release validation gates

* fix: finish agent runtime import

* test: finish post-rebase agent runtime mocks

* fix: keep codex compaction native

* fix: stabilize codex app-server hook tests

* test: isolate codex diagnostic active run

* test: remove codex diagnostic completion race

# Conflicts:
#	extensions/codex/src/app-server/run-attempt.test.ts

* ci: fix full release manifest performance run id

* refactor: narrow llm plugin sdk boundary

* chore: drop generated google boundary stamps

* fix: repair rebase fallout

* fix: clean up rebased runtime references

* fix: decode codex jwt payloads as base64url

* fix: preserve shipped pi runtime alias

* fix: add scoped sdk virtual modules

* fix: decode llm codex oauth jwt as base64url

* fix: avoid stale vertex adc negative cache

* fix: harden tool arg decoding and codeql path

* fix: keep vertex adc negative checks live

* refactor: consolidate codex jwt and edit helpers

* fix: await codex oauth node runtime imports

* fix: preserve sdk tool and notice contracts

* fix: preserve shipped compat config boundaries

* fix: align codex oauth callback host

* fix: terminate agent-core loop streams on failure

* fix: keep codex oauth callback alive during fallback

* ci: include session tools in critical codeql scans

* fix: keep Cloudflare Anthropic provider auth header

* docs: redirect legacy pi runtime pages

* fix: honor bundled web provider compat discovery

* fix: protect session output spill files

* fix: keep legacy agent dir env blocked

* fix: contain auto-discovered skill symlinks

* fix: harden agent core sdk proxy surfaces

* fix: restore approval reaction sdk compat

* fix: keep live docker runs bounded

* fix: keep codex oauth redirect host aligned

* fix: resolve post-rebase agent runtime drift

* fix: redact anthropic oauth parse failures

* fix: preserve responses strict tool shaping

* fix: repair agent runtime rebase cleanup

* docs: redirect retired parity pages

* fix: bound auto-discovered resources to roots

* fix: repair post-rebase agent test drift

* fix: preserve bundled provider allowlist migration

* fix: preserve manifest-owned provider aliases

* fix: declare photon image dependency

* fix: keep provider headers out of proxy body

* fix: preserve shipped env aliases

* fix: refresh control ui i18n generated state

* fix: quote read fallback paths

* fix: preview edits through configured backend

* test: satisfy core test typecheck

* fix: preserve ZAI usage auth fallback

* test: repair codex diagnostic test

* fix: repair agent runtime rebase drift

* test: finish embedded runner import rename

* fix: repair agent runtime rebase integrations

* test: align compaction oauth fallback expectations

* fix: allow sdk-auth session models

* fix: update doctor tool schema import

* fix: preserve bedrock plugin region

* fix: stream harmony-like prose immediately

* ci: include session runtime in codeql shards

* fix: repair latest rebase integrations

* fix: honor explicit codex websocket transport

* fix: keep openai-compatible credentials provider-scoped

* fix: refresh sdk api baseline after rebase

* fix: route cli runtime aliases through openclaw harness

* test: rename stale harness mock expectation

* test: rename embedded agent overflow calls

* test: clean embedded auth test wording

* test: use openclaw stream types in deepinfra cache test

* fix: refresh sdk api baseline on latest main

* fix: honor bundled discovery compat allowlists

* fix: refresh sdk api baseline after latest rebase

* fix: remove stale rebase imports

* test: rename stale model catalog mock

* test: mock renamed doctor runtime modules

* fix: map canonical kimi env auth

* fix: use internal model registry in bench script

* fix: migrate deepinfra provider catalog entry

* fix: enforce builtin tool suppression

* fix: route compaction auth and proxy payloads safely

* refactor: prune unused llm registry leftovers

* test: update codex hooks session import

* test: fix model picker ci coverage

* test: align model picker auth mock types
2026-05-27 19:24:04 +01:00

430 lines
13 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
import type { Context, Model } from "openclaw/plugin-sdk/llm";
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,
};
}
function expectReplayPolicyFields(
policy: Record<string, unknown> | undefined,
fields: Record<string, unknown>,
): void {
if (!policy) {
throw new Error("Expected replay policy");
}
for (const [key, value] of Object.entries(fields)) {
expect(policy[key]).toEqual(value);
}
}
function expectModelFields(
model: Record<string, unknown> | undefined,
fields: Record<string, unknown>,
): void {
if (!model) {
throw new Error("Expected provider model");
}
for (const [key, value] of Object.entries(fields)) {
expect(model[key]).toEqual(value);
}
}
describe("zai provider plugin", () => {
it("owns replay policy for OpenAI-compatible Z.ai transports", async () => {
const provider = await registerSingleProviderPlugin(plugin);
expectReplayPolicyFields(
provider.buildReplayPolicy?.({
provider: "zai",
modelApi: "openai-completions",
modelId: "glm-5.1",
} as never) as Record<string, unknown> | undefined,
{
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
applyAssistantFirstOrderingFix: true,
validateGeminiTurns: true,
validateAnthropicTurns: true,
},
);
expectReplayPolicyFields(
provider.buildReplayPolicy?.({
provider: "zai",
modelApi: "openai-responses",
modelId: "glm-5.1",
} as never) as Record<string, unknown> | undefined,
{
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) {
const resolved = provider.resolveDynamicModel?.({
provider: "zai",
modelId: testCase.modelId,
modelRegistry: {
find: (_provider: string, modelId: string) => (modelId === "glm-4.7" ? template : null),
},
} as never) as Record<string, unknown> | undefined;
expectModelFields(resolved, {
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();
const resolved = provider.resolveDynamicModel?.({
provider: "zai",
modelId: "glm-5-turbo",
modelRegistry: {
find: (_provider: string, modelId: string) => (modelId === "glm-4.7" ? template : null),
},
} as never) as Record<string, unknown> | undefined;
expectModelFields(resolved, {
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?.tool_stream).toBe(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?.tool_stream).toBe(true);
expect(capturedPayload?.thinking).toEqual({ 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?.tool_stream).toBe(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?.tool_stream).toBe(true);
expect(capturedPayload?.thinking).toEqual({ 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?.thinking).toEqual({ type: "enabled", clear_thinking: false });
const assistantMessage = (capturedPayload?.messages as Array<Record<string, unknown>>)[1];
expect(assistantMessage?.role).toBe("assistant");
expect(assistantMessage?.content).toBe("visible reply");
expect(assistantMessage?.reasoning_content).toBe("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);
});
it("uses deprecated pi agent auth.json for usage auth when modern sources are empty", async () => {
const home = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-zai-legacy-auth-"));
try {
const authDir = path.join(home, ".pi", "agent");
await fs.mkdir(authDir, { recursive: true });
await fs.writeFile(
path.join(authDir, "auth.json"),
`${JSON.stringify({ "z-ai": { access: "legacy-zai-token" } }, null, 2)}\n`,
"utf-8",
);
const provider = await registerSingleProviderPlugin(plugin);
await expect(
provider.resolveUsageAuth?.({
env: { HOME: home },
resolveApiKeyFromConfigAndStore: () => undefined,
} as never),
).resolves.toEqual({ token: "legacy-zai-token" });
} finally {
await fs.rm(home, { recursive: true, force: true });
}
});
});