Files
openclaw/extensions/zai/index.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

402 lines
12 KiB
TypeScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {
definePluginEntry,
type ProviderAuthContext,
type ProviderAuthMethod,
type ProviderAuthMethodNonInteractiveContext,
type ProviderResolveDynamicModelContext,
type ProviderRuntimeModel,
type ProviderWrapStreamFnContext,
} from "openclaw/plugin-sdk/plugin-entry";
import {
applyAuthProfileConfig,
buildApiKeyCredential,
ensureApiKeyFromOptionEnvOrPrompt,
normalizeApiKeyInput,
normalizeOptionalSecretInput,
type SecretInput,
upsertAuthProfileWithLock,
validateApiKeyInput,
} from "openclaw/plugin-sdk/provider-auth-api-key";
import {
buildProviderReplayFamilyHooks,
normalizeModelCompat,
} from "openclaw/plugin-sdk/provider-model-shared";
import {
createPayloadPatchStreamWrapper,
createToolStreamWrapper,
defaultToolStreamExtraParams,
} from "openclaw/plugin-sdk/provider-stream-shared";
import { fetchZaiUsage } from "openclaw/plugin-sdk/provider-usage";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import { detectZaiEndpoint, type ZaiEndpointId } from "./detect.js";
import { zaiMediaUnderstandingProvider } from "./media-understanding-provider.js";
import { buildZaiModelDefinition } from "./model-definitions.js";
import { applyZaiConfig, applyZaiProviderConfig, ZAI_DEFAULT_MODEL_REF } from "./onboard.js";
const PROVIDER_ID = "zai";
const GLM5_TEMPLATE_MODEL_ID = "glm-4.7";
const PROFILE_ID = "zai:default";
type UpsertAuthProfileParams = Parameters<typeof upsertAuthProfileWithLock>[0];
function resolveDeprecatedPiAgentAuthPath(env: NodeJS.ProcessEnv): string {
const home = env.HOME?.trim() || env.USERPROFILE?.trim() || os.homedir();
return path.join(home, ".pi", "agent", "auth.json");
}
function resolveDeprecatedPiAgentAccessToken(
env: NodeJS.ProcessEnv,
providerIds: readonly string[],
): string | undefined {
try {
const authPath = resolveDeprecatedPiAgentAuthPath(env);
if (!fs.existsSync(authPath)) {
return undefined;
}
const parsed = JSON.parse(fs.readFileSync(authPath, "utf-8")) as Record<
string,
{ access?: unknown }
>;
for (const providerId of providerIds) {
const token = parsed[providerId]?.access;
if (typeof token === "string" && token.trim()) {
return token;
}
}
} catch {}
return undefined;
}
async function upsertAuthProfileWithLockOrThrow(params: UpsertAuthProfileParams): Promise<void> {
const updated = await upsertAuthProfileWithLock(params);
if (!updated) {
throw new Error(
"Failed to update auth profile store; the auth store lock may be busy. Wait a moment and retry.",
);
}
}
function resolveGlm5ForwardCompatModel(
ctx: ProviderResolveDynamicModelContext,
): ProviderRuntimeModel | undefined {
const trimmedModelId = ctx.modelId.trim();
if (!normalizeLowercaseStringOrEmpty(trimmedModelId).startsWith("glm-5")) {
return undefined;
}
const existing = ctx.modelRegistry.find(
PROVIDER_ID,
trimmedModelId,
) as ProviderRuntimeModel | null;
if (existing) {
return existing;
}
const def = buildZaiModelDefinition({ id: trimmedModelId });
const template = ctx.modelRegistry.find(
PROVIDER_ID,
GLM5_TEMPLATE_MODEL_ID,
) as ProviderRuntimeModel | null;
return normalizeModelCompat({
...template,
id: def.id,
name: def.name,
api: "openai-completions",
provider: PROVIDER_ID,
reasoning: def.reasoning,
input: def.input,
cost: def.cost,
contextWindow: def.contextWindow,
maxTokens: def.maxTokens,
} as ProviderRuntimeModel);
}
function resolveZaiDefaultModel(modelIdOverride?: string): string {
return modelIdOverride ? `zai/${modelIdOverride}` : ZAI_DEFAULT_MODEL_REF;
}
function isTrueParam(value: unknown): boolean {
return value === true;
}
function shouldPreserveZaiThinking(extraParams?: Record<string, unknown>): boolean {
return isTrueParam(extraParams?.preserveThinking) || isTrueParam(extraParams?.preserve_thinking);
}
function isDisabledThinkingLevel(thinkingLevel: ProviderWrapStreamFnContext["thinkingLevel"]) {
return thinkingLevel === "off";
}
function wrapZaiStreamFn(ctx: ProviderWrapStreamFnContext) {
let streamFn = createToolStreamWrapper(ctx.streamFn, ctx.extraParams?.tool_stream !== false);
const preserveThinking = shouldPreserveZaiThinking(ctx.extraParams);
if (!isDisabledThinkingLevel(ctx.thinkingLevel) && !preserveThinking) {
return streamFn;
}
streamFn = createPayloadPatchStreamWrapper(streamFn, ({ payload, model }) => {
if (model.api !== "openai-completions" || model.provider !== PROVIDER_ID) {
return;
}
if (isDisabledThinkingLevel(ctx.thinkingLevel)) {
payload.thinking = { type: "disabled" };
return;
}
if (preserveThinking) {
payload.thinking = { type: "enabled", clear_thinking: false };
}
});
return streamFn;
}
async function promptForZaiEndpoint(ctx: ProviderAuthContext): Promise<ZaiEndpointId> {
return await ctx.prompter.select<ZaiEndpointId>({
message: "Select Z.AI endpoint",
initialValue: "global",
options: [
{ value: "global", label: "Global", hint: "Z.AI Global (api.z.ai)" },
{ value: "cn", label: "CN", hint: "Z.AI CN (open.bigmodel.cn)" },
{
value: "coding-global",
label: "Coding-Plan-Global",
hint: "GLM Coding Plan Global (api.z.ai)",
},
{
value: "coding-cn",
label: "Coding-Plan-CN",
hint: "GLM Coding Plan CN (open.bigmodel.cn)",
},
],
});
}
async function runZaiApiKeyAuth(
ctx: ProviderAuthContext,
endpoint?: ZaiEndpointId,
): Promise<{
profiles: Array<{ profileId: string; credential: ReturnType<typeof buildApiKeyCredential> }>;
configPatch: ReturnType<typeof applyZaiProviderConfig>;
defaultModel: string;
notes?: string[];
}> {
let capturedSecretInput: SecretInput | undefined;
let capturedCredential = false;
let capturedMode: "plaintext" | "ref" | undefined;
const apiKey = await ensureApiKeyFromOptionEnvOrPrompt({
token:
normalizeOptionalSecretInput(ctx.opts?.zaiApiKey) ??
normalizeOptionalSecretInput(ctx.opts?.token),
tokenProvider: normalizeOptionalSecretInput(ctx.opts?.zaiApiKey)
? PROVIDER_ID
: normalizeOptionalSecretInput(ctx.opts?.tokenProvider),
secretInputMode:
ctx.allowSecretRefPrompt === false
? (ctx.secretInputMode ?? "plaintext")
: ctx.secretInputMode,
config: ctx.config,
expectedProviders: [PROVIDER_ID, "z-ai"],
provider: PROVIDER_ID,
envLabel: "ZAI_API_KEY",
promptMessage: "Enter Z.AI API key",
normalize: normalizeApiKeyInput,
validate: validateApiKeyInput,
prompter: ctx.prompter,
setCredential: async (key, mode) => {
capturedSecretInput = key;
capturedCredential = true;
capturedMode = mode;
},
});
if (!capturedCredential) {
throw new Error("Missing Z.AI API key.");
}
const credentialInput = capturedSecretInput ?? "";
const detected = await detectZaiEndpoint({ apiKey, ...(endpoint ? { endpoint } : {}) });
const modelIdOverride = detected?.modelId;
const nextEndpoint = detected?.endpoint ?? endpoint ?? (await promptForZaiEndpoint(ctx));
return {
profiles: [
{
profileId: PROFILE_ID,
credential: buildApiKeyCredential(
PROVIDER_ID,
credentialInput,
undefined,
capturedMode ? { secretInputMode: capturedMode } : undefined,
),
},
],
configPatch: applyZaiProviderConfig(ctx.config, {
...(nextEndpoint ? { endpoint: nextEndpoint } : {}),
...(modelIdOverride ? { modelId: modelIdOverride } : {}),
}),
defaultModel: resolveZaiDefaultModel(modelIdOverride),
...(detected?.note ? { notes: [detected.note] } : {}),
};
}
async function runZaiApiKeyAuthNonInteractive(
ctx: ProviderAuthMethodNonInteractiveContext,
endpoint?: ZaiEndpointId,
) {
const resolved = await ctx.resolveApiKey({
provider: PROVIDER_ID,
flagValue: normalizeOptionalSecretInput(ctx.opts.zaiApiKey),
flagName: "--zai-api-key",
envVar: "ZAI_API_KEY",
});
if (!resolved) {
return null;
}
const detected = await detectZaiEndpoint({
apiKey: resolved.key,
...(endpoint ? { endpoint } : {}),
});
const modelIdOverride = detected?.modelId;
const nextEndpoint = detected?.endpoint ?? endpoint;
if (resolved.source !== "profile") {
const credential = ctx.toApiKeyCredential({
provider: PROVIDER_ID,
resolved,
});
if (!credential) {
return null;
}
await upsertAuthProfileWithLockOrThrow({
profileId: PROFILE_ID,
credential,
agentDir: ctx.agentDir,
});
}
const next = applyAuthProfileConfig(ctx.config, {
profileId: PROFILE_ID,
provider: PROVIDER_ID,
mode: "api_key",
});
return applyZaiConfig(next, {
...(nextEndpoint ? { endpoint: nextEndpoint } : {}),
...(modelIdOverride ? { modelId: modelIdOverride } : {}),
});
}
function buildZaiApiKeyMethod(params: {
id: string;
choiceId: string;
choiceLabel: string;
choiceHint?: string;
endpoint?: ZaiEndpointId;
}): ProviderAuthMethod {
return {
id: params.id,
label: params.choiceLabel,
hint: params.choiceHint,
kind: "api_key",
wizard: {
choiceId: params.choiceId,
choiceLabel: params.choiceLabel,
...(params.choiceHint ? { choiceHint: params.choiceHint } : {}),
groupId: "zai",
groupLabel: "Z.AI",
groupHint: "GLM Coding Plan / Global / CN",
},
run: async (ctx) => await runZaiApiKeyAuth(ctx, params.endpoint),
runNonInteractive: async (ctx) => await runZaiApiKeyAuthNonInteractive(ctx, params.endpoint),
};
}
export default definePluginEntry({
id: PROVIDER_ID,
name: "Z.AI Provider",
description: "Bundled Z.AI provider plugin",
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Z.AI",
aliases: ["z-ai", "z.ai"],
docsPath: "/providers/models",
envVars: ["ZAI_API_KEY", "Z_AI_API_KEY"],
auth: [
buildZaiApiKeyMethod({
id: "api-key",
choiceId: "zai-api-key",
choiceLabel: "Z.AI API key",
}),
buildZaiApiKeyMethod({
id: "coding-global",
choiceId: "zai-coding-global",
choiceLabel: "Coding-Plan-Global",
choiceHint: "GLM Coding Plan Global (api.z.ai)",
endpoint: "coding-global",
}),
buildZaiApiKeyMethod({
id: "coding-cn",
choiceId: "zai-coding-cn",
choiceLabel: "Coding-Plan-CN",
choiceHint: "GLM Coding Plan CN (open.bigmodel.cn)",
endpoint: "coding-cn",
}),
buildZaiApiKeyMethod({
id: "global",
choiceId: "zai-global",
choiceLabel: "Global",
choiceHint: "Z.AI Global (api.z.ai)",
endpoint: "global",
}),
buildZaiApiKeyMethod({
id: "cn",
choiceId: "zai-cn",
choiceLabel: "CN",
choiceHint: "Z.AI CN (open.bigmodel.cn)",
endpoint: "cn",
}),
],
resolveDynamicModel: (ctx) => resolveGlm5ForwardCompatModel(ctx),
...buildProviderReplayFamilyHooks({
family: "openai-compatible",
dropReasoningFromHistory: false,
}),
prepareExtraParams: (ctx) => defaultToolStreamExtraParams(ctx.extraParams),
wrapStreamFn: (ctx) => wrapZaiStreamFn(ctx),
resolveThinkingProfile: () => ({
levels: [
{ id: "off", label: "off" },
{ id: "low", label: "on" },
],
defaultLevel: "off",
}),
isModernModelRef: ({ modelId }) => {
const lower = normalizeLowercaseStringOrEmpty(modelId);
return (
lower.startsWith("glm-5") ||
lower.startsWith("glm-4.7") ||
lower.startsWith("glm-4.7-flash") ||
lower.startsWith("glm-4.7-flashx")
);
},
resolveUsageAuth: async (ctx) => {
const apiKey = ctx.resolveApiKeyFromConfigAndStore({
providerIds: [PROVIDER_ID, "z-ai"],
envDirect: [ctx.env.ZAI_API_KEY, ctx.env.Z_AI_API_KEY],
});
if (apiKey) {
return { token: apiKey };
}
const legacyToken = resolveDeprecatedPiAgentAccessToken(ctx.env, ["z-ai", PROVIDER_ID]);
return legacyToken ? { token: legacyToken } : null;
},
fetchUsageSnapshot: async (ctx) => await fetchZaiUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn),
isCacheTtlEligible: () => true,
});
api.registerMediaUnderstandingProvider(zaiMediaUnderstandingProvider);
},
});