mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(agents): normalize Copilot replay tool IDs
Normalize GitHub Copilot Responses replay tool-call IDs before dispatch so resumed sessions with historical overlong item IDs no longer fail Copilot schema validation. Closes #82749.
This commit is contained in:
@@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- Providers/Anthropic-messages: extract `reasoning_content` from `thinking` blocks during assistant replay so proxy providers that route through the Anthropic-messages transport preserve reasoning context across tool-call follow-up turns. Thanks @Sunnyone2three.
|
||||
- Agents/GitHub Copilot: normalize replayed Responses tool-call IDs before dispatch so resumed sessions with historical overlong tool IDs continue instead of failing Copilot schema validation. (#82750) Thanks @galiniliev.
|
||||
- Mac app: let menu gateway/session error text wrap across a few lines and stop rebuilding dynamic Context/Gateway menu rows while the menu is open, reducing flicker.
|
||||
- Mac app: make device pairing approval sheets friendlier, with concise Mac/device copy, shortened identifiers, friendly scope labels, and Approve as the primary action.
|
||||
- Providers/Qwen: honor session thinking level for `qwen-chat-template` payloads so `/think off` disables nested llama.cpp chat-template thinking controls. Fixes #82768. Thanks @bfox55.
|
||||
|
||||
@@ -2298,6 +2298,85 @@ describe("openai transport stream", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes overlong Copilot Responses replay tool ids before dispatch", () => {
|
||||
const longToolItemId = "iVec" + "A".repeat(360);
|
||||
const longToolCallId = `call_ug6lFGKwZDjHfzW8H0PDQRwN|${longToolItemId}`;
|
||||
const params = buildOpenAIResponsesParams(
|
||||
{
|
||||
id: "gpt-5.5",
|
||||
name: "GPT-5.5",
|
||||
api: "openai-responses",
|
||||
provider: "github-copilot",
|
||||
baseUrl: "https://api.githubcopilot.com",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 200000,
|
||||
maxTokens: 8192,
|
||||
} satisfies Model<"openai-responses">,
|
||||
{
|
||||
systemPrompt: "system",
|
||||
messages: [
|
||||
{ role: "user", content: "read the queue", timestamp: 0 },
|
||||
{
|
||||
role: "assistant",
|
||||
api: "openai-responses",
|
||||
provider: "github-copilot",
|
||||
model: "gpt-5.5",
|
||||
usage: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
stopReason: "toolUse",
|
||||
timestamp: 1,
|
||||
content: [
|
||||
{
|
||||
type: "toolCall",
|
||||
id: longToolCallId,
|
||||
name: "exec",
|
||||
arguments: { command: "gh pr list --limit 1" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: longToolCallId,
|
||||
toolName: "exec",
|
||||
content: [{ type: "text", text: "[]" }],
|
||||
isError: false,
|
||||
timestamp: 2,
|
||||
},
|
||||
{ role: "user", content: "continue", timestamp: 3 },
|
||||
],
|
||||
tools: [],
|
||||
} as never,
|
||||
{ sessionId: "session-123" },
|
||||
) as {
|
||||
input?: Array<{ type?: string; id?: string; call_id?: string }>;
|
||||
};
|
||||
|
||||
const functionCall = params.input?.find((item) => item.type === "function_call");
|
||||
const functionOutput = params.input?.find((item) => item.type === "function_call_output");
|
||||
expect(functionCall).toBeDefined();
|
||||
expect(functionOutput).toBeDefined();
|
||||
expect(functionCall?.id).toMatch(/^fc_/);
|
||||
expect(functionCall?.id?.length).toBeLessThanOrEqual(64);
|
||||
expect(functionCall?.call_id).toBe("call_ug6lFGKwZDjHfzW8H0PDQRwN");
|
||||
expect(functionOutput?.call_id).toBe(functionCall?.call_id);
|
||||
for (const item of params.input ?? []) {
|
||||
if (item.id !== undefined) {
|
||||
expect(item.id.length).toBeLessThanOrEqual(64);
|
||||
}
|
||||
if (item.call_id !== undefined) {
|
||||
expect(item.call_id.length).toBeLessThanOrEqual(64);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("adds minimal user input for Codex responses when only the system prompt is present", () => {
|
||||
const params = buildOpenAIResponsesParams(
|
||||
{
|
||||
|
||||
@@ -767,6 +767,7 @@ function convertResponsesMessages(
|
||||
const messages: ResponseInput = [];
|
||||
const shouldReplayReasoningItems = options?.replayReasoningItems ?? true;
|
||||
const shouldReplayResponsesItemIds = options?.replayResponsesItemIds ?? true;
|
||||
const shouldNormalizeSameModelToolCallIds = model.provider === "github-copilot";
|
||||
const normalizeIdPart = (part: string) => {
|
||||
const sanitized = part.replace(/[^a-zA-Z0-9_-]/g, "_");
|
||||
const normalized = sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized;
|
||||
@@ -802,6 +803,7 @@ function convertResponsesMessages(
|
||||
context.messages,
|
||||
model,
|
||||
normalizeToolCallId,
|
||||
{ normalizeSameModelToolCallIds: shouldNormalizeSameModelToolCallIds },
|
||||
);
|
||||
const includeSystemPrompt = options?.includeSystemPrompt ?? true;
|
||||
if (includeSystemPrompt && context.systemPrompt) {
|
||||
@@ -1692,7 +1694,7 @@ export function buildOpenAIResponsesParams(
|
||||
const messages = convertResponsesMessages(
|
||||
model,
|
||||
context,
|
||||
new Set(["openai", "openai-codex", "opencode", "azure-openai-responses"]),
|
||||
new Set(["openai", "openai-codex", "opencode", "azure-openai-responses", "github-copilot"]),
|
||||
{
|
||||
includeSystemPrompt: !isCodexResponses,
|
||||
supportsDeveloperRole,
|
||||
|
||||
@@ -45,7 +45,10 @@ export function transformTransportMessages(
|
||||
targetModel: Model<Api>,
|
||||
source: { provider: string; api: Api; model: string },
|
||||
) => string,
|
||||
options?: { preserveCrossModelToolCallThoughtSignature?: boolean },
|
||||
options?: {
|
||||
normalizeSameModelToolCallIds?: boolean;
|
||||
preserveCrossModelToolCallThoughtSignature?: boolean;
|
||||
},
|
||||
): Context["messages"] {
|
||||
const allowSyntheticToolResults = defaultAllowSyntheticToolResults(model.api);
|
||||
const syntheticToolResultText = CODEX_STYLE_ABORTED_OUTPUT_APIS.has(model.api)
|
||||
@@ -103,7 +106,10 @@ export function transformTransportMessages(
|
||||
normalizedToolCall = { ...normalizedToolCall };
|
||||
delete normalizedToolCall.thoughtSignature;
|
||||
}
|
||||
if (!isSameModel && normalizeToolCallId) {
|
||||
if (
|
||||
(!isSameModel || options?.normalizeSameModelToolCallIds === true) &&
|
||||
normalizeToolCallId
|
||||
) {
|
||||
const normalizedId = normalizeToolCallId(block.id, model, msg);
|
||||
if (normalizedId !== block.id) {
|
||||
toolCallIdMap.set(block.id, normalizedId);
|
||||
|
||||
Reference in New Issue
Block a user