mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 15:30:39 +00:00
fix(agents): sanitize strict openai-compatible turn ordering from #39252 (thanks @scoootscooob)
Co-authored-by: scoootscooob <zhentongfan@gmail.com>
This commit is contained in:
@@ -307,6 +307,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Signal/inbound attachment fan-in: forward all successfully fetched inbound attachments through `MediaPaths`/`MediaUrls`/`MediaTypes` (instead of only the first), and improve multi-attachment placeholder summaries in mention-gated pending history. (#39212) Thanks @joeykrug.
|
||||
- Nodes/system.run dispatch-wrapper boundary: keep shell-wrapper approval classification active at the depth boundary so `env` wrapper stacks cannot reach `/bin/sh -c` execution without the expected approval gate. Thanks @tdjackey for reporting.
|
||||
- Docker/token persistence on reconfigure: reuse the existing `.env` gateway token during `docker-setup.sh` reruns and align compose token env defaults, so Docker installs stop silently rotating tokens and breaking existing dashboard sessions. Landed from contributor PR #33097 by @chengzhichao-xydt. Thanks @chengzhichao-xydt.
|
||||
- Agents/strict OpenAI turn ordering: apply assistant-first transcript bootstrap sanitization to strict OpenAI-compatible providers (for example vLLM/Gemma via `openai-completions`) without adding Google-specific session markers, preventing assistant-first history rejections. (#39252) Thanks @scoootscooob.
|
||||
|
||||
## 2026.3.2
|
||||
|
||||
|
||||
@@ -255,6 +255,34 @@ describe("sanitizeSessionHistory", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("prepends a bootstrap user turn for strict OpenAI-compatible assistant-first history", async () => {
|
||||
setNonGoogleModelApi();
|
||||
const sessionEntries: Array<{ type: string; customType: string; data: unknown }> = [];
|
||||
const sessionManager = makeInMemorySessionManager(sessionEntries);
|
||||
const messages = castAgentMessages([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "hello from previous turn" }],
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await sanitizeSessionHistory({
|
||||
messages,
|
||||
modelApi: "openai-completions",
|
||||
provider: "vllm",
|
||||
modelId: "gemma-3-27b",
|
||||
sessionManager,
|
||||
sessionId: TEST_SESSION_ID,
|
||||
});
|
||||
|
||||
expect(result[0]?.role).toBe("user");
|
||||
expect((result[0] as { content?: unknown } | undefined)?.content).toBe("(session bootstrap)");
|
||||
expect(result[1]?.role).toBe("assistant");
|
||||
expect(
|
||||
sessionEntries.some((entry) => entry.customType === "google-turn-ordering-bootstrap"),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("annotates inter-session user messages before context sanitization", async () => {
|
||||
setNonGoogleModelApi();
|
||||
|
||||
|
||||
@@ -594,10 +594,19 @@ export async function sanitizeSessionHistory(params: {
|
||||
return sanitizedOpenAI;
|
||||
}
|
||||
|
||||
return applyGoogleTurnOrderingFix({
|
||||
messages: sanitizedOpenAI,
|
||||
modelApi: params.modelApi,
|
||||
sessionManager: params.sessionManager,
|
||||
sessionId: params.sessionId,
|
||||
}).messages;
|
||||
// Google models use the full wrapper with logging and session markers.
|
||||
if (isGoogleModelApi(params.modelApi)) {
|
||||
return applyGoogleTurnOrderingFix({
|
||||
messages: sanitizedOpenAI,
|
||||
modelApi: params.modelApi,
|
||||
sessionManager: params.sessionManager,
|
||||
sessionId: params.sessionId,
|
||||
}).messages;
|
||||
}
|
||||
|
||||
// Strict OpenAI-compatible providers (vLLM, Gemma, etc.) also reject
|
||||
// conversations that start with an assistant turn (e.g. delivery-mirror
|
||||
// messages after /new). Apply the same ordering fix without the
|
||||
// Google-specific session markers. See #38962.
|
||||
return sanitizeGoogleTurnOrdering(sanitizedOpenAI);
|
||||
}
|
||||
|
||||
@@ -60,6 +60,8 @@ describe("resolveTranscriptPolicy", () => {
|
||||
modelId: "kimi-k2.5",
|
||||
modelApi: "openai-completions",
|
||||
});
|
||||
expect(policy.applyGoogleTurnOrdering).toBe(true);
|
||||
expect(policy.validateGeminiTurns).toBe(true);
|
||||
expect(policy.validateAnthropicTurns).toBe(true);
|
||||
});
|
||||
|
||||
@@ -120,12 +122,25 @@ describe("resolveTranscriptPolicy", () => {
|
||||
expect(policy.preserveSignatures).toBe(false);
|
||||
});
|
||||
|
||||
it("enables turn-ordering and assistant-merge for strict OpenAI-compatible providers (#38962)", () => {
|
||||
const policy = resolveTranscriptPolicy({
|
||||
provider: "vllm",
|
||||
modelId: "gemma-3-27b",
|
||||
modelApi: "openai-completions",
|
||||
});
|
||||
expect(policy.applyGoogleTurnOrdering).toBe(true);
|
||||
expect(policy.validateGeminiTurns).toBe(true);
|
||||
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.applyGoogleTurnOrdering).toBe(false);
|
||||
expect(policy.validateGeminiTurns).toBe(false);
|
||||
expect(policy.validateAnthropicTurns).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -127,8 +127,8 @@ export function resolveTranscriptPolicy(params: {
|
||||
sanitizeThoughtSignatures: isOpenAi ? undefined : sanitizeThoughtSignatures,
|
||||
sanitizeThinkingSignatures: false,
|
||||
dropThinkingBlocks,
|
||||
applyGoogleTurnOrdering: !isOpenAi && isGoogle,
|
||||
validateGeminiTurns: !isOpenAi && isGoogle,
|
||||
applyGoogleTurnOrdering: !isOpenAi && (isGoogle || isStrictOpenAiCompatible),
|
||||
validateGeminiTurns: !isOpenAi && (isGoogle || isStrictOpenAiCompatible),
|
||||
validateAnthropicTurns: !isOpenAi && (isAnthropic || isStrictOpenAiCompatible),
|
||||
allowSyntheticToolResults: !isOpenAi && (isGoogle || isAnthropic),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user