diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index 9916dd1..0000000 --- a/Package.resolved +++ /dev/null @@ -1,51 +0,0 @@ -{ - "originHash" : "268e14d0644cbc22fdd80ecba0d8aef0cf7a4fcaa635e7ea46197cf462ba5b6b", - "pins" : [ - { - "identity" : "eventsource", - "kind" : "remoteSourceControl", - "location" : "https://github.com/mattt/EventSource.git", - "state" : { - "revision" : "ca2a9d90cbe49e09b92f4b6ebd922c03ebea51d0", - "version" : "1.3.0" - } - }, - { - "identity" : "jsonschema", - "kind" : "remoteSourceControl", - "location" : "https://github.com/mattt/JSONSchema.git", - "state" : { - "revision" : "4c6f2467d5bc72b062c7a9b7e4cd77a09635ea8b", - "version" : "1.3.1" - } - }, - { - "identity" : "partialjsondecoder", - "kind" : "remoteSourceControl", - "location" : "https://github.com/mattt/PartialJSONDecoder.git", - "state" : { - "revision" : "e4d389e6bcc6771bb988d1a8a17695d8bfa97172", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections.git", - "state" : { - "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", - "version" : "1.3.0" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-syntax.git", - "state" : { - "revision" : "0687f71944021d616d34d922343dcef086855920", - "version" : "600.0.1" - } - } - ], - "version" : 3 -} diff --git a/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift b/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift index 04d6700..38d6025 100644 --- a/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift +++ b/Sources/AnyLanguageModel/Models/OpenAILanguageModel.swift @@ -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("/") { @@ -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)))) @@ -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 } @@ -641,13 +638,6 @@ 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") @@ -655,6 +645,25 @@ public struct OpenAILanguageModel: LanguageModel { 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 = @@ -704,13 +713,6 @@ 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") @@ -718,6 +720,26 @@ public struct OpenAILanguageModel: LanguageModel { 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 = @@ -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 {