From 9757d2bb646d1a1cd9ea9dd041054af6988372d7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 23 Feb 2026 06:08:38 +0100 Subject: [PATCH] fix(agents): normalize strict openai-compatible turn ordering Co-authored-by: liuwenyong1985 <48443240+liuwenyong1985@users.noreply.github.com> --- CHANGELOG.md | 1 + src/agents/transcript-policy.test.ts | 18 ++++++++++++++++++ src/agents/transcript-policy.ts | 7 ++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4e404310..4191591ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ Docs: https://docs.openclaw.ai - Agents/Moonshot: force `supportsDeveloperRole=false` for Moonshot-compatible `openai-completions` models (provider `moonshot` and Moonshot base URLs), so initial runs no longer send unsupported `developer` roles that trigger `ROLE_UNSPECIFIED` errors. (#21060, #22194) Thanks @ShengFuC. - Agents/Kimi: classify Moonshot `Your request exceeded model token limit` failures as context overflows so auto-compaction and user-facing overflow recovery trigger correctly instead of surfacing raw invalid-request errors. (#9562) Thanks @danilofalcao. - Providers/Moonshot: mark Kimi K2.5 as image-capable in implicit + onboarding model definitions, and refresh stale explicit provider capability fields (`input`/`reasoning`/context limits) from implicit catalogs so existing configs pick up Moonshot vision support without manual model rewrites. (#13135, #4459) Thanks @manikv12. +- Agents/Transcript: enable consecutive-user turn merging for strict non-OpenAI `openai-completions` providers (for example Moonshot/Kimi), reducing `roles must alternate` ordering failures on OpenAI-compatible endpoints while preserving current OpenRouter/Opencode behavior. (#7693) - Install/Discord Voice: make `@discordjs/opus` an optional dependency so `openclaw` install/update no longer hard-fails when native Opus builds fail, while keeping `opusscript` as the runtime fallback decoder for Discord voice flows. (#23737, #23733, #23703) Thanks @jeadland, @Sheetaa, and @Breakyman. - Docker/Setup: precreate `$OPENCLAW_CONFIG_DIR/identity` during `docker-setup.sh` so CLI commands that need device identity (for example `devices list`) avoid `EACCES ... /home/node/.openclaw/identity` failures on restrictive bind mounts. (#23948) Thanks @ackson-beep. - Exec/Background: stop applying the default exec timeout to background sessions (`background: true` or explicit `yieldMs`) when no explicit timeout is set, so long-running background jobs are no longer terminated at the default timeout boundary. (#23303) diff --git a/src/agents/transcript-policy.test.ts b/src/agents/transcript-policy.test.ts index 1da438561..4ef038c81 100644 --- a/src/agents/transcript-policy.test.ts +++ b/src/agents/transcript-policy.test.ts @@ -43,4 +43,22 @@ describe("resolveTranscriptPolicy", () => { expect(policy.sanitizeToolCallIds).toBe(false); expect(policy.toolCallIdMode).toBeUndefined(); }); + + it("enables user-turn merge for strict OpenAI-compatible providers", () => { + const policy = resolveTranscriptPolicy({ + provider: "moonshot", + modelId: "kimi-k2.5", + modelApi: "openai-completions", + }); + expect(policy.validateAnthropicTurns).toBe(true); + }); + + it("keeps OpenRouter on its existing turn-validation path", () => { + const policy = resolveTranscriptPolicy({ + provider: "openrouter", + modelId: "openai/gpt-4.1", + modelApi: "openai-completions", + }); + expect(policy.validateAnthropicTurns).toBe(false); + }); }); diff --git a/src/agents/transcript-policy.ts b/src/agents/transcript-policy.ts index a94d7eb2c..7f7e08d63 100644 --- a/src/agents/transcript-policy.ts +++ b/src/agents/transcript-policy.ts @@ -38,6 +38,7 @@ const OPENAI_MODEL_APIS = new Set([ "openai-codex-responses", ]); const OPENAI_PROVIDERS = new Set(["openai", "openai-codex"]); +const OPENAI_COMPAT_TURN_MERGE_EXCLUDED_PROVIDERS = new Set(["openrouter", "opencode"]); function isOpenAiApi(modelApi?: string | null): boolean { if (!modelApi) { @@ -84,6 +85,10 @@ export function resolveTranscriptPolicy(params: { const isGoogle = isGoogleModelApi(params.modelApi); const isAnthropic = isAnthropicApi(params.modelApi, provider); const isOpenAi = isOpenAiProvider(provider) || (!provider && isOpenAiApi(params.modelApi)); + const isStrictOpenAiCompatible = + params.modelApi === "openai-completions" && + !isOpenAi && + !OPENAI_COMPAT_TURN_MERGE_EXCLUDED_PROVIDERS.has(provider); const isMistral = isMistralModel({ provider, modelId }); const isOpenRouterGemini = (provider === "openrouter" || provider === "opencode") && @@ -118,7 +123,7 @@ export function resolveTranscriptPolicy(params: { dropThinkingBlocks, applyGoogleTurnOrdering: !isOpenAi && isGoogle, validateGeminiTurns: !isOpenAi && isGoogle, - validateAnthropicTurns: !isOpenAi && isAnthropic, + validateAnthropicTurns: !isOpenAi && (isAnthropic || isStrictOpenAiCompatible), allowSyntheticToolResults: !isOpenAi && (isGoogle || isAnthropic), }; }