From 4c15f1310bd597242d95cae478d8ce09df0f189a Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 14 Apr 2026 16:33:55 +0100 Subject: [PATCH] fix(plugin-sdk): share canonical replay hook families --- extensions/arcee/index.ts | 5 +-- extensions/fireworks/index.ts | 6 +--- extensions/kilocode/index.ts | 5 +-- extensions/moonshot/index.ts | 5 +-- extensions/ollama/index.ts | 5 +-- extensions/opencode-go/index.ts | 6 +--- extensions/opencode/index.ts | 5 +-- extensions/openrouter/index.ts | 5 +-- extensions/xai/index.ts | 6 +--- extensions/zai/index.ts | 5 +-- src/plugin-sdk/provider-model-shared.test.ts | 36 +++++++++++++++++++- src/plugin-sdk/provider-model-shared.ts | 8 +++++ 12 files changed, 53 insertions(+), 44 deletions(-) diff --git a/extensions/arcee/index.ts b/extensions/arcee/index.ts index e9343358521..5f15490f0e9 100644 --- a/extensions/arcee/index.ts +++ b/extensions/arcee/index.ts @@ -4,7 +4,7 @@ import { readConfiguredProviderCatalogEntries, type ProviderCatalogContext, } from "openclaw/plugin-sdk/provider-catalog-shared"; -import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared"; +import { OPENAI_COMPATIBLE_REPLAY_HOOKS } from "openclaw/plugin-sdk/provider-model-shared"; import type { OpenClawConfig } from "openclaw/plugin-sdk/provider-onboard"; import { applyArceeConfig, @@ -20,9 +20,6 @@ import { } from "./provider-catalog.js"; const PROVIDER_ID = "arcee"; -const OPENAI_COMPATIBLE_REPLAY_HOOKS = buildProviderReplayFamilyHooks({ - family: "openai-compatible", -}); const ARCEE_WIZARD_GROUP = { groupId: "arcee", groupLabel: "Arcee AI", diff --git a/extensions/fireworks/index.ts b/extensions/fireworks/index.ts index 5aa59edbb3e..1cadc925a2b 100644 --- a/extensions/fireworks/index.ts +++ b/extensions/fireworks/index.ts @@ -1,10 +1,10 @@ import type { ProviderResolveDynamicModelContext } from "openclaw/plugin-sdk/plugin-entry"; import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; import { - buildProviderReplayFamilyHooks, cloneFirstTemplateModel, DEFAULT_CONTEXT_TOKENS, normalizeModelCompat, + OPENAI_COMPATIBLE_REPLAY_HOOKS, } from "openclaw/plugin-sdk/provider-model-shared"; import { isFireworksKimiModelId } from "./model-id.js"; import { applyFireworksConfig, FIREWORKS_DEFAULT_MODEL_REF } from "./onboard.js"; @@ -18,10 +18,6 @@ import { import { wrapFireworksProviderStream } from "./stream.js"; const PROVIDER_ID = "fireworks"; -const OPENAI_COMPATIBLE_REPLAY_HOOKS = buildProviderReplayFamilyHooks({ - family: "openai-compatible", -}); - function resolveFireworksDynamicModel(ctx: ProviderResolveDynamicModelContext) { const modelId = ctx.modelId.trim(); if (!modelId) { diff --git a/extensions/kilocode/index.ts b/extensions/kilocode/index.ts index 94c62a53525..074cdcb498a 100644 --- a/extensions/kilocode/index.ts +++ b/extensions/kilocode/index.ts @@ -1,14 +1,11 @@ import { readConfiguredProviderCatalogEntries } from "openclaw/plugin-sdk/provider-catalog-shared"; import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; -import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared"; +import { PASSTHROUGH_GEMINI_REPLAY_HOOKS } from "openclaw/plugin-sdk/provider-model-shared"; import { buildProviderStreamFamilyHooks } from "openclaw/plugin-sdk/provider-stream-family"; import { applyKilocodeConfig, KILOCODE_DEFAULT_MODEL_REF } from "./onboard.js"; import { buildKilocodeProviderWithDiscovery } from "./provider-catalog.js"; const PROVIDER_ID = "kilocode"; -const PASSTHROUGH_GEMINI_REPLAY_HOOKS = buildProviderReplayFamilyHooks({ - family: "passthrough-gemini", -}); const KILOCODE_THINKING_STREAM_HOOKS = buildProviderStreamFamilyHooks("kilocode-thinking"); export default defineSingleProviderPluginEntry({ diff --git a/extensions/moonshot/index.ts b/extensions/moonshot/index.ts index 91ca51b249c..1734ac63872 100644 --- a/extensions/moonshot/index.ts +++ b/extensions/moonshot/index.ts @@ -1,5 +1,5 @@ import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; -import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared"; +import { OPENAI_COMPATIBLE_REPLAY_HOOKS } from "openclaw/plugin-sdk/provider-model-shared"; import { buildProviderStreamFamilyHooks } from "openclaw/plugin-sdk/provider-stream-family"; import { applyMoonshotNativeStreamingUsageCompat } from "./api.js"; import { moonshotMediaUnderstandingProvider } from "./media-understanding-provider.js"; @@ -12,9 +12,6 @@ import { buildMoonshotProvider } from "./provider-catalog.js"; import { createKimiWebSearchProvider } from "./src/kimi-web-search-provider.js"; const PROVIDER_ID = "moonshot"; -const OPENAI_COMPATIBLE_REPLAY_HOOKS = buildProviderReplayFamilyHooks({ - family: "openai-compatible", -}); const MOONSHOT_THINKING_STREAM_HOOKS = buildProviderStreamFamilyHooks("moonshot-thinking"); export default defineSingleProviderPluginEntry({ diff --git a/extensions/ollama/index.ts b/extensions/ollama/index.ts index 673f0aff609..775d2532abe 100644 --- a/extensions/ollama/index.ts +++ b/extensions/ollama/index.ts @@ -7,7 +7,7 @@ import { type ProviderDiscoveryContext, } from "openclaw/plugin-sdk/plugin-entry"; import { - buildProviderReplayFamilyHooks, + OPENAI_COMPATIBLE_REPLAY_HOOKS, type ModelProviderConfig, } from "openclaw/plugin-sdk/provider-model-shared"; import { normalizeOptionalString, readStringValue } from "openclaw/plugin-sdk/text-runtime"; @@ -33,9 +33,6 @@ import { createOllamaWebSearchProvider } from "./src/web-search-provider.js"; const PROVIDER_ID = "ollama"; const DEFAULT_API_KEY = "ollama-local"; -const OPENAI_COMPATIBLE_REPLAY_HOOKS = buildProviderReplayFamilyHooks({ - family: "openai-compatible", -}); type OllamaPluginConfig = { discovery?: { diff --git a/extensions/opencode-go/index.ts b/extensions/opencode-go/index.ts index e36c8b9b808..d8eba215e7d 100644 --- a/extensions/opencode-go/index.ts +++ b/extensions/opencode-go/index.ts @@ -1,13 +1,9 @@ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key"; -import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared"; +import { PASSTHROUGH_GEMINI_REPLAY_HOOKS } from "openclaw/plugin-sdk/provider-model-shared"; import { applyOpencodeGoConfig, OPENCODE_GO_DEFAULT_MODEL_REF } from "./api.js"; const PROVIDER_ID = "opencode-go"; -const PASSTHROUGH_GEMINI_REPLAY_HOOKS = buildProviderReplayFamilyHooks({ - family: "passthrough-gemini", -}); - export default definePluginEntry({ id: PROVIDER_ID, name: "OpenCode Go Provider", diff --git a/extensions/opencode/index.ts b/extensions/opencode/index.ts index 3ec2da4d529..fcefd59b891 100644 --- a/extensions/opencode/index.ts +++ b/extensions/opencode/index.ts @@ -1,17 +1,14 @@ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key"; import { - buildProviderReplayFamilyHooks, matchesExactOrPrefix, + PASSTHROUGH_GEMINI_REPLAY_HOOKS, } from "openclaw/plugin-sdk/provider-model-shared"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { applyOpencodeZenConfig, OPENCODE_ZEN_DEFAULT_MODEL } from "./api.js"; const PROVIDER_ID = "opencode"; const MINIMAX_MODERN_MODEL_MATCHERS = ["minimax-m2.7"] as const; -const PASSTHROUGH_GEMINI_REPLAY_HOOKS = buildProviderReplayFamilyHooks({ - family: "passthrough-gemini", -}); function isModernOpencodeModel(modelId: string): boolean { const lower = normalizeLowercaseStringOrEmpty(modelId); diff --git a/extensions/openrouter/index.ts b/extensions/openrouter/index.ts index 3449d69de7a..0f6dc7baf4b 100644 --- a/extensions/openrouter/index.ts +++ b/extensions/openrouter/index.ts @@ -5,8 +5,8 @@ import { } from "openclaw/plugin-sdk/plugin-entry"; import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key"; import { - buildProviderReplayFamilyHooks, DEFAULT_CONTEXT_TOKENS, + PASSTHROUGH_GEMINI_REPLAY_HOOKS, } from "openclaw/plugin-sdk/provider-model-shared"; import { getOpenRouterModelCapabilities, @@ -26,9 +26,6 @@ const OPENROUTER_CACHE_TTL_MODEL_PREFIXES = [ "moonshotai/", "zai/", ] as const; -const PASSTHROUGH_GEMINI_REPLAY_HOOKS = buildProviderReplayFamilyHooks({ - family: "passthrough-gemini", -}); export default definePluginEntry({ id: "openrouter", diff --git a/extensions/xai/index.ts b/extensions/xai/index.ts index 9d1d37f6a12..2f4e9423374 100644 --- a/extensions/xai/index.ts +++ b/extensions/xai/index.ts @@ -1,6 +1,6 @@ import { Type } from "@sinclair/typebox"; import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; -import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared"; +import { OPENAI_COMPATIBLE_REPLAY_HOOKS } from "openclaw/plugin-sdk/provider-model-shared"; import { defaultToolStreamExtraParams } from "openclaw/plugin-sdk/provider-stream-shared"; import { jsonResult, readProviderEnvValue } from "openclaw/plugin-sdk/provider-web-search"; import { @@ -24,10 +24,6 @@ import { } from "./x-search-tool-shared.js"; const PROVIDER_ID = "xai"; -const OPENAI_COMPATIBLE_REPLAY_HOOKS = buildProviderReplayFamilyHooks({ - family: "openai-compatible", -}); - function hasResolvableXaiApiKey(config: unknown): boolean { return Boolean( resolveFallbackXaiAuth(config as never)?.apiKey || readProviderEnvValue(["XAI_API_KEY"]), diff --git a/extensions/zai/index.ts b/extensions/zai/index.ts index d3153c9c2e3..faf0c5fe4c0 100644 --- a/extensions/zai/index.ts +++ b/extensions/zai/index.ts @@ -17,8 +17,8 @@ import { validateApiKeyInput, } from "openclaw/plugin-sdk/provider-auth-api-key"; import { - buildProviderReplayFamilyHooks, normalizeModelCompat, + OPENAI_COMPATIBLE_REPLAY_HOOKS, } from "openclaw/plugin-sdk/provider-model-shared"; import { buildProviderStreamFamilyHooks } from "openclaw/plugin-sdk/provider-stream-family"; import { defaultToolStreamExtraParams } from "openclaw/plugin-sdk/provider-stream-shared"; @@ -32,9 +32,6 @@ import { applyZaiConfig, applyZaiProviderConfig, ZAI_DEFAULT_MODEL_REF } from ". const PROVIDER_ID = "zai"; const GLM5_TEMPLATE_MODEL_ID = "glm-4.7"; const PROFILE_ID = "zai:default"; -const OPENAI_COMPATIBLE_REPLAY_HOOKS = buildProviderReplayFamilyHooks({ - family: "openai-compatible", -}); const ZAI_TOOL_STREAM_HOOKS = buildProviderStreamFamilyHooks("tool-stream-default-on"); function resolveGlm5ForwardCompatModel( diff --git a/src/plugin-sdk/provider-model-shared.test.ts b/src/plugin-sdk/provider-model-shared.test.ts index 8303f152bad..5682bdebb24 100644 --- a/src/plugin-sdk/provider-model-shared.test.ts +++ b/src/plugin-sdk/provider-model-shared.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from "vitest"; -import { buildProviderReplayFamilyHooks } from "./provider-model-shared.js"; +import { + buildProviderReplayFamilyHooks, + OPENAI_COMPATIBLE_REPLAY_HOOKS, + PASSTHROUGH_GEMINI_REPLAY_HOOKS, +} from "./provider-model-shared.js"; describe("buildProviderReplayFamilyHooks", () => { it("covers the replay family matrix", async () => { @@ -171,4 +175,34 @@ describe("buildProviderReplayFamilyHooks", () => { } as never), ).not.toHaveProperty("dropThinkingBlocks"); }); + + it("exposes canonical replay hooks for reused provider families", () => { + expect( + OPENAI_COMPATIBLE_REPLAY_HOOKS.buildReplayPolicy?.({ + provider: "xai", + modelApi: "openai-completions", + modelId: "grok-4", + } as never), + ).toMatchObject({ + sanitizeToolCallIds: true, + applyAssistantFirstOrderingFix: true, + validateGeminiTurns: true, + }); + + expect( + PASSTHROUGH_GEMINI_REPLAY_HOOKS.buildReplayPolicy?.({ + provider: "openrouter", + modelApi: "openai-completions", + modelId: "gemini-2.5-pro", + } as never), + ).toMatchObject({ + applyAssistantFirstOrderingFix: false, + validateGeminiTurns: false, + validateAnthropicTurns: false, + sanitizeThoughtSignatures: { + allowBase64Only: true, + includeCamelCase: true, + }, + }); + }); }); diff --git a/src/plugin-sdk/provider-model-shared.ts b/src/plugin-sdk/provider-model-shared.ts index 40d1cdd18ff..46e5d612196 100644 --- a/src/plugin-sdk/provider-model-shared.ts +++ b/src/plugin-sdk/provider-model-shared.ts @@ -160,3 +160,11 @@ export function buildProviderReplayFamilyHooks( } throw new Error("Unsupported provider replay family"); } + +export const OPENAI_COMPATIBLE_REPLAY_HOOKS = buildProviderReplayFamilyHooks({ + family: "openai-compatible", +}); + +export const PASSTHROUGH_GEMINI_REPLAY_HOOKS = buildProviderReplayFamilyHooks({ + family: "passthrough-gemini", +});