mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(providers): strip cache-boundary marker from non-Anthropic prompts (#89716)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// Google shared provider tests cover response conversion and finish reasons.
|
||||
import { FinishReason, type GenerateContentResponse } from "@google/genai";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../agents/system-prompt-cache-boundary.js";
|
||||
import type { AssistantMessage, Model } from "../types.js";
|
||||
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
||||
import {
|
||||
@@ -146,4 +147,14 @@ describe("buildGoogleGenerateContentParams", () => {
|
||||
|
||||
expect(params.config?.stopSequences).toEqual(["STOP"]);
|
||||
});
|
||||
|
||||
it("strips the internal cache boundary marker from systemInstruction", () => {
|
||||
const params = buildGoogleGenerateContentParams(model, {
|
||||
systemPrompt: `Stable${SYSTEM_PROMPT_CACHE_BOUNDARY}Dynamic`,
|
||||
messages: [{ role: "user", content: "hello", timestamp: 0 }],
|
||||
});
|
||||
|
||||
expect(params.config?.systemInstruction).toBe("Stable\nDynamic");
|
||||
expect(JSON.stringify(params)).not.toContain("OPENCLAW_CACHE_BOUNDARY");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
type Part,
|
||||
type ThinkingConfig,
|
||||
} from "@google/genai";
|
||||
import { stripSystemPromptCacheBoundary } from "../../agents/system-prompt-cache-boundary.js";
|
||||
import { calculateCost, clampThinkingLevel } from "../model-utils.js";
|
||||
import type {
|
||||
Api,
|
||||
@@ -500,7 +501,9 @@ export function buildGoogleGenerateContentParams<T extends GoogleApiType>(
|
||||
|
||||
const config: GenerateContentConfig = {
|
||||
...(Object.keys(generationConfig).length > 0 && generationConfig),
|
||||
...(context.systemPrompt && { systemInstruction: sanitizeSurrogates(context.systemPrompt) }),
|
||||
...(context.systemPrompt && {
|
||||
systemInstruction: sanitizeSurrogates(stripSystemPromptCacheBoundary(context.systemPrompt)),
|
||||
}),
|
||||
...(context.tools && context.tools.length > 0 && { tools: convertTools(context.tools) }),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Mistral provider tests cover request mapping and stream conversion.
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../agents/system-prompt-cache-boundary.js";
|
||||
import type { Context, Model } from "../types.js";
|
||||
|
||||
const mistralMockState = vi.hoisted(() => ({
|
||||
@@ -214,4 +215,24 @@ describe("Mistral provider", () => {
|
||||
function: { name: "healthy_tool" },
|
||||
});
|
||||
});
|
||||
|
||||
it("strips the internal cache boundary marker from the system message", async () => {
|
||||
const stream = streamSimpleMistral(
|
||||
makeMistralModel(),
|
||||
{
|
||||
systemPrompt: `Stable${SYSTEM_PROMPT_CACHE_BOUNDARY}Dynamic`,
|
||||
messages: [{ role: "user", content: "hello", timestamp: 0 }],
|
||||
},
|
||||
{ apiKey: "sk-mistral-provider" },
|
||||
);
|
||||
|
||||
await stream.result();
|
||||
|
||||
const payload = mistralMockState.payloads[0] as {
|
||||
messages: Array<{ role: string; content: string }>;
|
||||
};
|
||||
const systemMessage = payload.messages.find((message) => message.role === "system");
|
||||
expect(systemMessage?.content).toBe("Stable\nDynamic");
|
||||
expect(JSON.stringify(payload)).not.toContain("OPENCLAW_CACHE_BOUNDARY");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
ContentChunk,
|
||||
FunctionTool,
|
||||
} from "@mistralai/mistralai/models/components";
|
||||
import { stripSystemPromptCacheBoundary } from "../../agents/system-prompt-cache-boundary.js";
|
||||
import { getEnvApiKey } from "../env-api-keys.js";
|
||||
import { calculateCost, clampThinkingLevel } from "../model-utils.js";
|
||||
import type {
|
||||
@@ -309,7 +310,7 @@ function buildChatPayload(
|
||||
if (context.systemPrompt) {
|
||||
payload.messages.unshift({
|
||||
role: "system",
|
||||
content: sanitizeSurrogates(context.systemPrompt),
|
||||
content: sanitizeSurrogates(stripSystemPromptCacheBoundary(context.systemPrompt)),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// ChatGPT Responses provider tests cover stream handling and timeout behavior.
|
||||
import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../agents/system-prompt-cache-boundary.js";
|
||||
import type { Context, Model } from "../types.js";
|
||||
import {
|
||||
extractOpenAICodexAccountId,
|
||||
@@ -403,6 +404,60 @@ describe("streamOpenAICodexResponses transport", () => {
|
||||
expect(result.errorMessage).toContain("Request timed out after 5ms");
|
||||
});
|
||||
|
||||
it("strips the internal cache boundary marker from request instructions", async () => {
|
||||
let capturedPayload: { instructions?: string } | undefined;
|
||||
const stream = streamOpenAICodexResponses(
|
||||
model,
|
||||
{
|
||||
systemPrompt: `Stable${SYSTEM_PROMPT_CACHE_BOUNDARY}Dynamic`,
|
||||
messages: [{ role: "user", content: "hi", timestamp: 1 }],
|
||||
},
|
||||
{
|
||||
apiKey: createJwt({
|
||||
"https://api.openai.com/auth": {
|
||||
chatgpt_account_id: "acct-1",
|
||||
},
|
||||
}),
|
||||
transport: "sse",
|
||||
onPayload: (payload) => {
|
||||
capturedPayload = payload as typeof capturedPayload;
|
||||
throw new Error("stop after payload");
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const result = await stream.result();
|
||||
|
||||
expect(result.stopReason).toBe("error");
|
||||
expect(capturedPayload?.instructions).toBe("Stable\nDynamic");
|
||||
expect(JSON.stringify(capturedPayload)).not.toContain("OPENCLAW_CACHE_BOUNDARY");
|
||||
});
|
||||
|
||||
it("falls back to the default instructions when no system prompt is set", async () => {
|
||||
let capturedPayload: { instructions?: string } | undefined;
|
||||
const stream = streamOpenAICodexResponses(
|
||||
model,
|
||||
{ messages: [{ role: "user", content: "hi", timestamp: 1 }] },
|
||||
{
|
||||
apiKey: createJwt({
|
||||
"https://api.openai.com/auth": {
|
||||
chatgpt_account_id: "acct-1",
|
||||
},
|
||||
}),
|
||||
transport: "sse",
|
||||
onPayload: (payload) => {
|
||||
capturedPayload = payload as typeof capturedPayload;
|
||||
throw new Error("stop after payload");
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const result = await stream.result();
|
||||
|
||||
expect(result.stopReason).toBe("error");
|
||||
expect(capturedPayload?.instructions).toBe("You are a helpful assistant.");
|
||||
});
|
||||
|
||||
it("prefers promptCacheKey over sessionId for request cache affinity", async () => {
|
||||
let payload: unknown;
|
||||
vi.stubGlobal(
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
resolveTimerTimeoutMs,
|
||||
clampTimerTimeoutMs,
|
||||
} from "@openclaw/normalization-core/number-coercion";
|
||||
import { stripSystemPromptCacheBoundary } from "../../agents/system-prompt-cache-boundary.js";
|
||||
import { getEnvApiKey } from "../env-api-keys.js";
|
||||
import { clampThinkingLevel } from "../model-utils.js";
|
||||
import { registerSessionResourceCleanup } from "../session-resources.js";
|
||||
@@ -488,7 +489,8 @@ function buildRequestBody(
|
||||
model: model.id,
|
||||
store: false,
|
||||
stream: true,
|
||||
instructions: context.systemPrompt || "You are a helpful assistant.",
|
||||
instructions:
|
||||
stripSystemPromptCacheBoundary(context.systemPrompt ?? "") || "You are a helpful assistant.",
|
||||
input: messages,
|
||||
text: { verbosity: options?.textVerbosity || "low" },
|
||||
include: ["reasoning.encrypted_content"],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// OpenAI Responses shared tests cover tool conversion and response item mapping.
|
||||
import type { Tool as OpenAIResponsesTool } from "openai/resources/responses/responses.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../agents/system-prompt-cache-boundary.js";
|
||||
import type { AssistantMessage, AssistantMessageEvent, Context, Model, Tool } from "../types.js";
|
||||
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
||||
import {
|
||||
@@ -262,6 +263,24 @@ describe("convertResponsesMessages", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("strips the internal cache boundary marker from the system prompt message", () => {
|
||||
const input = convertResponsesMessages(
|
||||
nativeOpenAIModel,
|
||||
{
|
||||
systemPrompt: `Stable${SYSTEM_PROMPT_CACHE_BOUNDARY}Dynamic`,
|
||||
messages: [],
|
||||
} satisfies Context,
|
||||
allowedToolCallProviders,
|
||||
);
|
||||
|
||||
expect(input[0]).toMatchObject({
|
||||
type: "message",
|
||||
role: "developer",
|
||||
content: [{ type: "input_text", text: "Stable\nDynamic" }],
|
||||
});
|
||||
expect(JSON.stringify(input)).not.toContain("OPENCLAW_CACHE_BOUNDARY");
|
||||
});
|
||||
|
||||
it("omits phase-tagged assistant replay ids without reasoning", () => {
|
||||
const input = convertResponsesMessages(
|
||||
nativeOpenAIModel,
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
ResponseReasoningItem,
|
||||
ResponseStreamEvent,
|
||||
} from "openai/resources/responses/responses.js";
|
||||
import { stripSystemPromptCacheBoundary } from "../../agents/system-prompt-cache-boundary.js";
|
||||
import {
|
||||
AZURE_RESPONSES_TEXT_CONTENT_PART_TYPE,
|
||||
OPENAI_RESPONSES_OUTPUT_TEXT_CONTENT_PART_TYPE,
|
||||
@@ -254,7 +255,12 @@ export function convertResponsesMessages<TApi extends Api>(
|
||||
messages.push({
|
||||
type: "message",
|
||||
role,
|
||||
content: [{ type: "input_text", text: sanitizeSurrogates(context.systemPrompt) }],
|
||||
content: [
|
||||
{
|
||||
type: "input_text",
|
||||
text: sanitizeSurrogates(stripSystemPromptCacheBoundary(context.systemPrompt)),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user