fix: send copilot headers during compaction

This commit is contained in:
Peter Steinberger
2026-04-24 23:54:51 +01:00
parent a35c166348
commit 9eeceaca43
4 changed files with 77 additions and 5 deletions

View File

@@ -99,7 +99,7 @@ Docs: https://docs.openclaw.ai
- Models/fallback: resolve bare fallback model provider ids before model switching, so configured fallback chains keep working when a fallback is named without an explicit provider prefix. Thanks @steipete.
- Voice-call/Telnyx: preserve inbound/outbound callback metadata and read transcription text from Telnyx's current `transcription_data` payload. Thanks @steipete.
- Providers/DeepSeek: wire V4 thinking controls and OpenAI-compatible replay policy so follow-up turns preserve DeepSeek `reasoning_content`, while the None/off thinking path strips replayed reasoning fields. Fixes #70931. Thanks @lsdsjy.
- Providers/GitHub Copilot: align Copilot request headers across Anthropic and Responses transports, including tool-result and image follow-up turns, without enabling unverified Responses continuation. Thanks @steipete.
- Providers/GitHub Copilot: align Copilot request headers across Anthropic, Responses, and built-in compaction summarization paths, including tool-result and image follow-up turns, without enabling unverified Responses continuation. Thanks @steipete.
- Codex harness: send verbose tool progress to chat channels for native app-server runs, matching the Pi harness `/verbose on` and `/verbose full` behavior. (#70966) Thanks @jalehman.
- Codex models: fetch paginated Codex app-server model catalogs, mark truncated `/codex models` output, and keep ChatGPT OAuth defaults on the `openai-codex/gpt-5.5` route instead of the OpenAI API-key route. Thanks @steipete.
- Codex status: report Codex CLI OAuth as `oauth (codex-cli)` for native `codex/*` sessions instead of showing unknown auth. Fixes #70688. Thanks @jb510.

View File

@@ -92,9 +92,9 @@ openclaw models auth login --provider github-copilot --method device --set-defau
<Accordion title="Request compatibility">
OpenClaw sends Copilot IDE-style request headers on Copilot transports,
including tool-result and image follow-up turns. It does not enable
provider-level Responses continuation for Copilot unless that behavior has
been verified against Copilot's API.
including built-in compaction, tool-result, and image follow-up turns. It
does not enable provider-level Responses continuation for Copilot unless
that behavior has been verified against Copilot's API.
</Accordion>
<Accordion title="Environment variable resolution order">

View File

@@ -1286,6 +1286,52 @@ describe("compaction-safeguard recent-turn preservation", () => {
expect(droppedCall?.customInstructions).toContain("Keep security caveats.");
});
it("adds Copilot IDE headers to built-in compaction summarization", async () => {
mockSummarizeInStages.mockReset();
mockSummarizeInStages.mockResolvedValue("mock summary");
const sessionManager = stubSessionManager();
const model = createAnthropicModelFixture({
id: "gpt-5.4",
name: "gpt-5.4",
provider: "github-copilot",
api: "openai-responses" as const,
baseUrl: "https://api.githubcopilot.com",
});
setCompactionSafeguardRuntime(sessionManager, { model, recentTurnsPreserve: 0 });
const getApiKeyAndHeadersMock = vi.fn().mockResolvedValue({
ok: true,
apiKey: "github-token",
headers: { "X-Test": "1" },
});
const mockContext = createCompactionContext({
sessionManager,
getApiKeyAndHeadersMock,
});
const compactionHandler = createCompactionHandler();
const event = createCompactionEvent({
messageText: "summarize me",
tokensBefore: 1000,
});
(event.preparation as { settings?: { reserveTokens: number } }).settings = {
reserveTokens: 4000,
};
const result = (await compactionHandler(event, mockContext)) as { cancel?: boolean };
expect(result.cancel).not.toBe(true);
const summaryCall = mockSummarizeInStages.mock.calls.at(-1)?.[0];
expect(summaryCall?.headers).toMatchObject({
"Copilot-Integration-Id": "vscode-chat",
"Editor-Plugin-Version": "copilot-chat/0.35.0",
"Openai-Organization": "github-copilot",
"User-Agent": "GitHubCopilotChat/0.26.7",
"X-Test": "1",
"x-initiator": "user",
});
});
it("does not retry summaries unless quality guard is explicitly enabled", async () => {
mockSummarizeInStages.mockReset();
mockSummarizeInStages.mockResolvedValue("summary missing headings");

View File

@@ -28,6 +28,7 @@ import {
summarizeInStages,
} from "../compaction.js";
import { collectTextContentBlocks } from "../content-blocks.js";
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "../copilot-dynamic-headers.js";
import { isTimeoutError } from "../failover-error.js";
import { repairToolUseResultPairing } from "../session-transcript-repair.js";
import { extractToolCallsFromAssistant, extractToolResultId } from "../tool-call-id.js";
@@ -236,6 +237,26 @@ async function resolveModelAuth(
return { ok: true, apiKey: requestAuth.apiKey, headers: requestAuth.headers };
}
function buildCompactionSummaryHeaders(params: {
model: NonNullable<ExtensionContext["model"]>;
messages: AgentMessage[];
headers?: Record<string, string>;
}): Record<string, string> | undefined {
if (params.model.provider !== "github-copilot") {
return params.headers;
}
const messages = params.messages as unknown as Parameters<
typeof buildCopilotDynamicHeaders
>[0]["messages"];
return {
...buildCopilotDynamicHeaders({
messages,
hasImages: hasCopilotVisionInput(messages),
}),
...params.headers,
};
}
function clampNonNegativeInt(value: unknown, fallback: number): number {
const normalized = typeof value === "number" && Number.isFinite(value) ? value : fallback;
return Math.max(0, Math.floor(normalized));
@@ -880,12 +901,17 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
return { cancel: true };
}
const apiKey = authResult.apiKey ?? "";
const headers = authResult.headers;
const authHeaders = authResult.headers;
try {
const modelContextWindow = resolveContextWindowTokens(model);
const contextWindowTokens = runtime?.contextWindowTokens ?? modelContextWindow;
let messagesToSummarize = preparation.messagesToSummarize;
const headers = buildCompactionSummaryHeaders({
model,
messages: messagesToSummarize,
headers: authHeaders,
});
const qualityGuardEnabled = runtime?.qualityGuardEnabled ?? false;
const qualityGuardMaxRetries = resolveQualityGuardMaxRetries(runtime?.qualityGuardMaxRetries);