fix(web): add prompt cache affinity to canvas agent (#74)
Co-authored-by: Tiantianr <260026737+Tiantianr@users.noreply.github.com>
This commit is contained in:
@@ -23,6 +23,7 @@ import { NODE_DEFAULT_SIZE } from "@/constant/canvas";
|
||||
import { CanvasNodeType, type CanvasAssistantMessage, type CanvasAssistantPendingBackendSession, type CanvasAssistantReference, type CanvasAssistantSession, type CanvasNodeData } from "@/types/canvas";
|
||||
import { useCanvasAgentStore } from "@/stores/canvas/use-canvas-agent-store";
|
||||
import { previewCanvasAgentOps, summarizeCanvasAgentOps, type CanvasAgentOp, type CanvasAgentOperationImpact, type CanvasAgentSnapshot } from "@/lib/canvas/canvas-agent-ops";
|
||||
import { canvasAgentPromptCacheKey } from "@/lib/openai-prompt-cache";
|
||||
|
||||
export const CANVAS_AGENT_PANEL_MOTION_MS = 500;
|
||||
const PANEL_MOTION_SECONDS = CANVAS_AGENT_PANEL_MOTION_MS / 1000;
|
||||
@@ -412,7 +413,7 @@ export function CanvasAssistantPanel({ nodes, selectedNodeIds, snapshot, project
|
||||
const result = await requestToolResponse({ ...requestConfig, systemPrompt: "" }, messages, ONLINE_AGENT_TOOLS, "required", (text) => {
|
||||
streamed = text;
|
||||
if (text.trim()) upsertMessage(sessionId, { id: assistantId, role: "assistant", text });
|
||||
});
|
||||
}, { promptCacheKey: canvasAgentPromptCacheKey(sessionId) });
|
||||
addOnlineLog("模型工具回复", result);
|
||||
if (result.toolCalls.length) {
|
||||
const writableCalls = result.toolCalls.filter(isWritableToolCall);
|
||||
@@ -468,7 +469,7 @@ export function CanvasAssistantPanel({ nodes, selectedNodeIds, snapshot, project
|
||||
const next = await requestToolResponse({ ...requestConfig, systemPrompt: "" }, nextMessages, ONLINE_AGENT_TOOLS, "auto", (text) => {
|
||||
streamed = text;
|
||||
if (text.trim()) upsertMessage(sessionId, { id: assistantId, role: "assistant", text });
|
||||
});
|
||||
}, { promptCacheKey: canvasAgentPromptCacheKey(sessionId) });
|
||||
addOnlineLog(`Agent Tool Loop ${step + 1} 回复`, next);
|
||||
if (next.toolCalls.length) {
|
||||
const writableCalls = next.toolCalls.filter(isWritableToolCall);
|
||||
|
||||
17
web/src/lib/openai-prompt-cache.ts
Normal file
17
web/src/lib/openai-prompt-cache.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
|
||||
|
||||
export function clampOpenAIPromptCacheKey(value?: string) {
|
||||
const key = value?.trim();
|
||||
if (!key) return undefined;
|
||||
return Array.from(key).slice(0, OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH).join("");
|
||||
}
|
||||
|
||||
export function canvasAgentPromptCacheKey(sessionId: string) {
|
||||
const value = sessionId.trim();
|
||||
return value ? clampOpenAIPromptCacheKey(`canvas-agent:${value}`) : undefined;
|
||||
}
|
||||
|
||||
export function withOpenAIPromptCacheKey<T extends Record<string, unknown>>(body: T, value?: string): T & { prompt_cache_key?: string } {
|
||||
const key = clampOpenAIPromptCacheKey(value);
|
||||
return key ? { ...body, prompt_cache_key: key } : body;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { buildImageReferencePromptText } from "@/lib/image-reference-prompt";
|
||||
import { channelRequest } from "@/services/api/custom-channel-relay";
|
||||
import { imageToDataUrl } from "@/services/image-storage";
|
||||
import type { ReferenceImage } from "@/types/image";
|
||||
import { withOpenAIPromptCacheKey } from "@/lib/openai-prompt-cache";
|
||||
|
||||
export type AiTextMessage = {
|
||||
role: "system" | "user" | "assistant";
|
||||
@@ -100,7 +101,7 @@ type GeminiPayload = {
|
||||
promptFeedback?: { blockReason?: string };
|
||||
};
|
||||
type GeminiStreamState = { buffer: string; text: string; toolCalls: ResponseToolCall[]; error?: string };
|
||||
type RequestOptions = { signal?: AbortSignal };
|
||||
type RequestOptions = { signal?: AbortSignal; promptCacheKey?: string };
|
||||
|
||||
const QUALITY_BASE: Record<string, number> = {
|
||||
low: 1024,
|
||||
@@ -945,13 +946,21 @@ export async function requestToolResponse(config: AiConfig, messages: ResponseIn
|
||||
parallel_tool_calls: false,
|
||||
}, onDelta, options);
|
||||
}
|
||||
return await requestStreamingResponse(requestConfig, {
|
||||
model: requestConfig.model,
|
||||
input: toResponseInput(withSystemMessage(requestConfig, messages)),
|
||||
tools: tools.map(toResponseTool),
|
||||
tool_choice: toolChoice,
|
||||
parallel_tool_calls: false,
|
||||
}, onDelta, options);
|
||||
return await requestStreamingResponse(
|
||||
requestConfig,
|
||||
withOpenAIPromptCacheKey(
|
||||
{
|
||||
model: requestConfig.model,
|
||||
input: toResponseInput(withSystemMessage(requestConfig, messages)),
|
||||
tools: tools.map(toResponseTool),
|
||||
tool_choice: toolChoice,
|
||||
parallel_tool_calls: false,
|
||||
},
|
||||
options?.promptCacheKey,
|
||||
),
|
||||
onDelta,
|
||||
options,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(readAxiosError(error, "请求失败"));
|
||||
}
|
||||
|
||||
27
web/test/agent-prompt-cache.test.ts
Normal file
27
web/test/agent-prompt-cache.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { canvasAgentPromptCacheKey, clampOpenAIPromptCacheKey, OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH, withOpenAIPromptCacheKey } from "../src/lib/openai-prompt-cache";
|
||||
|
||||
describe("Agent prompt cache", () => {
|
||||
test("同一会话生成稳定且隔离的缓存键", () => {
|
||||
expect(canvasAgentPromptCacheKey("session-a")).toBe("canvas-agent:session-a");
|
||||
expect(canvasAgentPromptCacheKey("session-a")).toBe(canvasAgentPromptCacheKey("session-a"));
|
||||
expect(canvasAgentPromptCacheKey("session-a")).not.toBe(canvasAgentPromptCacheKey("session-b"));
|
||||
expect(canvasAgentPromptCacheKey(" ")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("缓存键按 OpenAI 上限截断", () => {
|
||||
const key = clampOpenAIPromptCacheKey(`canvas-agent:${"x".repeat(100)}`);
|
||||
expect(Array.from(key || "")).toHaveLength(OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH);
|
||||
});
|
||||
|
||||
test("Responses 请求体携带缓存键", () => {
|
||||
const body = withOpenAIPromptCacheKey({ model: "test-model", input: [] }, " canvas-agent:session-a ");
|
||||
expect(body.prompt_cache_key).toBe("canvas-agent:session-a");
|
||||
});
|
||||
|
||||
test("空缓存键不会写入 Responses 请求体", () => {
|
||||
const body = withOpenAIPromptCacheKey({ model: "test-model", input: [] }, " ");
|
||||
expect(body).not.toHaveProperty("prompt_cache_key");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user