mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
8ed79aab02
* fix(github-copilot): preserve active encrypted reasoning Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(github-copilot): normalize null reasoning status Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test(github-copilot): cover adjacent reasoning cleanup Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
73 lines
2.7 KiB
TypeScript
73 lines
2.7 KiB
TypeScript
// Github Copilot plugin module implements replay policy behavior.
|
|
import type {
|
|
ProviderReplayPolicy,
|
|
ProviderReplayPolicyContext,
|
|
ProviderSanitizeReplayHistoryContext,
|
|
} from "openclaw/plugin-sdk/plugin-entry";
|
|
import { buildStrictAnthropicReplayPolicy } from "openclaw/plugin-sdk/provider-model-shared";
|
|
|
|
const OMITTED_COPILOT_REASONING_TEXT = "[assistant reasoning omitted]";
|
|
|
|
// Copilot routes Claude over the real Anthropic Messages transport, so transport
|
|
// identity - not the model id - owns replay behavior. The wire patch in
|
|
// stream.ts gates on the same signal; keep the two in sync.
|
|
function isCopilotAnthropicTransport(modelApi?: string | null): boolean {
|
|
return modelApi === "anthropic-messages";
|
|
}
|
|
|
|
function isThinkingBlock(value: unknown): boolean {
|
|
if (!value || typeof value !== "object") {
|
|
return false;
|
|
}
|
|
const type = (value as { type?: unknown }).type;
|
|
return type === "thinking" || type === "redacted_thinking";
|
|
}
|
|
|
|
export function stripCopilotAssistantThinkingMessages<T>(messages: T[]): T[] {
|
|
let touched = false;
|
|
const sanitized = messages.map((message) => {
|
|
if (!message || typeof message !== "object") {
|
|
return message;
|
|
}
|
|
const record = message as { role?: unknown; content?: unknown };
|
|
if (record.role !== "assistant" || !Array.isArray(record.content)) {
|
|
return message;
|
|
}
|
|
const content = record.content.filter((block) => !isThinkingBlock(block));
|
|
if (content.length === record.content.length) {
|
|
return message;
|
|
}
|
|
touched = true;
|
|
return {
|
|
...message,
|
|
content:
|
|
content.length > 0 ? content : [{ type: "text", text: OMITTED_COPILOT_REASONING_TEXT }],
|
|
};
|
|
});
|
|
return touched ? sanitized : messages;
|
|
}
|
|
|
|
export function buildGithubCopilotReplayPolicy(
|
|
ctx: ProviderReplayPolicyContext,
|
|
): ProviderReplayPolicy | undefined {
|
|
if (!isCopilotAnthropicTransport(ctx.modelApi)) {
|
|
return undefined;
|
|
}
|
|
return buildStrictAnthropicReplayPolicy({
|
|
// Unconditional: Copilot strips replayed thinking for every Claude model, so
|
|
// it never owns signed-thinking replay. The shared by-model helper would
|
|
// re-enable it for thinking-preserving Claude ids.
|
|
dropThinkingBlocks: true,
|
|
// wrapCopilotAnthropicStream rewrites tool ids on the wire and deliberately
|
|
// leaves the persisted transcript untouched. Core-side rewriting would
|
|
// mutate that transcript instead.
|
|
sanitizeToolCallIds: false,
|
|
});
|
|
}
|
|
|
|
export function sanitizeGithubCopilotReplayHistory(ctx: ProviderSanitizeReplayHistoryContext) {
|
|
return ctx.modelApi === "openai-responses" || isCopilotAnthropicTransport(ctx.modelApi)
|
|
? stripCopilotAssistantThinkingMessages(ctx.messages)
|
|
: ctx.messages;
|
|
}
|