fix(agents): normalize strict openai-compatible turn ordering

Co-authored-by: liuwenyong1985 <48443240+liuwenyong1985@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-02-23 06:08:38 +01:00
parent 15e32c7341
commit 9757d2bb64
3 changed files with 25 additions and 1 deletions

View File

@@ -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)

View File

@@ -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);
});
});

View File

@@ -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),
};
}