mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-02 15:21:04 +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
280 lines
9.4 KiB
TypeScript
280 lines
9.4 KiB
TypeScript
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
|
import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api";
|
|
import {
|
|
registerProviderPlugin,
|
|
registerSingleProviderPlugin,
|
|
} from "openclaw/plugin-sdk/plugin-test-runtime";
|
|
import { describe, expect, it } from "vitest";
|
|
import plugin from "./index.js";
|
|
import setupPlugin from "./setup-api.js";
|
|
import {
|
|
createXaiPayloadCaptureStream,
|
|
expectXaiFastToolStreamShaping,
|
|
runXaiGrok4ResponseStream,
|
|
} from "./test-helpers.js";
|
|
|
|
function createProviderModel(overrides: {
|
|
id: string;
|
|
api?: string;
|
|
baseUrl?: string;
|
|
provider?: string;
|
|
}) {
|
|
return {
|
|
id: overrides.id,
|
|
name: overrides.id,
|
|
api: overrides.api ?? "openai-completions",
|
|
provider: overrides.provider ?? "xai",
|
|
baseUrl: overrides.baseUrl ?? "https://api.x.ai/v1",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 200_000,
|
|
maxTokens: 8_192,
|
|
};
|
|
}
|
|
|
|
type XaiAutoEnableProbe = Parameters<OpenClawPluginApi["registerAutoEnableProbe"]>[0];
|
|
|
|
function registerXaiAutoEnableProbe(): XaiAutoEnableProbe {
|
|
const probes: XaiAutoEnableProbe[] = [];
|
|
setupPlugin.register(
|
|
createTestPluginApi({
|
|
registerAutoEnableProbe(probe) {
|
|
probes.push(probe);
|
|
},
|
|
}),
|
|
);
|
|
const probe = probes[0];
|
|
if (!probe) {
|
|
throw new Error("expected xAI setup plugin to register an auto-enable probe");
|
|
}
|
|
return probe;
|
|
}
|
|
|
|
function requireEntry<T extends { id?: string }>(entries: T[], id: string): T {
|
|
const entry = entries.find((candidate) => candidate.id === id);
|
|
if (!entry) {
|
|
throw new Error(`Expected entry ${id}`);
|
|
}
|
|
return entry;
|
|
}
|
|
|
|
describe("xai provider plugin", () => {
|
|
it("exposes OAuth and device-code auth choices", async () => {
|
|
const provider = await registerSingleProviderPlugin(plugin);
|
|
|
|
expect(provider.auth?.map((method) => method.id)).toEqual(["api-key", "oauth", "device-code"]);
|
|
const deviceCode = provider.auth?.find((method) => method.id === "device-code");
|
|
expect(deviceCode?.kind).toBe("device_code");
|
|
expect(deviceCode?.wizard?.choiceId).toBe("xai-device-code");
|
|
});
|
|
|
|
it("classifies Grok usage and spending limit errors", async () => {
|
|
const provider = await registerSingleProviderPlugin(plugin);
|
|
|
|
expect(
|
|
provider.classifyFailoverReason?.({
|
|
errorMessage:
|
|
'403 {"code":"The caller does not have permission to execute the specified operation","error":"Your team team-redacted has either used all available credits or reached its monthly spending limit. To continue making API requests, please purchase more credits or raise your spending limit."}',
|
|
}),
|
|
).toBe("billing");
|
|
expect(
|
|
provider.classifyFailoverReason?.({
|
|
errorMessage:
|
|
'429 {"code":"Some resource has been exhausted","error":"Your team team-redacted has either used all available credits or reached its monthly spending limit. To continue making API requests, please purchase more credits or raise your spending limit."}',
|
|
}),
|
|
).toBe("billing");
|
|
expect(
|
|
provider.classifyFailoverReason?.({
|
|
errorMessage:
|
|
'429 {"code":"Some resource has been exhausted","error":"Rate limit exceeded"}',
|
|
}),
|
|
).toBe("rate_limit");
|
|
expect(
|
|
provider.classifyFailoverReason?.({
|
|
errorMessage:
|
|
'400 {"code":"Client specified an invalid argument","error":"Incorrect API key provided: xa***en. You can obtain an API key from https://console.x.ai."}',
|
|
}),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("registers xAI speech providers for batch and streaming STT", async () => {
|
|
const { mediaProviders, realtimeTranscriptionProviders } = await registerProviderPlugin({
|
|
plugin,
|
|
id: "xai",
|
|
name: "xAI Provider",
|
|
});
|
|
|
|
const mediaProvider = requireEntry(mediaProviders, "xai");
|
|
expect(mediaProvider.capabilities).toEqual(["audio"]);
|
|
expect(mediaProvider.defaultModels).toEqual({ audio: "grok-stt" });
|
|
const realtimeProvider = requireEntry(realtimeTranscriptionProviders, "xai");
|
|
expect(realtimeProvider.label).toBe("xAI Realtime Transcription");
|
|
expect(realtimeProvider.aliases).toContain("xai-realtime");
|
|
});
|
|
|
|
it("declares setup auto-enable reasons for plugin-owned tool config", () => {
|
|
const probe = registerXaiAutoEnableProbe();
|
|
|
|
expect(
|
|
probe({
|
|
config: { plugins: { entries: { xai: { config: { xSearch: { enabled: true } } } } } },
|
|
env: {},
|
|
}),
|
|
).toBe("xai tool configured");
|
|
expect(
|
|
probe({
|
|
config: {
|
|
plugins: { entries: { xai: { config: { codeExecution: { enabled: true } } } } },
|
|
},
|
|
env: {},
|
|
}),
|
|
).toBe("xai tool configured");
|
|
expect(probe({ config: {}, env: {} })).toBeNull();
|
|
});
|
|
|
|
it("owns replay policy for xAI OpenAI-compatible transports", async () => {
|
|
const provider = await registerSingleProviderPlugin(plugin);
|
|
|
|
const completionsPolicy = provider.buildReplayPolicy?.({
|
|
provider: "xai",
|
|
modelApi: "openai-completions",
|
|
modelId: "grok-3",
|
|
} as never);
|
|
expect(completionsPolicy?.sanitizeToolCallIds).toBe(true);
|
|
expect(completionsPolicy?.toolCallIdMode).toBe("strict");
|
|
expect(completionsPolicy?.applyAssistantFirstOrderingFix).toBe(true);
|
|
expect(completionsPolicy?.validateGeminiTurns).toBe(true);
|
|
expect(completionsPolicy?.validateAnthropicTurns).toBe(true);
|
|
|
|
const responsesPolicy = provider.buildReplayPolicy?.({
|
|
provider: "xai",
|
|
modelApi: "openai-responses",
|
|
modelId: "grok-4-fast",
|
|
} as never);
|
|
expect(responsesPolicy?.sanitizeToolCallIds).toBe(true);
|
|
expect(responsesPolicy?.toolCallIdMode).toBe("strict");
|
|
expect(responsesPolicy?.applyAssistantFirstOrderingFix).toBe(false);
|
|
expect(responsesPolicy?.validateGeminiTurns).toBe(false);
|
|
expect(responsesPolicy?.validateAnthropicTurns).toBe(false);
|
|
});
|
|
|
|
it("wires provider stream shaping for fast mode and tool-stream defaults", async () => {
|
|
const provider = await registerSingleProviderPlugin(plugin);
|
|
const capture = createXaiPayloadCaptureStream();
|
|
|
|
const wrapped = provider.wrapStreamFn?.({
|
|
provider: "xai",
|
|
modelId: "grok-4",
|
|
extraParams: { fastMode: true },
|
|
streamFn: capture.streamFn,
|
|
} as never);
|
|
|
|
runXaiGrok4ResponseStream(wrapped);
|
|
expectXaiFastToolStreamShaping(capture);
|
|
});
|
|
|
|
it("defaults tool_stream extra params but preserves explicit values", async () => {
|
|
const provider = await registerSingleProviderPlugin(plugin);
|
|
|
|
expect(
|
|
provider.prepareExtraParams?.({
|
|
provider: "xai",
|
|
modelId: "grok-4",
|
|
extraParams: { fastMode: true },
|
|
} as never),
|
|
).toEqual({
|
|
fastMode: true,
|
|
tool_stream: true,
|
|
});
|
|
|
|
const explicit = { fastMode: true, tool_stream: false };
|
|
expect(
|
|
provider.prepareExtraParams?.({
|
|
provider: "xai",
|
|
modelId: "grok-4",
|
|
extraParams: explicit,
|
|
} as never),
|
|
).toBe(explicit);
|
|
});
|
|
|
|
it("owns forward-compatible Grok model resolution", async () => {
|
|
const provider = await registerSingleProviderPlugin(plugin);
|
|
|
|
const resolved = provider.resolveDynamicModel?.({
|
|
provider: "xai",
|
|
modelId: "grok-4.3",
|
|
modelRegistry: { find: () => null } as never,
|
|
providerConfig: {
|
|
api: "openai-completions",
|
|
baseUrl: "https://api.x.ai/v1",
|
|
},
|
|
} as never);
|
|
expect(resolved?.id).toBe("grok-4.3");
|
|
expect(resolved?.provider).toBe("xai");
|
|
expect(resolved?.api).toBe("openai-completions");
|
|
expect(resolved?.baseUrl).toBe("https://api.x.ai/v1");
|
|
expect(resolved?.reasoning).toBe(true);
|
|
expect(resolved?.input).toEqual(["text", "image"]);
|
|
expect(resolved?.contextWindow).toBe(1_000_000);
|
|
});
|
|
|
|
it("marks modern Grok refs without accepting multi-agent ids", async () => {
|
|
const provider = await registerSingleProviderPlugin(plugin);
|
|
|
|
expect(
|
|
provider.isModernModelRef?.({
|
|
provider: "xai",
|
|
modelId: "grok-4.3",
|
|
} as never),
|
|
).toBe(true);
|
|
expect(
|
|
provider.isModernModelRef?.({
|
|
provider: "xai",
|
|
modelId: "grok-4.20-multi-agent-experimental-beta-0304",
|
|
} as never),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("owns xai compat flags for direct and downstream routed models", async () => {
|
|
const provider = await registerSingleProviderPlugin(plugin);
|
|
|
|
const normalized = provider.normalizeResolvedModel?.({
|
|
provider: "xai",
|
|
modelId: "grok-4.3",
|
|
model: createProviderModel({ id: "grok-4.3" }),
|
|
} as never);
|
|
expect(normalized?.thinkingLevelMap).toEqual({
|
|
off: null,
|
|
minimal: "low",
|
|
low: "low",
|
|
medium: "medium",
|
|
high: "high",
|
|
xhigh: "high",
|
|
});
|
|
const olderReasoningModel = provider.normalizeResolvedModel?.({
|
|
provider: "xai",
|
|
modelId: "grok-4-1-fast",
|
|
model: createProviderModel({ id: "grok-4-1-fast" }),
|
|
} as never);
|
|
expect(olderReasoningModel?.thinkingLevelMap).toEqual({
|
|
off: null,
|
|
minimal: null,
|
|
low: null,
|
|
medium: null,
|
|
high: null,
|
|
xhigh: null,
|
|
});
|
|
const normalizedCompat = normalized?.compat as
|
|
| {
|
|
toolSchemaProfile?: string;
|
|
nativeWebSearchTool?: boolean;
|
|
toolCallArgumentsEncoding?: string;
|
|
}
|
|
| undefined;
|
|
expect(normalizedCompat?.toolSchemaProfile).toBe("xai");
|
|
expect(normalizedCompat?.nativeWebSearchTool).toBe(true);
|
|
expect(normalizedCompat?.toolCallArgumentsEncoding).toBe("html-entities");
|
|
});
|
|
});
|