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
51 changes: 0 additions & 51 deletions Package.resolved

This file was deleted.

140 changes: 95 additions & 45 deletions Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,13 @@ public struct OpenAILanguageModel: LanguageModel {
apiKey tokenProvider: @escaping @autoclosure @Sendable () -> String,
model: String,
apiVariant: APIVariant = .chatCompletions,
session: URLSession = URLSession(configuration: .default)
session: URLSession = {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 300 // 5 minutes
config.timeoutIntervalForResource = 300 // 5 minutes
return URLSession(configuration: config)
}()

) {
var baseURL = baseURL
if !baseURL.path.hasSuffix("/") {
Expand All @@ -433,13 +439,13 @@ public struct OpenAILanguageModel: LanguageModel {
guard type == String.self else {
fatalError("OpenAILanguageModel only supports generating String content")
}

// Build messages: instructions + conversation history + current prompt
var messages: [OpenAIMessage] = []
if let systemSegments = extractInstructionSegments(from: session) {
messages.append(
OpenAIMessage(role: .system, content: .blocks(convertSegmentsToOpenAIBlocks(systemSegments)))
)
}

// Add conversation history (previous turns, excluding current prompt)
messages.append(contentsOf: extractConversationHistory(from: session))

// Add current prompt (always from fallbackText - reliable source)
let userSegments = extractPromptSegments(from: session, fallbackText: prompt.description)
messages.append(OpenAIMessage(role: .user, content: .blocks(convertSegmentsToOpenAIBlocks(userSegments))))

Expand Down Expand Up @@ -619,15 +625,6 @@ public struct OpenAILanguageModel: LanguageModel {
fatalError("OpenAILanguageModel only supports generating String content")
}

var messages: [OpenAIMessage] = []
if let systemSegments = extractInstructionSegments(from: session) {
messages.append(
OpenAIMessage(role: .system, content: .blocks(convertSegmentsToOpenAIBlocks(systemSegments)))
)
}
let userSegments = extractPromptSegments(from: session, fallbackText: prompt.description)
messages.append(OpenAIMessage(role: .user, content: .blocks(convertSegmentsToOpenAIBlocks(userSegments))))

// Convert tools if any are available in the session
let openAITools: [OpenAITool]? = {
guard !session.tools.isEmpty else { return nil }
Expand All @@ -641,20 +638,32 @@ public struct OpenAILanguageModel: LanguageModel {

switch apiVariant {
case .responses:
let params = Responses.createRequestBody(
model: model,
messages: messages,
tools: openAITools,
options: options,
stream: true
)

let url = baseURL.appendingPathComponent("responses")

let stream: AsyncThrowingStream<LanguageModelSession.ResponseStream<Content>.Snapshot, any Error> = .init {
continuation in
let task = Task { @Sendable in
do {
// Build messages: instructions + conversation history + current prompt
var messages: [OpenAIMessage] = []

// Add conversation history (instructions,previous turns, excluding current prompt)
messages.append(contentsOf: extractConversationHistory(from: session))

// Add current prompt (always from fallbackText - reliable source)
let userSegments = extractPromptSegments(from: session, fallbackText: prompt.description)
messages.append(
OpenAIMessage(role: .user, content: .blocks(convertSegmentsToOpenAIBlocks(userSegments)))
)

let params = Responses.createRequestBody(
model: model,
messages: messages,
tools: openAITools,
options: options,
stream: true
)
let body = try JSONEncoder().encode(params)

let events: AsyncThrowingStream<OpenAIResponsesServerEvent, any Error> =
Expand Down Expand Up @@ -704,20 +713,33 @@ public struct OpenAILanguageModel: LanguageModel {
return LanguageModelSession.ResponseStream(stream: stream)

case .chatCompletions:
let params = ChatCompletions.createRequestBody(
model: model,
messages: messages,
tools: openAITools,
options: options,
stream: true
)

let url = baseURL.appendingPathComponent("chat/completions")

let stream: AsyncThrowingStream<LanguageModelSession.ResponseStream<Content>.Snapshot, any Error> = .init {
continuation in
let task = Task { @Sendable in
do {
// Build messages: instructions + conversation history + current prompt
var messages: [OpenAIMessage] = []

// Add conversation history (instructions, previous turns, excluding current prompt)
messages.append(contentsOf: extractConversationHistory(from: session))

// Add current prompt (always from fallbackText - reliable source)
let userSegments = extractPromptSegments(from: session, fallbackText: prompt.description)
messages.append(
OpenAIMessage(role: .user, content: .blocks(convertSegmentsToOpenAIBlocks(userSegments)))
)

let params = ChatCompletions.createRequestBody(
model: model,
messages: messages,
tools: openAITools,
options: options,
stream: true
)

let body = try JSONEncoder().encode(params)

let events: AsyncThrowingStream<OpenAIChatCompletionsChunk, any Error> =
Expand Down Expand Up @@ -1254,27 +1276,55 @@ private func convertSegmentsToOpenAIBlocks(_ segments: [Transcript.Segment]) ->
return blocks
}

private func extractPromptSegments(from session: LanguageModelSession, fallbackText: String) -> [Transcript.Segment] {
// Prefer the most recent Transcript.Prompt entry if present
for entry in session.transcript.reversed() {
if case .prompt(let p) = entry {
return p.segments
private func extractConversationHistory(from session: LanguageModelSession) -> [OpenAIMessage] {
var messages: [OpenAIMessage] = []

// Iterate through transcript to build history
// Skip the last prompt (current turn) - it will be added separately via fallbackText
var entries = Array(session.transcript)

// Find and remove the last prompt entry (current turn)
if let lastPromptIndex = entries.lastIndex(where: {
if case .prompt = $0 { return true }
return false
}) {
entries.remove(at: lastPromptIndex)
}

for entry in entries {
switch entry {
case .instructions(let instr):
let blocks = convertSegmentsToOpenAIBlocks(instr.segments)
messages.append(OpenAIMessage(role: .system, content: .blocks(blocks)))
case .prompt(let prompt):
let blocks = convertSegmentsToOpenAIBlocks(prompt.segments)
messages.append(OpenAIMessage(role: .user, content: .blocks(blocks)))
case .response(let response):
let blocks = convertSegmentsToOpenAIBlocks(response.segments)
messages.append(OpenAIMessage(role: .assistant, content: .blocks(blocks)))
case .toolCalls:
break
case .toolOutput(let toolOutput):
let blocks = convertSegmentsToOpenAIBlocks(toolOutput.segments)
messages.append(OpenAIMessage(role: .tool(id: toolOutput.id), content: .blocks(blocks)))
}
}
return [.text(.init(content: fallbackText))]

return messages
}

private func extractInstructionSegments(from session: LanguageModelSession) -> [Transcript.Segment]? {
// Prefer the first Transcript.Instructions entry if present
for entry in session.transcript {
if case .instructions(let i) = entry {
return i.segments
private func extractPromptSegments(from session: LanguageModelSession, fallbackText: String) -> [Transcript.Segment] {
// Try to get segments from the last prompt in transcript (includes images)
if let lastPromptEntry = session.transcript.reversed().first(where: {
if case .prompt = $0 { return true }
return false
}) {
if case .prompt(let prompt) = lastPromptEntry {
return prompt.segments
}
}
if let instructions = session.instructions?.description, !instructions.isEmpty {
return [.text(.init(content: instructions))]
}
return nil
// Fallback to text-only if no prompt in transcript
return [.text(.init(content: fallbackText))]
}

private struct OpenAITool: Hashable, Codable, Sendable {
Expand Down