mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
b4de5fdd3a
Copilot routes Claude over api.*.githubcopilot.com/v1/messages, a real
Anthropic Messages endpoint, but its replay policy returned only
`{ dropThinkingBlocks: true }`. resolveTranscriptPolicy skips the core
transport-family fallback entirely once a provider registers
buildReplayPolicy, so validateAnthropicTurns stayed false and core never
stripped a trailing assistant prefill turn. The next request after
auto-compaction was rejected with 400 "This model does not support
assistant message prefill".
Dispatch on ctx.modelApi and reuse buildStrictAnthropicReplayPolicy for
the Anthropic transport. dropThinkingBlocks stays unconditional so the
#81520 fix holds, and tool-call ids stay owned by wrapCopilotAnthropicStream
so the persisted transcript is not rewritten.
73 lines
2.6 KiB
TypeScript
73 lines
2.6 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 isCopilotAnthropicTransport(ctx.modelApi)
|
|
? stripCopilotAssistantThinkingMessages(ctx.messages)
|
|
: ctx.messages;
|
|
}
|