





















@@ -714,6 +714,19 @@ describe("OpenAI-compatible HTTP API (e2e)", () => {
714714715715{
716716agentCommand.mockClear();
717+agentCommand.mockResolvedValueOnce({
718+payloads: [{ text: "tool choice function" }],
719+meta: {
720+stopReason: "tool_calls",
721+pendingToolCalls: [
722+{
723+id: "call_1",
724+name: "get_weather",
725+arguments: '{"city":"Taipei"}',
726+},
727+],
728+},
729+} as never);
717730const res = await postChatCompletions(port, {
718731model: "openclaw",
719732tool_choice: { type: "function", function: { name: "get_weather" } },
@@ -741,11 +754,121 @@ describe("OpenAI-compatible HTTP API (e2e)", () => {
741754],
742755messages: [{ role: "user", content: "weather?" }],
743756});
744-expect(res.status).toBe(400);
757+expect(res.status).toBe(200);
758+const firstCall = getFirstAgentCall();
759+const clientTools = firstCall?.clientTools ?? [];
760+expect(clientTools).toHaveLength(1);
761+expect(clientTools[0]?.function?.name).toBe("get_weather");
762+expect(firstCall?.extraSystemPrompt ?? "").toContain("You must call the get_weather tool");
763+const json = (await res.json()) as { choices?: Array<{ finish_reason?: string | null }> };
764+expect(json.choices?.[0]?.finish_reason).toBe("tool_calls");
765+}
766+767+{
768+agentCommand.mockClear();
769+agentCommand.mockResolvedValueOnce({
770+payloads: [{ text: "tool choice required" }],
771+meta: {
772+stopReason: "tool_calls",
773+pendingToolCalls: [
774+{
775+id: "call_1",
776+name: "get_weather",
777+arguments: '{"city":"Taipei"}',
778+},
779+],
780+},
781+} as never);
782+const res = await postChatCompletions(port, {
783+model: "openclaw",
784+tool_choice: "required",
785+tools: [
786+{
787+type: "function",
788+function: {
789+name: "get_weather",
790+description: "Get current weather",
791+parameters: {
792+type: "object",
793+properties: { city: { type: "string" } },
794+required: ["city"],
795+},
796+},
797+},
798+],
799+messages: [{ role: "user", content: "weather?" }],
800+});
801+expect(res.status).toBe(200);
802+const firstCall = getFirstAgentCall();
803+const clientTools = firstCall?.clientTools ?? [];
804+expect(clientTools).toHaveLength(1);
805+expect(clientTools[0]?.function?.name).toBe("get_weather");
806+expect(firstCall?.extraSystemPrompt ?? "").toContain(
807+"You must call one of the available tools",
808+);
809+const json = (await res.json()) as { choices?: Array<{ finish_reason?: string | null }> };
810+expect(json.choices?.[0]?.finish_reason).toBe("tool_calls");
811+}
812+813+{
814+mockAgentOnce([{ text: "plain text despite required" }]);
815+const res = await postChatCompletions(port, {
816+model: "openclaw",
817+tool_choice: "required",
818+tools: [
819+{
820+type: "function",
821+function: {
822+name: "get_weather",
823+description: "Get current weather",
824+parameters: { type: "object", properties: {} },
825+},
826+},
827+],
828+messages: [{ role: "user", content: "weather?" }],
829+});
830+expect(res.status).toBe(502);
745831const json = (await res.json()) as { error?: { type?: string; message?: string } };
746-expect(json.error?.type).toBe("invalid_request_error");
747-expect(json.error?.message ?? "").toContain("not supported");
748-expect(agentCommand).toHaveBeenCalledTimes(0);
832+expect(json.error?.type).toBe("api_error");
833+expect(json.error?.message ?? "").toContain("tool_choice=required was not satisfied");
834+}
835+836+{
837+agentCommand.mockClear();
838+agentCommand.mockResolvedValueOnce({
839+payloads: [{ text: "Calling a different tool." }],
840+meta: {
841+stopReason: "tool_calls",
842+pendingToolCalls: [{ id: "call_1", name: "get_time", arguments: "{}" }],
843+},
844+} as never);
845+const res = await postChatCompletions(port, {
846+model: "openclaw",
847+tool_choice: { type: "function", function: { name: "get_weather" } },
848+tools: [
849+{
850+type: "function",
851+function: {
852+name: "get_weather",
853+description: "Get current weather",
854+parameters: { type: "object", properties: {} },
855+},
856+},
857+{
858+type: "function",
859+function: {
860+name: "get_time",
861+description: "Get current time",
862+parameters: { type: "object", properties: {} },
863+},
864+},
865+],
866+messages: [{ role: "user", content: "weather?" }],
867+});
868+expect(res.status).toBe(502);
869+const json = (await res.json()) as { error?: { type?: string; message?: string } };
870+expect(json.error?.type).toBe("api_error");
871+expect(json.error?.message ?? "").toContain("tool_choice required a get_weather tool call");
749872}
750873751874{
@@ -758,7 +881,7 @@ describe("OpenAI-compatible HTTP API (e2e)", () => {
758881expect(res.status).toBe(400);
759882const json = (await res.json()) as { error?: { type?: string; message?: string } };
760883expect(json.error?.type).toBe("invalid_request_error");
761-expect(json.error?.message ?? "").toContain("tool_choice=required");
884+expect(json.error?.message ?? "").toContain("no tools were provided");
762885expect(agentCommand).toHaveBeenCalledTimes(0);
763886}
764887@@ -782,7 +905,7 @@ describe("OpenAI-compatible HTTP API (e2e)", () => {
782905expect(res.status).toBe(400);
783906const json = (await res.json()) as { error?: { type?: string; message?: string } };
784907expect(json.error?.type).toBe("invalid_request_error");
785-expect(json.error?.message ?? "").toContain("not supported");
908+expect(json.error?.message ?? "").toContain("unknown tool");
786909expect(agentCommand).toHaveBeenCalledTimes(0);
787910}
788911@@ -1739,6 +1862,39 @@ describe("OpenAI-compatible HTTP API (e2e)", () => {
17391862expect(fallbackText).toContain("hello");
17401863}
174118641865+{
1866+agentCommand.mockClear();
1867+agentCommand.mockImplementationOnce((async (opts: unknown) =>
1868+buildAssistantDeltaResult({
1869+ opts,
1870+emit: emitAgentEvent,
1871+deltas: ["plain text despite required"],
1872+text: "plain text despite required",
1873+})) as never);
1874+1875+const requiredFailureRes = await postChatCompletions(port, {
1876+stream: true,
1877+model: "openclaw",
1878+tool_choice: "required",
1879+tools: [
1880+{
1881+type: "function",
1882+function: {
1883+name: "get_weather",
1884+description: "Get weather",
1885+parameters: { type: "object", properties: {} },
1886+},
1887+},
1888+],
1889+messages: [{ role: "user", content: "weather?" }],
1890+});
1891+expect(requiredFailureRes.status).toBe(200);
1892+const requiredFailureText = await requiredFailureRes.text();
1893+expect(requiredFailureText).toContain("[DONE]");
1894+expect(requiredFailureText).toContain("tool_choice=required was not satisfied");
1895+expect(requiredFailureText).not.toContain("plain text despite required");
1896+}
1897+17421898{
17431899agentCommand.mockClear();
17441900agentCommand.mockResolvedValueOnce({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。