| name | using-swift-open-responses-dsl |
| description | Helps the agent use SwiftOpenResponsesDSL to build type-safe LLM request pipelines for the Open Responses API with tool calling, sessions, and persistent agents with conversation continuity via previous_response_id. Useful when defining ResponseRequest objects, wiring LLMTool instances into a ToolSession or Agent, chaining responses across turns, or processing streaming events in Swift.
|
Using SwiftOpenResponsesDSL
Installation
Add to Package.swift:
dependencies: [
.package(url: "https://github.com/RichNasz/SwiftOpenResponsesDSL.git", from: "0.1.0"),
.package(url: "https://github.com/RichNasz/SwiftLLMToolMacros.git", from: "0.1.0")
]
Target dependencies:
.target(
name: "YourTarget",
dependencies: [
"SwiftOpenResponsesDSL",
"SwiftLLMToolMacros"
]
)
Imports at the top of each file:
import SwiftOpenResponsesDSL
import SwiftLLMToolMacros
LLMClient
let client = try LLMClient(
baseURL: "https://api.openai.com/v1/responses",
apiKey: "your-api-key"
)
Basic Request
let request = try ResponseRequest(model: "gpt-4o", text: "Explain async/await in Swift")
let response = try await client.send(request)
print(response.firstOutputText ?? "")
let request = try ResponseRequest(model: "gpt-4o") {
System("You are a helpful assistant.")
User("Explain async/await in Swift.")
}
let response = try await client.send(request)
Convenience Input Functions
| Function | Creates |
|---|
System("...") | System message InputItem |
Developer("...") | Developer message InputItem |
User("...") | User message InputItem |
UserImage("url", detail: "auto") | User image InputItem — image_url encodes as a flat string, detail ("low", "high", "auto") is a sibling field |
FunctionOutput(callId: "...", output: "...") | Function call output InputItem |
Configuration Parameters
Use @ResponseConfigBuilder to compose parameters:
let request = try ResponseRequest(model: "gpt-4o", config: {
try Temperature(0.7)
try MaxOutputTokens(2000)
Reasoning(effort: .high, summary: .concise)
}) {
User("Explain quantum computing.")
}
| Parameter | Init | Range/Constraint |
|---|
Temperature | try Temperature(0.7) | 0.0–2.0 |
TopP | try TopP(0.9) | 0.0–1.0 |
FrequencyPenalty | try FrequencyPenalty(0.5) | -2.0–2.0 |
PresencePenalty | try PresencePenalty(0.5) | -2.0–2.0 |
MaxOutputTokens | try MaxOutputTokens(1000) | > 0 |
Instructions | try Instructions("...") | non-empty |
PreviousResponseId | try PreviousResponseId("resp_...") | non-empty |
Reasoning | Reasoning(effort: .high, summary: .concise) | see Reasoning section |
TruncationParam | TruncationParam(.auto) | .auto / .disabled |
ServiceTierParam | ServiceTierParam(.auto) | .auto / .default / .flex / .priority |
Metadata | Metadata(["key": "val"]) | dictionary |
ParallelToolCalls | ParallelToolCalls(true) | Bool |
IncludeParam | try IncludeParam(["reasoning.encrypted_content"]) | non-empty array |
RequestTimeout | try RequestTimeout(60) | 10–900 seconds |
ResourceTimeout | try ResourceTimeout(300) | 30–3600 seconds |
Store | Store(true) | Bool |
Background | Background(true) | Bool |
MaxToolCalls | try MaxToolCalls(5) | > 0 |
TextConfig | TextConfig(TextParam(format: .jsonObject)) | see Structured Output |
ToolChoiceParam | ToolChoiceParam(.required) | see ToolChoice |
Parameters with try validate their input and throw LLMError.invalidValue on failure.
Conversation Continuity
The Responses API does not require re-sending full message history. Chain responses using previous_response_id:
let first = try ResponseRequest(model: "gpt-4o", text: "My name is Alice.")
let firstResponse = try await client.send(first)
let followUp = try ResponseRequest(model: "gpt-4o", config: {
try PreviousResponseId(firstResponse.id)
}, text: "What is my name?")
let followUpResponse = try await client.send(followUp)
Reasoning Models
Configure reasoning with Reasoning(effort:summary:):
let request = try ResponseRequest(model: "o3-mini", config: {
Reasoning(effort: .high, summary: .concise)
}) {
User("Solve this step by step: what is 127 * 89?")
}
let response = try await client.send(request)
ReasoningEffort: .none, .low, .medium, .high, .xhigh
ReasoningSummaryType: .concise, .detailed, .auto
Accessing Reasoning Output
response.reasoningItems
response.firstReasoningItem
response.reasoningTokens
item.id
item.summaryText
item.contentText
item.encryptedContent
Reasoning Streaming Events
case .reasoningSummaryPartAdded(let part, let index, let summaryIndex)
case .reasoningSummaryPartDone(let part, let index, let summaryIndex)
Structured Output
Control the response format with TextConfig:
TextConfig(TextParam(format: .jsonObject))
TextConfig(TextParam(format: .jsonSchema(
name: "weather_response",
schema: .object(properties: ["temp": .string()], required: ["temp"]),
strict: true
)))
TextConfig(TextParam(verbosity: .low))
Tool Calling
Defining Tools
Macro-powered (recommended): Use @LLMTool from SwiftLLMToolMacros.
@LLMTool
struct GetWeather {
@LLMToolArguments
struct Arguments {
@LLMToolGuide(description: "City and state, e.g. San Francisco, CA")
var location: String
@LLMToolGuide(description: "Temperature unit", .anyOf(["celsius", "fahrenheit"]))
var unit: String?
}
func call(arguments: Arguments) async throws -> ToolOutput {
ToolOutput(content: "{\"temperature\": \"72F\"}")
}
}
Manual: Construct a FunctionToolParam directly.
let weatherTool = FunctionToolParam(
name: "get_weather",
description: "Get current weather for a city",
parameters: .object(
properties: ["city": .string(description: "City name")],
required: ["city"]
)
)
Bridging from a ToolDefinition:
let functionTool = FunctionToolParam(from: GetWeather.toolDefinition)
let strictTool = FunctionToolParam(from: GetWeather.toolDefinition, strict: true)
@ToolsBuilder composes [FunctionToolParam] arrays declaratively, though @SessionBuilder is preferred for most use cases.
AgentTool — Bridging a Tool to a Session
AgentTool pairs a FunctionToolParam definition with its handler closure.
AgentTool(GetWeather())
AgentTool(GetWeather(), strict: true)
AgentTool(tool: weatherTool) { argumentsJSON in
return "{\"temperature\": \"72F\"}"
}
The ToolHandler type is @Sendable (String) async throws -> String.
ToolChoice
Control how the model selects tools:
ToolChoiceParam(.auto)
ToolChoiceParam(.none)
ToolChoiceParam(.required)
ToolChoiceParam(.function("get_weather"))
ToolChoiceParam(.allowedTools(mode: .auto, tools: ["get_weather"]))
ToolChoiceMode for .allowedTools: .none, .auto, .required.
ToolSession
ToolSession orchestrates the tool-calling loop, accumulating full conversation history across iterations until the model produces a final response.
Declarative Init (recommended)
let session = ToolSession(client: client, model: "gpt-4o") {
System("You are a weather assistant.")
AgentTool(GetWeather())
}
let result = try await session.run("What's the weather in Paris?")
print(result.response.firstOutputText ?? "")
Explicit Init
let session = ToolSession(
client: client,
tools: [weatherTool],
handlers: ["get_weather": { args in "{\"temperature\": \"72F\"}" }]
)
let result = try await session.run(
model: "gpt-4o",
input: [User("What's the weather in Paris?")]
)
Note: ToolSession uses input: [InputItem], not messages:. The two inits differ in their run call signature.
Streaming
let stream = session.stream("What's the weather in Paris?")
for try await event in stream {
switch event {
case .iterationStarted(let n):
print("[iteration \(n)]")
case .llm(let streamEvent):
break
case .toolCallStarted(let callId, let name, let arguments):
print("[calling \(name)]")
case .toolCallCompleted(let callId, let name, let output, let duration):
print("[done: \(name) in \(duration)]")
case .usageUpdate(let usage, let iteration):
print("[iteration \(iteration): \(usage.totalTokens) tokens]")
}
}
ToolSessionResult
result.response
result.iterations
result.log
result.iterationUsages
result.totalUsage
Agent
Agent is an actor for persistent multi-turn conversations. It uses previous_response_id for continuity — it does not re-send the full history on each turn.
Declarative Init (recommended)
let agent = try Agent(client: client, model: "gpt-4o") {
System("You are a weather assistant.")
AgentTool(GetWeather())
}
let reply1 = try await agent.run("What's the weather in Paris?")
let reply2 = try await agent.run("How about London?")
Explicit Init
let agent = Agent(
client: client,
model: "gpt-4o",
instructions: "You are a weather assistant.",
tools: [weatherTool],
toolHandlers: ["get_weather": { args in "{\"temperature\": \"72F\"}" }]
)
Note: Agent uses instructions:, not systemPrompt:.
Agent Methods
All methods require await (Agent is an actor):
try await agent.send("Hello")
try await agent.run("Hello")
await agent.stream("Hello")
await agent.reset()
Agent State
await agent.lastResponseId
await agent.lastUsage
Transcript
for entry in await agent.transcript {
switch entry {
case .userMessage(let msg): print("[User] \(msg)")
case .assistantMessage(let msg): print("[Assistant] \(msg)")
case .toolCall(let name, let args): print("[Tool] \(name)(\(args))")
case .toolResult(let name, _, let dur): print("[Result] \(name) in \(dur)")
case .reasoning(let item): print("[Reasoning] \(item.summaryText ?? "")")
case .error(let msg): print("[Error] \(msg)")
}
}
Error Handling
do {
let reply = try await agent.run("Hello")
} catch LLMError.rateLimit {
} catch LLMError.serverError(let code, let message) {
print("HTTP \(code): \(message ?? "")")
} catch LLMError.networkError(let description) {
print(description)
} catch LLMError.maxIterationsExceeded(let max) {
print("Loop exceeded \(max) iterations")
} catch LLMError.unknownTool(let name) {
print("Model called unregistered tool: \(name)")
} catch LLMError.toolExecutionFailed(let name, let message) {
print("Tool \(name) failed: \(message)")
}
Common Pitfalls
instructions not systemPrompt — Agent takes instructions: in the explicit init, not systemPrompt:. System(...) in @SessionBuilder maps to instructions.
input: not messages: — ToolSession.run(model:input:) takes input: [InputItem], not messages:. Use User("...") and System("...") as InputItem values.
- Continuity model —
Agent uses previous_response_id for turn continuity. ToolSession accumulates full conversation history in the input array (does NOT use previous_response_id).
strict on AgentTool — AgentTool(instance, strict: true) enables strict mode for JSON Schema validation. Default is nil (server decides).
agent.stream() not agent.streamSend() — Streaming on Agent uses stream, not streamSend. (streamSend is SwiftChatCompletionsDSL's name.)
TranscriptEntry.reasoning — This DSL's transcript includes .reasoning(ReasoningItem) for reasoning model output. SwiftChatCompletionsDSL does not have this case.
- ToolSessionEvent differences — Events here (
.iterationStarted, .llm, .toolCallStarted, .toolCallCompleted, .usageUpdate) differ from SwiftChatCompletionsDSL's events.
- Config params that throw —
Temperature, TopP, FrequencyPenalty, PresencePenalty, MaxOutputTokens, Instructions, PreviousResponseId, IncludeParam, RequestTimeout, ResourceTimeout, MaxToolCalls all require try. Others (Reasoning, TruncationParam, Store, etc.) do not.
Out of Scope
This skill covers SwiftOpenResponsesDSL wiring only. For designing @LLMTool structs, consult the using-swift-llm-tool-macros and design-llm-tool skills. For the Chat Completions API, consult the using-swift-chat-completions-dsl skill.