mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-02 02:04:58 +00:00
* 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
402 lines
12 KiB
TypeScript
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);
|
|
},
|
|
});
|