Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -471,13 +471,17 @@ 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
"custom_param": .string("value")
]
)
```
`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

Expand Down
27 changes: 27 additions & 0 deletions Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@ public struct OpenAILanguageModel: LanguageModel {
/// Defaults to `true`.
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
/// in a response.
///
Expand Down Expand Up @@ -323,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.
Expand All @@ -346,6 +353,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,
Expand All @@ -369,6 +377,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
Expand Down Expand Up @@ -508,6 +517,24 @@ 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 })))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ struct OpenAICustomOptionsTests {
reasoningEffort: .high,
reasoning: .init(effort: .medium, summary: "concise"),
parallelToolCalls: false,
stopAfterToolCalls: true,
maxToolCalls: 10,
serviceTier: .priority,
store: true,
Expand All @@ -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)
Expand Down Expand Up @@ -416,7 +418,8 @@ struct OpenAICustomOptionsTests {
frequencyPenalty: 0.5,
topLogprobs: 5,
reasoningEffort: .medium,
parallelToolCalls: true
parallelToolCalls: true,
stopAfterToolCalls: true
)

let encoder = JSONEncoder()
Expand All @@ -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() {
Expand All @@ -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)
Expand Down