From 9224afca3d2315604c43828a0e3412b2fcadf802 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 4 Apr 2026 04:08:06 +0900 Subject: [PATCH] refactor(providers): share xai and replay helpers --- extensions/minimax/index.ts | 29 ++++-------- extensions/moonshot/index.ts | 13 +----- extensions/ollama/index.ts | 26 +---------- extensions/xai/api.test.ts | 51 +++++++++++++++++++++ extensions/xai/api.ts | 18 +++----- extensions/zai/index.ts | 30 ++---------- src/plugin-sdk/provider-model-shared.ts | 9 ++++ src/plugins/provider-replay-helpers.test.ts | 27 +++++++++++ src/plugins/provider-replay-helpers.ts | 45 ++++++++++++++++++ 9 files changed, 156 insertions(+), 92 deletions(-) create mode 100644 extensions/xai/api.test.ts create mode 100644 src/plugins/provider-replay-helpers.test.ts create mode 100644 src/plugins/provider-replay-helpers.ts diff --git a/extensions/minimax/index.ts b/extensions/minimax/index.ts index a5f87aca7b2..435bc782c8e 100644 --- a/extensions/minimax/index.ts +++ b/extensions/minimax/index.ts @@ -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 { diff --git a/extensions/moonshot/index.ts b/extensions/moonshot/index.ts index 57059578367..f1c9baf4ae3 100644 --- a/extensions/moonshot/index.ts +++ b/extensions/moonshot/index.ts @@ -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({ diff --git a/extensions/ollama/index.ts b/extensions/ollama/index.ts index 77b3522215f..1863bfb60f4 100644 --- a/extensions/ollama/index.ts +++ b/extensions/ollama/index.ts @@ -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 { diff --git a/extensions/xai/api.test.ts b/extensions/xai/api.test.ts new file mode 100644 index 00000000000..b665030a0b5 --- /dev/null +++ b/extensions/xai/api.test.ts @@ -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); + }); +}); diff --git a/extensions/xai/api.ts b/extensions/xai/api.ts index 1004874fd3b..a830f7712db 100644 --- a/extensions/xai/api.ts +++ b/extensions/xai/api.ts @@ -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(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: { diff --git a/extensions/zai/index.ts b/extensions/zai/index.ts index aaa1049848b..1fd9bae14b9 100644 --- a/extensions/zai/index.ts +++ b/extensions/zai/index.ts @@ -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( diff --git a/src/plugin-sdk/provider-model-shared.ts b/src/plugin-sdk/provider-model-shared.ts index 7d2d055d7e5..19029851971 100644 --- a/src/plugin-sdk/provider-model-shared.ts +++ b/src/plugin-sdk/provider-model-shared.ts @@ -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, diff --git a/src/plugins/provider-replay-helpers.test.ts b/src/plugins/provider-replay-helpers.test.ts new file mode 100644 index 00000000000..45c25f38ffe --- /dev/null +++ b/src/plugins/provider-replay-helpers.test.ts @@ -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, + }); + }); +}); diff --git a/src/plugins/provider-replay-helpers.ts b/src/plugins/provider-replay-helpers.ts new file mode 100644 index 00000000000..536ceb792e4 --- /dev/null +++ b/src/plugins/provider-replay-helpers.ts @@ -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 } : {}), + }; +}