refactor(providers): share xai and replay helpers

This commit is contained in:
Vincent Koc
2026-04-04 04:08:06 +09:00
parent cc1881a838
commit 9224afca3d
9 changed files with 156 additions and 92 deletions

View File

@@ -13,6 +13,10 @@ import {
} from "openclaw/plugin-sdk/provider-auth";
import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-auth";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
import {
buildOpenAICompatibleReplayPolicy,
buildStrictAnthropicReplayPolicy,
} from "openclaw/plugin-sdk/provider-model-shared";
import { createMinimaxFastModeWrapper } from "openclaw/plugin-sdk/provider-stream";
import { fetchMinimaxUsage } from "openclaw/plugin-sdk/provider-usage";
import { isMiniMaxModernModelId, MINIMAX_DEFAULT_MODEL_ID } from "./api.js";
@@ -46,29 +50,12 @@ function buildMinimaxReplayPolicy(
): ProviderReplayPolicy | undefined {
if (ctx.modelApi === "anthropic-messages" || ctx.modelApi === "bedrock-converse-stream") {
const modelId = ctx.modelId?.toLowerCase() ?? "";
return {
sanitizeMode: "full",
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
preserveSignatures: true,
repairToolUseResultPairing: true,
validateAnthropicTurns: true,
allowSyntheticToolResults: true,
...(modelId.includes("claude") ? { dropThinkingBlocks: true } : {}),
};
return buildStrictAnthropicReplayPolicy({
dropThinkingBlocks: modelId.includes("claude"),
});
}
if (ctx.modelApi === "openai-completions") {
return {
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
applyAssistantFirstOrderingFix: true,
validateGeminiTurns: true,
validateAnthropicTurns: true,
};
}
return undefined;
return buildOpenAICompatibleReplayPolicy(ctx.modelApi);
}
function getDefaultBaseUrl(region: MiniMaxRegion): string {

View File

@@ -3,6 +3,7 @@ import type {
ProviderReplayPolicyContext,
} from "openclaw/plugin-sdk/plugin-entry";
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { buildOpenAICompatibleReplayPolicy } from "openclaw/plugin-sdk/provider-model-shared";
import {
createMoonshotThinkingWrapper,
resolveMoonshotThinkingType,
@@ -22,17 +23,7 @@ const PROVIDER_ID = "moonshot";
function buildMoonshotReplayPolicy(
ctx: ProviderReplayPolicyContext,
): ProviderReplayPolicy | undefined {
if (ctx.modelApi !== "openai-completions") {
return undefined;
}
return {
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
applyAssistantFirstOrderingFix: true,
validateGeminiTurns: true,
validateAnthropicTurns: true,
};
return buildOpenAICompatibleReplayPolicy(ctx.modelApi);
}
export default defineSingleProviderPluginEntry({

View File

@@ -8,6 +8,7 @@ import {
type ProviderReplayPolicy,
type ProviderReplayPolicyContext,
} from "openclaw/plugin-sdk/plugin-entry";
import { buildOpenAICompatibleReplayPolicy } from "openclaw/plugin-sdk/provider-model-shared";
import {
buildOllamaProvider,
configureOllamaNonInteractive,
@@ -31,30 +32,7 @@ const DEFAULT_API_KEY = "ollama-local";
function buildOllamaReplayPolicy(
ctx: ProviderReplayPolicyContext,
): ProviderReplayPolicy | undefined {
if (
ctx.modelApi !== "openai-completions" &&
ctx.modelApi !== "openai-responses" &&
ctx.modelApi !== "openai-codex-responses" &&
ctx.modelApi !== "azure-openai-responses"
) {
return undefined;
}
return {
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
...(ctx.modelApi === "openai-completions"
? {
applyAssistantFirstOrderingFix: true,
validateGeminiTurns: true,
validateAnthropicTurns: true,
}
: {
applyAssistantFirstOrderingFix: false,
validateGeminiTurns: false,
validateAnthropicTurns: false,
}),
};
return buildOpenAICompatibleReplayPolicy(ctx.modelApi);
}
function shouldSkipAmbientOllamaDiscovery(env: NodeJS.ProcessEnv): boolean {

View File

@@ -0,0 +1,51 @@
import { describe, expect, it } from "vitest";
import { isXaiModelHint, resolveXaiTransport, shouldContributeXaiCompat } from "./api.js";
describe("xai api helpers", () => {
it("uses shared endpoint classification for native xAI transports", () => {
expect(
resolveXaiTransport({
provider: "custom-xai",
api: "openai-completions",
baseUrl: "https://api.x.ai/v1",
}),
).toEqual({
api: "openai-responses",
baseUrl: "https://api.x.ai/v1",
});
});
it("keeps default-route xAI transport for the declared provider", () => {
expect(
resolveXaiTransport({
provider: "xai",
api: "openai-completions",
}),
).toEqual({
api: "openai-responses",
baseUrl: undefined,
});
});
it("contributes compat for native xAI hosts and model hints", () => {
expect(
shouldContributeXaiCompat({
modelId: "custom-model",
model: {
api: "openai-completions",
baseUrl: "https://api.x.ai/v1",
},
}),
).toBe(true);
expect(
shouldContributeXaiCompat({
modelId: "x-ai/grok-4",
model: {
api: "openai-completions",
baseUrl: "https://proxy.example.com/v1",
},
}),
).toBe(true);
expect(isXaiModelHint("x-ai/grok-4")).toBe(true);
});
});

View File

@@ -1,6 +1,7 @@
import {
applyModelCompatPatch,
normalizeProviderId,
resolveProviderEndpoint,
} from "openclaw/plugin-sdk/provider-model-shared";
import type { ModelCompatConfig } from "openclaw/plugin-sdk/provider-model-shared";
import { XAI_UNSUPPORTED_SCHEMA_KEYWORDS } from "openclaw/plugin-sdk/provider-tools";
@@ -39,15 +40,10 @@ export function applyXaiModelCompat<T extends { compat?: unknown }>(model: T): T
) as T;
}
function isXaiBaseUrl(baseUrl: unknown): boolean {
if (typeof baseUrl !== "string" || !baseUrl.trim()) {
return false;
}
try {
return new URL(baseUrl).hostname.toLowerCase() === "api.x.ai";
} catch {
return baseUrl.toLowerCase().includes("api.x.ai");
}
function isXaiNativeEndpoint(baseUrl: unknown): boolean {
return (
typeof baseUrl === "string" && resolveProviderEndpoint(baseUrl).endpointClass === "xai-native"
);
}
export function isXaiModelHint(modelId: string): boolean {
@@ -62,7 +58,7 @@ function shouldUseXaiResponsesTransport(params: {
if (params.api !== "openai-completions") {
return false;
}
if (isXaiBaseUrl(params.baseUrl)) {
if (isXaiNativeEndpoint(params.baseUrl)) {
return true;
}
return normalizeProviderId(params.provider) === "xai" && !params.baseUrl;
@@ -75,7 +71,7 @@ export function shouldContributeXaiCompat(params: {
if (params.model.api !== "openai-completions") {
return false;
}
return isXaiBaseUrl(params.model.baseUrl) || isXaiModelHint(params.modelId);
return isXaiNativeEndpoint(params.model.baseUrl) || isXaiModelHint(params.modelId);
}
export function resolveXaiTransport(params: {

View File

@@ -18,7 +18,10 @@ import {
upsertAuthProfile,
validateApiKeyInput,
} from "openclaw/plugin-sdk/provider-auth-api-key";
import { normalizeModelCompat } from "openclaw/plugin-sdk/provider-model-shared";
import {
buildOpenAICompatibleReplayPolicy,
normalizeModelCompat,
} from "openclaw/plugin-sdk/provider-model-shared";
import { createZaiToolStreamWrapper } from "openclaw/plugin-sdk/provider-stream";
import { fetchZaiUsage, resolveLegacyPiAgentAccessToken } from "openclaw/plugin-sdk/provider-usage";
import { detectZaiEndpoint, type ZaiEndpointId } from "./detect.js";
@@ -31,30 +34,7 @@ const GLM5_TEMPLATE_MODEL_ID = "glm-4.7";
const PROFILE_ID = "zai:default";
function buildZaiReplayPolicy(ctx: ProviderReplayPolicyContext): ProviderReplayPolicy | undefined {
if (
ctx.modelApi !== "openai-completions" &&
ctx.modelApi !== "openai-responses" &&
ctx.modelApi !== "openai-codex-responses" &&
ctx.modelApi !== "azure-openai-responses"
) {
return undefined;
}
return {
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
...(ctx.modelApi === "openai-completions"
? {
applyAssistantFirstOrderingFix: true,
validateGeminiTurns: true,
validateAnthropicTurns: true,
}
: {
applyAssistantFirstOrderingFix: false,
validateGeminiTurns: false,
validateAnthropicTurns: false,
}),
};
return buildOpenAICompatibleReplayPolicy(ctx.modelApi);
}
function resolveGlm5ForwardCompatModel(

View File

@@ -11,10 +11,15 @@ export type {
ModelCompatConfig,
ModelDefinitionConfig,
} from "../config/types.models.js";
export type {
ProviderEndpointClass,
ProviderEndpointResolution,
} from "../agents/provider-attribution.js";
export type { ProviderPlugin } from "../plugins/types.js";
export type { KilocodeModelCatalogEntry } from "../plugins/provider-model-kilocode.js";
export { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js";
export { resolveProviderEndpoint } from "../agents/provider-attribution.js";
export {
applyModelCompatPatch,
hasToolSchemaProfile,
@@ -24,6 +29,10 @@ export {
resolveToolCallArgumentsEncoding,
} from "../plugins/provider-model-compat.js";
export { normalizeProviderId } from "../agents/provider-id.js";
export {
buildOpenAICompatibleReplayPolicy,
buildStrictAnthropicReplayPolicy,
} from "../plugins/provider-replay-helpers.js";
export {
createMoonshotThinkingWrapper,
resolveMoonshotThinkingType,

View File

@@ -0,0 +1,27 @@
import { describe, expect, it } from "vitest";
import {
buildOpenAICompatibleReplayPolicy,
buildStrictAnthropicReplayPolicy,
} from "./provider-replay-helpers.js";
describe("provider replay helpers", () => {
it("builds strict openai-completions replay policy", () => {
expect(buildOpenAICompatibleReplayPolicy("openai-completions")).toMatchObject({
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
applyAssistantFirstOrderingFix: true,
validateGeminiTurns: true,
validateAnthropicTurns: true,
});
});
it("builds strict anthropic replay policy", () => {
expect(buildStrictAnthropicReplayPolicy({ dropThinkingBlocks: true })).toMatchObject({
sanitizeMode: "full",
preserveSignatures: true,
repairToolUseResultPairing: true,
allowSyntheticToolResults: true,
dropThinkingBlocks: true,
});
});
});

View File

@@ -0,0 +1,45 @@
import type { ProviderReplayPolicy } from "./types.js";
export function buildOpenAICompatibleReplayPolicy(
modelApi: string | null | undefined,
): ProviderReplayPolicy | undefined {
if (
modelApi !== "openai-completions" &&
modelApi !== "openai-responses" &&
modelApi !== "openai-codex-responses" &&
modelApi !== "azure-openai-responses"
) {
return undefined;
}
return {
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
...(modelApi === "openai-completions"
? {
applyAssistantFirstOrderingFix: true,
validateGeminiTurns: true,
validateAnthropicTurns: true,
}
: {
applyAssistantFirstOrderingFix: false,
validateGeminiTurns: false,
validateAnthropicTurns: false,
}),
};
}
export function buildStrictAnthropicReplayPolicy(
options: { dropThinkingBlocks?: boolean } = {},
): ProviderReplayPolicy {
return {
sanitizeMode: "full",
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
preserveSignatures: true,
repairToolUseResultPairing: true,
validateAnthropicTurns: true,
allowSyntheticToolResults: true,
...(options.dropThinkingBlocks ? { dropThinkingBlocks: true } : {}),
};
}