mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-04 21:02:54 +00:00
Avoid replaying prior OpenAI Responses reasoning/message/function-call item ids when the outgoing request disables store, while preserving encrypted reasoning and normalized summary arrays for stateless replay. Keep explicit store-enabled OpenAI wrapper paths opted into item-id replay, and cover shared/simple Responses, ChatGPT/Codex Responses, and GitHub Copilot sanitizer behavior. Regression tests cover store-disabled id omission, encrypted reasoning preservation, idless Copilot reasoning replay, and direct builder payloads. Local proof included focused Vitest, broad lint, broad test-types, bundled-extension lint, plugin boundary checks, autoreview clean, and live OpenAI Responses gpt-5.5 proof. Co-authored-by: hang <zhanghang02@gmail.com>
82 lines
2.6 KiB
TypeScript
82 lines
2.6 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
|
|
// Copilot's OpenAI-compatible `/responses` endpoint can emit replay item IDs
|
|
// that encode upstream connection state. Those IDs are rejected after the
|
|
// connection changes, so sanitize them at the provider boundary before send.
|
|
|
|
function looksLikeConnectionBoundId(id: string): boolean {
|
|
if (id.length < 24) {
|
|
return false;
|
|
}
|
|
if (/^(?:rs|msg|fc)_[A-Za-z0-9_-]+$/.test(id)) {
|
|
return false;
|
|
}
|
|
if (!/^[A-Za-z0-9+/_-]+=*$/.test(id)) {
|
|
return false;
|
|
}
|
|
return Buffer.from(id, "base64").length >= 16;
|
|
}
|
|
|
|
function deriveReplacementId(type: string | undefined, originalId: string): string {
|
|
const prefix = type === "function_call" ? "fc" : "msg";
|
|
const hex = createHash("sha256").update(originalId).digest("hex").slice(0, 16);
|
|
return `${prefix}_${hex}`;
|
|
}
|
|
|
|
type InputItem = Record<string, unknown> & { id?: unknown; type?: unknown };
|
|
|
|
function isInputItem(value: unknown): value is InputItem {
|
|
return Boolean(value) && typeof value === "object";
|
|
}
|
|
|
|
function isValidReasoningReplayId(id: unknown): id is string {
|
|
return typeof id === "string" && id.length > 0 && id.length <= 64;
|
|
}
|
|
|
|
export function sanitizeCopilotReplayResponseIds(input: unknown): boolean {
|
|
if (!Array.isArray(input)) {
|
|
return false;
|
|
}
|
|
let rewrote = false;
|
|
for (let index = input.length - 1; index >= 0; index -= 1) {
|
|
const item = input[index];
|
|
if (!isInputItem(item)) {
|
|
continue;
|
|
}
|
|
const id = item.id;
|
|
// Reasoning items with replay IDs reference server-side encrypted state
|
|
// bound to that ID. Drop unsafe IDs, but keep the store-disabled idless
|
|
// replay form produced by core Responses conversion.
|
|
if (item.type === "reasoning") {
|
|
if (id !== undefined && !isValidReasoningReplayId(id)) {
|
|
input.splice(index, 1);
|
|
rewrote = true;
|
|
}
|
|
continue;
|
|
}
|
|
if (typeof id !== "string" || id.length === 0) {
|
|
continue;
|
|
}
|
|
if (looksLikeConnectionBoundId(id)) {
|
|
item.id = deriveReplacementId(typeof item.type === "string" ? item.type : undefined, id);
|
|
rewrote = true;
|
|
}
|
|
}
|
|
return rewrote;
|
|
}
|
|
|
|
export function rewriteCopilotConnectionBoundResponseIds(input: unknown): boolean {
|
|
return sanitizeCopilotReplayResponseIds(input);
|
|
}
|
|
|
|
export function sanitizeCopilotReplayResponsePayloadIds(payload: unknown): boolean {
|
|
if (!payload || typeof payload !== "object") {
|
|
return false;
|
|
}
|
|
return sanitizeCopilotReplayResponseIds((payload as { input?: unknown }).input);
|
|
}
|
|
|
|
export function rewriteCopilotResponsePayloadConnectionBoundIds(payload: unknown): boolean {
|
|
return sanitizeCopilotReplayResponsePayloadIds(payload);
|
|
}
|