mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 21:24:46 +00:00
fix: stabilize OpenAI tool payload ordering
This commit is contained in:
@@ -2932,6 +2932,53 @@ describe("openai transport stream", () => {
|
||||
expect(params.tool_choice).toBe("required");
|
||||
});
|
||||
|
||||
it("sorts Responses tools by name for stable prompt-cache payloads", () => {
|
||||
const model = {
|
||||
id: "gpt-5.4",
|
||||
name: "GPT-5.4",
|
||||
api: "openai-responses",
|
||||
provider: "openai",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 200000,
|
||||
maxTokens: 8192,
|
||||
} satisfies Model<"openai-responses">;
|
||||
const zetaTool = {
|
||||
name: "zeta",
|
||||
description: "Z",
|
||||
parameters: { type: "object", properties: {}, additionalProperties: false },
|
||||
};
|
||||
const alphaTool = {
|
||||
name: "alpha",
|
||||
description: "A",
|
||||
parameters: { type: "object", properties: {}, additionalProperties: false },
|
||||
};
|
||||
|
||||
const first = buildOpenAIResponsesParams(
|
||||
model,
|
||||
{
|
||||
systemPrompt: "system",
|
||||
messages: [],
|
||||
tools: [zetaTool, alphaTool],
|
||||
} as never,
|
||||
{ sessionId: "session-123" } as never,
|
||||
) as { tools?: Array<{ name?: string }> };
|
||||
const second = buildOpenAIResponsesParams(
|
||||
model,
|
||||
{
|
||||
systemPrompt: "system",
|
||||
messages: [],
|
||||
tools: [alphaTool, zetaTool],
|
||||
} as never,
|
||||
{ sessionId: "session-123" } as never,
|
||||
) as { tools?: Array<{ name?: string }> };
|
||||
|
||||
expect(first.tools?.map((tool) => tool.name)).toEqual(["alpha", "zeta"]);
|
||||
expect(first.tools).toEqual(second.tools);
|
||||
});
|
||||
|
||||
it("falls back to strict:false when a native OpenAI tool schema is not strict-compatible", () => {
|
||||
const params = buildOpenAIResponsesParams(
|
||||
{
|
||||
@@ -3809,6 +3856,54 @@ describe("openai transport stream", () => {
|
||||
expect(notOptedIn.prompt_cache_key).toBeUndefined();
|
||||
});
|
||||
|
||||
it("sorts Chat Completions tools by function name for stable prompt-cache payloads", () => {
|
||||
const model = {
|
||||
id: "custom-model",
|
||||
name: "Custom Model",
|
||||
api: "openai-completions",
|
||||
provider: "custom-cpa",
|
||||
baseUrl: "https://proxy.example.com/v1",
|
||||
compat: { supportsPromptCacheKey: true },
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 32768,
|
||||
maxTokens: 8192,
|
||||
} as unknown as Model<"openai-completions">;
|
||||
const zetaTool = {
|
||||
name: "zeta",
|
||||
description: "Z",
|
||||
parameters: { type: "object", properties: {} },
|
||||
};
|
||||
const alphaTool = {
|
||||
name: "alpha",
|
||||
description: "A",
|
||||
parameters: { type: "object", properties: {} },
|
||||
};
|
||||
|
||||
const first = buildOpenAICompletionsParams(
|
||||
model,
|
||||
{
|
||||
systemPrompt: "system",
|
||||
messages: [],
|
||||
tools: [zetaTool, alphaTool],
|
||||
} as never,
|
||||
{ sessionId: "session-123" },
|
||||
) as { tools?: Array<{ function?: { name?: string } }> };
|
||||
const second = buildOpenAICompletionsParams(
|
||||
model,
|
||||
{
|
||||
systemPrompt: "system",
|
||||
messages: [],
|
||||
tools: [alphaTool, zetaTool],
|
||||
} as never,
|
||||
{ sessionId: "session-123" },
|
||||
) as { tools?: Array<{ function?: { name?: string } }> };
|
||||
|
||||
expect(first.tools?.map((tool) => tool.function?.name)).toEqual(["alpha", "zeta"]);
|
||||
expect(first.tools).toEqual(second.tools);
|
||||
});
|
||||
|
||||
it("disables developer-role-only compat defaults for configured custom proxy completions providers", () => {
|
||||
const params = buildOpenAICompletionsParams(
|
||||
{
|
||||
|
||||
@@ -950,7 +950,7 @@ function convertResponsesTools(
|
||||
transport: "responses",
|
||||
model,
|
||||
});
|
||||
return tools.map((tool): FunctionTool => {
|
||||
return sortTransportToolsByName(tools).map((tool): FunctionTool => {
|
||||
const base = {
|
||||
type: "function" as const,
|
||||
name: tool.name,
|
||||
@@ -2686,7 +2686,7 @@ function convertTools(
|
||||
model,
|
||||
},
|
||||
);
|
||||
return tools.map((tool) => ({
|
||||
return sortTransportToolsByName(tools).map((tool) => ({
|
||||
type: "function",
|
||||
function: {
|
||||
name: tool.name,
|
||||
@@ -2701,6 +2701,28 @@ function convertTools(
|
||||
}));
|
||||
}
|
||||
|
||||
function compareTransportToolText(left: string | undefined, right: string | undefined): number {
|
||||
const leftText = left ?? "";
|
||||
const rightText = right ?? "";
|
||||
if (leftText < rightText) {
|
||||
return -1;
|
||||
}
|
||||
if (leftText > rightText) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function sortTransportToolsByName<T extends { name?: string; description?: string }>(
|
||||
tools: readonly T[],
|
||||
): T[] {
|
||||
return tools.toSorted(
|
||||
(left, right) =>
|
||||
compareTransportToolText(left.name, right.name) ||
|
||||
compareTransportToolText(left.description, right.description),
|
||||
);
|
||||
}
|
||||
|
||||
function extractGoogleThoughtSignature(toolCall: unknown): string | undefined {
|
||||
const tc = toolCall as Record<string, unknown> | undefined;
|
||||
if (!tc) {
|
||||
|
||||
Reference in New Issue
Block a user