mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 21:21:10 +00:00
fix(gateway): /v1/responses tool schema should use flat Responses API format (#57166)
* gateway: fix /v1/responses tool schema to use flat Responses API format * gateway: fix remaining stale wrapped-format tools in parity tests * gateway: propagate strict flag through extractClientTools normalization * fix(gateway): cover responses tool boundary * Delete docs/internal/vincentkoc/2026-03-30-pr-57166-responses-tool-schema-followup.md --------- Co-authored-by: Michel Belleau <mbelleau@Michels-MacBook-Pro.local> Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -39,6 +39,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/context pruning: count supplementary-plane CJK characters with the shared code-point-aware estimator so context pruning stops underestimating Japanese and Chinese text that uses Extension B ideographs. (#39985) Thanks @Edward-Qiang-2024.
|
||||
- Slack/status reactions: add a reaction lifecycle for queued, thinking, tool, done, and error phases in Slack monitors, with safer cleanup so queued ack reactions stay correct across silent runs, pre-reply failures, and delayed transitions. (#56430) Thanks @hsiaoa.
|
||||
- macOS/local gateway: stop OpenClaw.app from killing healthy local gateway listeners after startup by recognizing the current `openclaw-gateway` process title and using the current `openclaw gateway` launch shape.
|
||||
- Gateway/OpenAI compatibility: accept flat Responses API function tool definitions on `/v1/responses` and preserve `strict` when normalizing hosted tools into the embedded runner, so spec-compliant clients like Codex no longer fail validation or silently lose strict tool enforcement. Thanks @malaiwah and @vincentkoc.
|
||||
- Memory/QMD: resolve slugified `memory_search` file hints back to the indexed filesystem path before returning search hits, so `memory_get` works again for mixed-case and spaced paths. (#50313) Thanks @erra9x.
|
||||
- OpenAI/Codex fast mode: map `/fast` to priority processing on native OpenAI and Codex Responses endpoints instead of rewriting reasoning settings, and document the exact endpoint and override behavior.
|
||||
- Memory/QMD: weight CJK-heavy text correctly when estimating chunk sizes, preserve surrogate-pair characters during fine splits, and keep long Latin lines on the old chunk boundaries so memory indexing produces better-sized chunks for CJK notes. (#40271) Thanks @AaronLuo00.
|
||||
|
||||
@@ -17,6 +17,8 @@ export type ClientToolDefinition = {
|
||||
name: string;
|
||||
description?: string;
|
||||
parameters?: Record<string, unknown>;
|
||||
/** Strict argument enforcement (Responses API). Propagated from the request. */
|
||||
strict?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -148,18 +148,18 @@ export type ItemParam = z.infer<typeof ItemParamSchema>;
|
||||
// Tool Definitions
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Responses API tool definition uses a flat format (not the Chat Completions
|
||||
// wrapped-function format). Fields are at the top level alongside `type`.
|
||||
export const FunctionToolDefinitionSchema = z
|
||||
.object({
|
||||
type: z.literal("function"),
|
||||
function: z.object({
|
||||
name: z.string().min(1, "Tool name cannot be empty"),
|
||||
description: z.string().optional(),
|
||||
parameters: z.record(z.string(), z.unknown()).optional(),
|
||||
}),
|
||||
name: z.string().min(1, "Tool name cannot be empty"),
|
||||
description: z.string().optional(),
|
||||
parameters: z.record(z.string(), z.unknown()).optional(),
|
||||
strict: z.boolean().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
// OpenResponses tool definitions match internal ToolDefinition structure
|
||||
export const ToolDefinitionSchema = FunctionToolDefinitionSchema;
|
||||
|
||||
export type ToolDefinition = z.infer<typeof ToolDefinitionSchema>;
|
||||
|
||||
@@ -114,7 +114,8 @@ async function ensureResponseConsumed(res: Response) {
|
||||
const WEATHER_TOOL = [
|
||||
{
|
||||
type: "function",
|
||||
function: { name: "get_weather", description: "Get weather" },
|
||||
name: "get_weather",
|
||||
description: "Get weather",
|
||||
},
|
||||
] as const;
|
||||
|
||||
@@ -511,11 +512,14 @@ describe("OpenResponses HTTP API (e2e)", () => {
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
function: { name: "get_weather", description: "Get weather" },
|
||||
name: "get_weather",
|
||||
description: "Get weather",
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: { name: "get_time", description: "Get time" },
|
||||
name: "get_time",
|
||||
description: "Get time",
|
||||
strict: true,
|
||||
},
|
||||
],
|
||||
tool_choice: { type: "function", function: { name: "get_time" } },
|
||||
@@ -523,10 +527,16 @@ describe("OpenResponses HTTP API (e2e)", () => {
|
||||
expect(resToolChoice.status).toBe(200);
|
||||
const optsToolChoice = (agentCommand.mock.calls[0] as unknown[] | undefined)?.[0];
|
||||
const clientTools =
|
||||
(optsToolChoice as { clientTools?: Array<{ function?: { name?: string } }> } | undefined)
|
||||
?.clientTools ?? [];
|
||||
(
|
||||
optsToolChoice as
|
||||
| {
|
||||
clientTools?: Array<{ function?: { name?: string; strict?: boolean } }>;
|
||||
}
|
||||
| undefined
|
||||
)?.clientTools ?? [];
|
||||
expect(clientTools).toHaveLength(1);
|
||||
expect(clientTools[0]?.function?.name).toBe("get_time");
|
||||
expect(clientTools[0]?.function?.strict).toBe(true);
|
||||
await ensureResponseConsumed(resToolChoice);
|
||||
|
||||
const resUnknownTool = await postResponses(port, {
|
||||
|
||||
@@ -255,7 +255,16 @@ function resolveResponsesLimits(
|
||||
}
|
||||
|
||||
function extractClientTools(body: CreateResponseBody): ClientToolDefinition[] {
|
||||
return (body.tools ?? []) as ClientToolDefinition[];
|
||||
// Normalize from Responses API flat format to the internal wrapped format.
|
||||
return (body.tools ?? []).map((tool) => ({
|
||||
type: "function",
|
||||
function: {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: tool.parameters,
|
||||
strict: tool.strict,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
function applyToolChoice(params: {
|
||||
|
||||
@@ -110,19 +110,17 @@ describe("OpenResponses Feature Parity", () => {
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should validate tool definition", async () => {
|
||||
it("should validate tool definition in flat Responses API format", async () => {
|
||||
const validTool = {
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "get_weather",
|
||||
description: "Get the current weather",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
location: { type: "string" },
|
||||
},
|
||||
required: ["location"],
|
||||
name: "get_weather",
|
||||
description: "Get the current weather",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
location: { type: "string" },
|
||||
},
|
||||
required: ["location"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -130,13 +128,24 @@ describe("OpenResponses Feature Parity", () => {
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should reject wrapped Chat Completions format (function: {...} wrapper)", async () => {
|
||||
const wrappedTool = {
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "get_weather",
|
||||
description: "Get the current weather",
|
||||
},
|
||||
};
|
||||
|
||||
const result = ToolDefinitionSchema.safeParse(wrappedTool);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("should reject tool definition without name", async () => {
|
||||
const invalidTool = {
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "", // Empty name
|
||||
description: "Get the current weather",
|
||||
},
|
||||
name: "", // Empty name
|
||||
description: "Get the current weather",
|
||||
};
|
||||
|
||||
const result = ToolDefinitionSchema.safeParse(invalidTool);
|
||||
@@ -186,16 +195,14 @@ describe("OpenResponses Feature Parity", () => {
|
||||
tools: [
|
||||
{
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "get_weather",
|
||||
description: "Get weather for a location",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
location: { type: "string" },
|
||||
},
|
||||
required: ["location"],
|
||||
name: "get_weather",
|
||||
description: "Get weather for a location",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
location: { type: "string" },
|
||||
},
|
||||
required: ["location"],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -234,10 +241,8 @@ describe("OpenResponses Feature Parity", () => {
|
||||
tools: [
|
||||
{
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: "get_weather",
|
||||
description: "Get weather for a location",
|
||||
},
|
||||
name: "get_weather",
|
||||
description: "Get weather for a location",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user