From 665b737c5ef75c071cd2286c3e6917247c354bae Mon Sep 17 00:00:00 2001 From: jasonxue Date: Mon, 2 Feb 2026 09:38:56 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=201.=20=E5=9C=A8=20OpenAILanguageMode?= =?UTF-8?q?l=20=E7=9A=84=E9=85=8D=E7=BD=AE=E9=80=89=E9=A1=B9=E4=B8=AD?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20stopAfterToolCalls=20=E5=B1=9E=E6=80=A7=20?= =?UTF-8?q?2.=20=E5=AE=9E=E7=8E=B0=E7=94=9F=E6=88=90=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E5=90=8E=E7=AB=8B=E5=8D=B3=E5=81=9C=E6=AD=A2?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E7=9A=84=E9=80=BB=E8=BE=91=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=9B=B4=E6=8E=A5=E8=BF=94=E5=9B=9E=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E4=BF=A1=E6=81=AF=E8=80=8C=E4=B8=8D=E8=A7=A6?= =?UTF-8?q?=E5=8F=91=E5=90=8E=E7=BB=AD=E6=89=A7=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Models/OpenAILanguageModel.swift | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift b/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift index 6299ce0..9323834 100644 --- a/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift +++ b/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift @@ -145,6 +145,9 @@ public struct OpenAILanguageModel: LanguageModel { /// Defaults to `true`. public var parallelToolCalls: Bool? + /// Whether to stop generation immediately after tool calls are generated. + public var stopAfterToolCalls: Bool? + /// The maximum number of total calls to built-in tools that can be processed /// in a response. /// @@ -346,6 +349,7 @@ public struct OpenAILanguageModel: LanguageModel { reasoningEffort: ReasoningEffort? = nil, reasoning: ReasoningConfiguration? = nil, parallelToolCalls: Bool? = nil, + stopAfterToolCalls: Bool? = nil, maxToolCalls: Int? = nil, serviceTier: ServiceTier? = nil, store: Bool? = nil, @@ -369,6 +373,7 @@ public struct OpenAILanguageModel: LanguageModel { self.reasoningEffort = reasoningEffort self.reasoning = reasoning self.parallelToolCalls = parallelToolCalls + self.stopAfterToolCalls = stopAfterToolCalls self.maxToolCalls = maxToolCalls self.serviceTier = serviceTier self.store = store @@ -508,6 +513,23 @@ public struct OpenAILanguageModel: LanguageModel { if let value = try? JSONValue(toolCallMessage) { messages.append(OpenAIMessage(role: .raw(rawContent: value), content: .text(""))) } + + if let stop = options[custom: OpenAILanguageModel.self]?.stopAfterToolCalls, stop { + let calls = toolCalls.map { tc in + Transcript.ToolCall( + id: tc.id ?? UUID().uuidString, + toolName: tc.function?.name ?? "", + arguments: (try? GeneratedContent(json: tc.function?.arguments ?? "{}")) ?? GeneratedContent(tc.function?.arguments ?? "") + ) + } + entries.append(.toolCalls(Transcript.ToolCalls(calls))) + return LanguageModelSession.Response( + content: "" as! Content, + rawContent: GeneratedContent(""), + transcriptEntries: ArraySlice(entries) + ) + } + let invocations = try await resolveToolCalls(toolCalls, session: session) if !invocations.isEmpty { entries.append(.toolCalls(Transcript.ToolCalls(invocations.map { $0.call }))) From b5785985b16d59edde40615e62ba9e112e9ca2ed Mon Sep 17 00:00:00 2001 From: jasonxue Date: Thu, 5 Feb 2026 09:36:21 +0800 Subject: [PATCH 2/3] Add stop-after-tool-calls option docs and tests --- README.md | 4 ++++ Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift | 4 ++++ .../CustomGenerationOptionsTests.swift | 7 ++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 99a0569..fd87c16 100644 --- a/README.md +++ b/README.md @@ -471,6 +471,7 @@ options[custom: OpenAILanguageModel.self] = .init( frequencyPenalty: 0.5, presencePenalty: 0.3, stopSequences: ["END"], + stopAfterToolCalls: true, reasoningEffort: .high, // For reasoning models (o3, o4-mini) serviceTier: .priority, extraBody: [ // Vendor-specific parameters @@ -478,6 +479,9 @@ options[custom: OpenAILanguageModel.self] = .init( ] ) ``` +`stopAfterToolCalls` lets you return tool calls immediately without executing +tools, which is useful when you want to delegate tool execution to your own +orchestrator. ### Anthropic diff --git a/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift b/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift index 9323834..12cf51a 100644 --- a/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift +++ b/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift @@ -146,6 +146,9 @@ public struct OpenAILanguageModel: LanguageModel { public var parallelToolCalls: Bool? /// Whether to stop generation immediately after tool calls are generated. + /// + /// When enabled, the response returns the tool call transcript entries + /// without executing tools, and the content is empty. public var stopAfterToolCalls: Bool? /// The maximum number of total calls to built-in tools that can be processed @@ -326,6 +329,7 @@ public struct OpenAILanguageModel: LanguageModel { /// - reasoningEffort: Reasoning effort for reasoning models. /// - reasoning: Reasoning configuration (Responses API). /// - parallelToolCalls: Whether to allow parallel tool calls. + /// - stopAfterToolCalls: Whether to return tool calls without executing them. /// - maxToolCalls: Maximum number of tool calls (Responses API). /// - serviceTier: Service tier for request processing. /// - store: Whether to store the response. diff --git a/Tests/AnyLanguageModelTests/CustomGenerationOptionsTests.swift b/Tests/AnyLanguageModelTests/CustomGenerationOptionsTests.swift index 3486fe8..c37a19c 100644 --- a/Tests/AnyLanguageModelTests/CustomGenerationOptionsTests.swift +++ b/Tests/AnyLanguageModelTests/CustomGenerationOptionsTests.swift @@ -334,6 +334,7 @@ struct OpenAICustomOptionsTests { reasoningEffort: .high, reasoning: .init(effort: .medium, summary: "concise"), parallelToolCalls: false, + stopAfterToolCalls: true, maxToolCalls: 10, serviceTier: .priority, store: true, @@ -359,6 +360,7 @@ struct OpenAICustomOptionsTests { #expect(options.reasoning?.effort == .medium) #expect(options.reasoning?.summary == "concise") #expect(options.parallelToolCalls == false) + #expect(options.stopAfterToolCalls == true) #expect(options.maxToolCalls == 10) #expect(options.serviceTier == .priority) #expect(options.store == true) @@ -416,7 +418,8 @@ struct OpenAICustomOptionsTests { frequencyPenalty: 0.5, topLogprobs: 5, reasoningEffort: .medium, - parallelToolCalls: true + parallelToolCalls: true, + stopAfterToolCalls: true ) let encoder = JSONEncoder() @@ -429,6 +432,7 @@ struct OpenAICustomOptionsTests { #expect(json.contains("\"top_logprobs\"")) #expect(json.contains("\"reasoning_effort\"")) #expect(json.contains("\"parallel_tool_calls\"")) + #expect(!json.contains("stop_after_tool_calls")) } @Test func nilProperties() { @@ -446,6 +450,7 @@ struct OpenAICustomOptionsTests { #expect(options.reasoningEffort == nil) #expect(options.reasoning == nil) #expect(options.parallelToolCalls == nil) + #expect(options.stopAfterToolCalls == nil) #expect(options.maxToolCalls == nil) #expect(options.serviceTier == nil) #expect(options.store == nil) From d7723b45b6290bc37327b23abb7c88ec84aff687 Mon Sep 17 00:00:00 2001 From: jasonxue Date: Thu, 5 Feb 2026 10:42:23 +0800 Subject: [PATCH 3/3] Fix formatting for stop-after-tool-calls --- Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift b/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift index 12cf51a..018b5f2 100644 --- a/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift +++ b/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift @@ -523,7 +523,8 @@ public struct OpenAILanguageModel: LanguageModel { Transcript.ToolCall( id: tc.id ?? UUID().uuidString, toolName: tc.function?.name ?? "", - arguments: (try? GeneratedContent(json: tc.function?.arguments ?? "{}")) ?? GeneratedContent(tc.function?.arguments ?? "") + arguments: (try? GeneratedContent(json: tc.function?.arguments ?? "{}")) + ?? GeneratedContent(tc.function?.arguments ?? "") ) } entries.append(.toolCalls(Transcript.ToolCalls(calls)))