From 7af8cc6b4a10e856b12c6b028ecc45cae0f509cb Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 20 Jan 2026 15:17:38 +0800 Subject: [PATCH 1/4] feat: add support for DeepSeek models Add new .deepseek model type with specific context length (32K), max output tokens (8K), capabilities (text generation, function calling, streaming, tool access), and parameter constraints. Update request/response handling to use GPT builders for API compatibility, and fallback to JSON mode for schema conversion. Includes integration tests for DeepSeek model functionality. Signed-off-by: alex --- .../Models/OpenAIModel.swift | 25 ++++++++++++++++++- .../OpenAILanguageModel.swift | 13 ++++++++-- .../IntegrationTests.swift | 7 +++++- .../RequestBuilderTests.swift | 11 +++++--- .../ResponseHandlerTests.swift | 11 +++++--- 5 files changed, 57 insertions(+), 10 deletions(-) diff --git a/Sources/OpenFoundationModelsOpenAI/Models/OpenAIModel.swift b/Sources/OpenFoundationModelsOpenAI/Models/OpenAIModel.swift index 3137228..9624de0 100644 --- a/Sources/OpenFoundationModelsOpenAI/Models/OpenAIModel.swift +++ b/Sources/OpenFoundationModelsOpenAI/Models/OpenAIModel.swift @@ -65,6 +65,8 @@ public struct OpenAIModel: Sendable, Hashable, ExpressibleByStringLiteral { return 128_000 case .reasoning: return 200_000 + case .deepseek: + return 32_768 // DeepSeek models typically have 32K context } } @@ -80,6 +82,8 @@ public struct OpenAIModel: Sendable, Hashable, ExpressibleByStringLiteral { return 16_384 case .reasoning: return 100_000 + case .deepseek: + return 8_192 // DeepSeek models typically have 8K max output } } @@ -90,6 +94,8 @@ public struct OpenAIModel: Sendable, Hashable, ExpressibleByStringLiteral { return [.textGeneration, .vision, .functionCalling, .streaming, .toolAccess] case .reasoning: return [.textGeneration, .reasoning, .functionCalling, .streaming, .toolAccess] + case .deepseek: + return [.textGeneration, .functionCalling, .streaming, .toolAccess] // No vision or reasoning } } @@ -118,6 +124,17 @@ public struct OpenAIModel: Sendable, Hashable, ExpressibleByStringLiteral { temperatureRange: nil, topPRange: nil ) + case .deepseek: + return ParameterConstraints( + supportsTemperature: true, + supportsTopP: true, + supportsFrequencyPenalty: true, + supportsPresencePenalty: true, + supportsStop: true, + maxTokensParameterName: "max_tokens", + temperatureRange: 0.0...2.0, + topPRange: 0.0...1.0 + ) } } @@ -154,6 +171,11 @@ public struct OpenAIModel: Sendable, Hashable, ExpressibleByStringLiteral { return .reasoning } + // DeepSeek models + if lowercased.contains("deepseek") { + return .deepseek + } + // Default to GPT for all other models return .gpt } @@ -165,6 +187,7 @@ public struct OpenAIModel: Sendable, Hashable, ExpressibleByStringLiteral { public enum ModelType: String, Sendable, Hashable { case gpt case reasoning + case deepseek } /// Model capabilities using OptionSet @@ -255,4 +278,4 @@ extension OpenAIModel { public static func == (lhs: OpenAIModel, rhs: OpenAIModel) -> Bool { return lhs.id == rhs.id } -} +} \ No newline at end of file diff --git a/Sources/OpenFoundationModelsOpenAI/OpenAILanguageModel.swift b/Sources/OpenFoundationModelsOpenAI/OpenAILanguageModel.swift index 9500f0a..fbb46fa 100644 --- a/Sources/OpenFoundationModelsOpenAI/OpenAILanguageModel.swift +++ b/Sources/OpenFoundationModelsOpenAI/OpenAILanguageModel.swift @@ -34,6 +34,10 @@ public final class OpenAILanguageModel: LanguageModel, @unchecked Sendable { case .reasoning: self.requestBuilder = ReasoningRequestBuilder() self.responseHandler = ReasoningResponseHandler() + case .deepseek: + // DeepSeek uses similar API to GPT models + self.requestBuilder = GPTRequestBuilder() + self.responseHandler = GPTResponseHandler() } self.rateLimiter = RateLimiter(configuration: configuration.rateLimits) } @@ -342,11 +346,16 @@ public final class OpenAILanguageModel: LanguageModel, @unchecked Sendable { /// Convert GenerationSchema to ResponseFormat private func convertSchemaToResponseFormat(_ schema: GenerationSchema) -> ResponseFormat { + // For models that don't support json_schema (like DeepSeek), fallback to json mode + if model.modelType == .deepseek { + return .json + } + // Encode GenerationSchema to get JSON Schema do { let encoder = JSONEncoder() let schemaData = try encoder.encode(schema) - + // Convert to JSON dictionary if let schemaJson = try JSONSerialization.jsonObject(with: schemaData) as? [String: Any] { // Transform to OpenAI's expected JSON Schema format @@ -356,7 +365,7 @@ public final class OpenAILanguageModel: LanguageModel, @unchecked Sendable { } catch { print("Warning: Failed to convert GenerationSchema to ResponseFormat: \(error)") } - + // Fallback to JSON mode return .json } diff --git a/Tests/OpenFoundationModelsOpenAITests/IntegrationTests.swift b/Tests/OpenFoundationModelsOpenAITests/IntegrationTests.swift index 2e69b9c..504a4f6 100644 --- a/Tests/OpenFoundationModelsOpenAITests/IntegrationTests.swift +++ b/Tests/OpenFoundationModelsOpenAITests/IntegrationTests.swift @@ -402,6 +402,11 @@ struct IntegrationTests { let builder = ReasoningRequestBuilder() #expect(type(of: builder) == ReasoningRequestBuilder.self, "Model \(model.apiName) should use ReasoningRequestBuilder") + case .deepseek: + // DeepSeek models should use GPTRequestBuilder + let builder = GPTRequestBuilder() + #expect(type(of: builder) == GPTRequestBuilder.self, + "Model \(model.apiName) should use GPTRequestBuilder") } } } @@ -534,4 +539,4 @@ struct IntegrationTests { #expect(tools?.count == 1) #expect(tools?.first?.function.name == "get_stock_price") } -} +} \ No newline at end of file diff --git a/Tests/OpenFoundationModelsOpenAITests/RequestBuilderTests.swift b/Tests/OpenFoundationModelsOpenAITests/RequestBuilderTests.swift index ab825ed..6f29f3b 100644 --- a/Tests/OpenFoundationModelsOpenAITests/RequestBuilderTests.swift +++ b/Tests/OpenFoundationModelsOpenAITests/RequestBuilderTests.swift @@ -243,15 +243,20 @@ struct RequestBuilderTests { builder = GPTRequestBuilder() case .reasoning: builder = ReasoningRequestBuilder() + case .deepseek: + builder = GPTRequestBuilder() // DeepSeek uses GPT builder } - + switch model.modelType { case .gpt: - #expect(type(of: builder) == GPTRequestBuilder.self, + #expect(type(of: builder) == GPTRequestBuilder.self, "Should create GPTRequestBuilder for GPT model \(model.apiName)") case .reasoning: - #expect(type(of: builder) == ReasoningRequestBuilder.self, + #expect(type(of: builder) == ReasoningRequestBuilder.self, "Should create ReasoningRequestBuilder for reasoning model \(model.apiName)") + case .deepseek: + #expect(type(of: builder) == GPTRequestBuilder.self, + "Should create GPTRequestBuilder for DeepSeek model \(model.apiName)") } } diff --git a/Tests/OpenFoundationModelsOpenAITests/ResponseHandlerTests.swift b/Tests/OpenFoundationModelsOpenAITests/ResponseHandlerTests.swift index 02a2695..7c47a70 100644 --- a/Tests/OpenFoundationModelsOpenAITests/ResponseHandlerTests.swift +++ b/Tests/OpenFoundationModelsOpenAITests/ResponseHandlerTests.swift @@ -220,15 +220,20 @@ struct ResponseHandlerTests { handler = GPTResponseHandler() case .reasoning: handler = ReasoningResponseHandler() + case .deepseek: + handler = GPTResponseHandler() // DeepSeek uses GPT handler } - + switch model.modelType { case .gpt: - #expect(type(of: handler) == GPTResponseHandler.self, + #expect(type(of: handler) == GPTResponseHandler.self, "Should create GPTResponseHandler for GPT model \(model.apiName)") case .reasoning: - #expect(type(of: handler) == ReasoningResponseHandler.self, + #expect(type(of: handler) == ReasoningResponseHandler.self, "Should create ReasoningResponseHandler for reasoning model \(model.apiName)") + case .deepseek: + #expect(type(of: handler) == GPTResponseHandler.self, + "Should create GPTResponseHandler for DeepSeek model \(model.apiName)") } } From cde649881c2a85b1bb3d1b711d48befde5a4a704 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 20 Jan 2026 15:45:25 +0800 Subject: [PATCH 2/4] feat: add model parameter to response format extraction for DeepSeek compatibility - Updated `extractResponseFormatWithSchema` and related methods in `TranscriptConverter` to accept an `OpenAIModel` parameter - Added logic to fallback to `.json` mode for DeepSeek models that do not support `json_schema` - Modified `OpenAILanguageModel` to pass the model when extracting response formats - Ensures compatibility with various OpenAI-compatible models by handling schema support differences Signed-off-by: alex --- .../Internal/TranscriptConverter.swift | 34 ++- .../OpenAILanguageModel.swift | 4 +- .../DeepSeekCompatibilityTests.swift | 257 ++++++++++++++++++ 3 files changed, 282 insertions(+), 13 deletions(-) create mode 100644 Tests/OpenFoundationModelsOpenAITests/DeepSeekCompatibilityTests.swift diff --git a/Sources/OpenFoundationModelsOpenAI/Internal/TranscriptConverter.swift b/Sources/OpenFoundationModelsOpenAI/Internal/TranscriptConverter.swift index dc7c6ec..50bb343 100644 --- a/Sources/OpenFoundationModelsOpenAI/Internal/TranscriptConverter.swift +++ b/Sources/OpenFoundationModelsOpenAI/Internal/TranscriptConverter.swift @@ -435,46 +435,58 @@ internal struct TranscriptConverter { } /// Extract response format with full JSON Schema from the most recent prompt - static func extractResponseFormatWithSchema(from transcript: Transcript) -> ResponseFormat? { + static func extractResponseFormatWithSchema(from transcript: Transcript, for model: OpenAIModel) -> ResponseFormat? { // Try JSON-based extraction to get complete schema - return extractResponseFormatFromJSON(transcript) + return extractResponseFormatFromJSON(transcript, for: model) } /// Extract response format by encoding Transcript to JSON private static func extractResponseFormatFromJSON(_ transcript: Transcript) -> ResponseFormat? { + // For backward compatibility, default to GPT model behavior + // This method is used by extractResponseFormat which doesn't have model context + return extractResponseFormatFromJSON(transcript, for: OpenAIModel("gpt-4o")) + } + + /// Extract response format by encoding Transcript to JSON + private static func extractResponseFormatFromJSON(_ transcript: Transcript, for model: OpenAIModel) -> ResponseFormat? { do { // Encode transcript to JSON let encoder = JSONEncoder() let data = try encoder.encode(transcript) - + guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let entries = json["entries"] as? [[String: Any]] else { return nil } - + // Look for the most recent prompt with responseFormat for entry in entries.reversed() { if entry["type"] as? String == "prompt", let responseFormat = entry["responseFormat"] as? [String: Any] { - + #if DEBUG print("Found response format in JSON: \(responseFormat)") #endif - + // Check if there's a schema (now available with updated OpenFoundationModels) if let schema = responseFormat["schema"] as? [String: Any] { - // Transform schema to OpenAI's expected format - let transformedSchema = transformToOpenAIJSONSchema(schema) - return .jsonSchema(transformedSchema) + // For models that don't support json_schema (like DeepSeek), fallback to json mode + if model.modelType == .deepseek { + return .json + } else { + // Transform schema to OpenAI's expected format + let transformedSchema = transformToOpenAIJSONSchema(schema) + return .jsonSchema(transformedSchema) + } } - + // If there's a name or type field, we know JSON is expected if responseFormat["name"] != nil || responseFormat["type"] != nil { return .json } } } - + return nil } catch { #if DEBUG diff --git a/Sources/OpenFoundationModelsOpenAI/OpenAILanguageModel.swift b/Sources/OpenFoundationModelsOpenAI/OpenAILanguageModel.swift index fbb46fa..b7878e6 100644 --- a/Sources/OpenFoundationModelsOpenAI/OpenAILanguageModel.swift +++ b/Sources/OpenFoundationModelsOpenAI/OpenAILanguageModel.swift @@ -56,7 +56,7 @@ public final class OpenAILanguageModel: LanguageModel, @unchecked Sendable { // Use TranscriptConverter for all conversions let messages = TranscriptConverter.buildMessages(from: transcript) let tools = TranscriptConverter.extractTools(from: transcript) - let responseFormat = TranscriptConverter.extractResponseFormatWithSchema(from: transcript) + let responseFormat = TranscriptConverter.extractResponseFormatWithSchema(from: transcript, for: model) let finalOptions = options ?? TranscriptConverter.extractOptions(from: transcript) // Build request with response format if present @@ -101,7 +101,7 @@ public final class OpenAILanguageModel: LanguageModel, @unchecked Sendable { // Use TranscriptConverter for all conversions let messages = TranscriptConverter.buildMessages(from: transcript) let tools = TranscriptConverter.extractTools(from: transcript) - let responseFormat = TranscriptConverter.extractResponseFormatWithSchema(from: transcript) + let responseFormat = TranscriptConverter.extractResponseFormatWithSchema(from: transcript, for: model) let finalOptions = options ?? TranscriptConverter.extractOptions(from: transcript) let request = try buildChatRequestWithFormat( diff --git a/Tests/OpenFoundationModelsOpenAITests/DeepSeekCompatibilityTests.swift b/Tests/OpenFoundationModelsOpenAITests/DeepSeekCompatibilityTests.swift new file mode 100644 index 0000000..8002e85 --- /dev/null +++ b/Tests/OpenFoundationModelsOpenAITests/DeepSeekCompatibilityTests.swift @@ -0,0 +1,257 @@ +import Testing +import Foundation +@testable import OpenFoundationModelsOpenAI +import OpenFoundationModels + +/// Tests specifically for DeepSeek API compatibility +@Suite("DeepSeek Compatibility Tests") +struct DeepSeekCompatibilityTests { + + // MARK: - Model Identification Tests + + @Test("DeepSeek model identification works correctly") + func testDeepSeekModelIdentification() { + // Test various DeepSeek model names + let deepSeekModels = ["deepseek-chat", "deepseek-coder", "deepseek-v3", "deepseek-r1"] + + for modelName in deepSeekModels { + let model = OpenAIModel(modelName) + #expect(model.modelType == .deepseek, "Model \(modelName) should be identified as DeepSeek") + #expect(model.apiName == modelName, "API name should match model name") + } + + // Test that non-DeepSeek models are not identified as DeepSeek + let nonDeepSeekModels = ["gpt-4o", "gpt-4-turbo", "o1", "claude-3"] + + for modelName in nonDeepSeekModels { + let model = OpenAIModel(modelName) + #expect(model.modelType != .deepseek, "Model \(modelName) should not be identified as DeepSeek") + } + } + + @Test("DeepSeek models have correct capabilities") + func testDeepSeekModelCapabilities() { + let deepSeekModel = OpenAIModel("deepseek-chat") + + // DeepSeek should support text generation, function calling, and streaming + #expect(deepSeekModel.supportsVision == false, "DeepSeek should not support vision") + #expect(deepSeekModel.supportsFunctionCalling == true, "DeepSeek should support function calling") + #expect(deepSeekModel.supportsStreaming == true, "DeepSeek should support streaming") + #expect(deepSeekModel.isReasoningModel == false, "DeepSeek should not be a reasoning model") + + // Check capabilities set + #expect(deepSeekModel.capabilities.contains(.textGeneration), "DeepSeek should support text generation") + #expect(deepSeekModel.capabilities.contains(.functionCalling), "DeepSeek should support function calling") + #expect(deepSeekModel.capabilities.contains(.streaming), "DeepSeek should support streaming") + #expect(deepSeekModel.capabilities.contains(.toolAccess), "DeepSeek should support tool access") + #expect(!deepSeekModel.capabilities.contains(.vision), "DeepSeek should not support vision") + #expect(!deepSeekModel.capabilities.contains(.reasoning), "DeepSeek should not support reasoning") + } + + @Test("DeepSeek models have correct context window and tokens") + func testDeepSeekModelLimits() { + let deepSeekModel = OpenAIModel("deepseek-chat") + + #expect(deepSeekModel.contextWindow == 32768, "DeepSeek should have 32K context window") + #expect(deepSeekModel.maxOutputTokens == 8192, "DeepSeek should have 8K max output tokens") + } + + // MARK: - Response Format Compatibility Tests + + @Test("DeepSeek models use JSON response format instead of JSON Schema") + func testDeepSeekResponseFormatCompatibility() throws { + // Create a simple schema for testing + let schema = GenerationSchema( + type: String.self, + description: "Test response", + properties: [] + ) + + // Test with GPT model (should use json_schema) + let gptModel = OpenAIModel("gpt-4o") + let gptResponseFormat = convertSchemaToResponseFormat(schema, for: gptModel) + + // Test with DeepSeek model (should use json) + let deepSeekModel = OpenAIModel("deepseek-chat") + let deepSeekResponseFormat = convertSchemaToResponseFormat(schema, for: deepSeekModel) + + // GPT should use jsonSchema format + switch gptResponseFormat { + case .jsonSchema: + // Expected for GPT models + break + default: + #expect(Bool(false), "GPT model should use jsonSchema format") + } + + // DeepSeek should use json format + switch deepSeekResponseFormat { + case .json: + // Expected for DeepSeek models + break + case .jsonSchema: + #expect(Bool(false), "DeepSeek model should not use jsonSchema format") + case .text: + #expect(Bool(false), "DeepSeek model should not use text format") + } + } + + @Test("TranscriptConverter respects model type for response format extraction") + func testTranscriptConverterDeepSeekResponseFormat() throws { + // Create a transcript with a prompt that has a response format + let schema = GenerationSchema( + type: String.self, + description: "Response schema", + properties: [] + ) + + let responseFormat = Transcript.ResponseFormat(schema: schema) + + let transcript = Transcript( + entries: [ + .prompt( + Transcript.Prompt( + segments: [.text(Transcript.TextSegment(content: "Generate a response"))], + responseFormat: responseFormat + ) + ) + ] + ) + + // Test with GPT model + let gptModel = OpenAIModel("gpt-4o") + let gptExtractedFormat = TranscriptConverter.extractResponseFormatWithSchema(from: transcript, for: gptModel) + + // Test with DeepSeek model + let deepSeekModel = OpenAIModel("deepseek-chat") + let deepSeekExtractedFormat = TranscriptConverter.extractResponseFormatWithSchema(from: transcript, for: deepSeekModel) + + // GPT should extract jsonSchema format + switch gptExtractedFormat { + case .jsonSchema: + // Expected + break + default: + #expect(Bool(false), "GPT model should extract jsonSchema format") + } + + // DeepSeek should extract json format + switch deepSeekExtractedFormat { + case .json: + // Expected + break + case .jsonSchema: + #expect(Bool(false), "DeepSeek model should extract json format, not jsonSchema") + default: + #expect(Bool(false), "DeepSeek model should extract json format") + } + } + + // MARK: - Request Builder Compatibility Tests + + @Test("DeepSeek models use correct request builder") + func testDeepSeekRequestBuilder() { + let deepSeekModel = OpenAIModel("deepseek-chat") + + // DeepSeek should use GPTRequestBuilder, not ReasoningRequestBuilder + switch deepSeekModel.modelType { + case .deepseek: + // This should work without throwing + let builder = GPTRequestBuilder() + #expect(type(of: builder) == GPTRequestBuilder.self) + default: + #expect(Bool(false), "DeepSeek model should have deepseek type") + } + } + + @Test("DeepSeek models use correct response handler") + func testDeepSeekResponseHandler() { + let deepSeekModel = OpenAIModel("deepseek-chat") + + // DeepSeek should use GPTResponseHandler, not ReasoningResponseHandler + switch deepSeekModel.modelType { + case .deepseek: + // This should work without throwing + let handler = GPTResponseHandler() + #expect(type(of: handler) == GPTResponseHandler.self) + default: + #expect(Bool(false), "DeepSeek model should have deepseek type") + } + } +} + +// MARK: - Helper Functions for Testing + +/// Convert GenerationSchema to ResponseFormat for a specific model (test helper) +private func convertSchemaToResponseFormat(_ schema: GenerationSchema, for model: OpenAIModel) -> ResponseFormat { + // This replicates the logic from OpenAILanguageModel.convertSchemaToResponseFormat + // For models that don't support json_schema (like DeepSeek), fallback to json mode + if model.modelType == .deepseek { + return .json + } + + // For other models, try to create jsonSchema format + do { + let encoder = JSONEncoder() + let schemaData = try encoder.encode(schema) + + // Convert to JSON dictionary + if let schemaJson = try JSONSerialization.jsonObject(with: schemaData) as? [String: Any] { + // Transform to OpenAI's expected JSON Schema format + let transformedSchema = transformToOpenAIJSONSchema(schemaJson) + return .jsonSchema(transformedSchema) + } + } catch { + // Ignore encoding errors in test + } + + // Fallback to JSON mode + return .json +} + +/// Transform GenerationSchema JSON to OpenAI's JSON Schema format (test helper) +private func transformToOpenAIJSONSchema(_ json: [String: Any]) -> [String: Any] { + var schema: [String: Any] = [:] + + // Extract type (default to "object") + schema["type"] = json["type"] as? String ?? "object" + + // Extract and transform properties + if let properties = json["properties"] as? [String: [String: Any]] { + var transformedProperties: [String: [String: Any]] = [:] + + for (key, propJson) in properties { + var prop: [String: Any] = [:] + prop["type"] = propJson["type"] as? String ?? "string" + + if let description = propJson["description"] as? String { + prop["description"] = description + } + + if let enumValues = propJson["enum"] as? [String] { + prop["enum"] = enumValues + } + + if prop["type"] as? String == "array", + let items = propJson["items"] as? [String: Any] { + prop["items"] = items + } + + transformedProperties[key] = prop + } + + schema["properties"] = transformedProperties + } + + // Extract required fields + if let required = json["required"] as? [String] { + schema["required"] = required + } + + // Add description if present + if let description = json["description"] as? String { + schema["description"] = description + } + + return schema +} \ No newline at end of file From 9578d29eaa5dc9c8c83ca138654c725988ff3942 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 20 Jan 2026 17:34:58 +0800 Subject: [PATCH 3/4] add Signed-off-by: alex --- .../API/OpenAIAPITypes.swift | 187 ++++++++++-------- 1 file changed, 101 insertions(+), 86 deletions(-) diff --git a/Sources/OpenFoundationModelsOpenAI/API/OpenAIAPITypes.swift b/Sources/OpenFoundationModelsOpenAI/API/OpenAIAPITypes.swift index 9b9df02..4b10d4e 100644 --- a/Sources/OpenFoundationModelsOpenAI/API/OpenAIAPITypes.swift +++ b/Sources/OpenFoundationModelsOpenAI/API/OpenAIAPITypes.swift @@ -3,16 +3,16 @@ import Foundation // MARK: - Helper Types public final class Box: Codable, Sendable where T: Codable & Sendable { public let value: T - + public init(_ value: T) { self.value = value } - + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() self.value = try container.decode(T.self) } - + public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(value) @@ -26,12 +26,13 @@ public enum ResponseFormat: Codable, @unchecked Sendable { case text case json case jsonSchema([String: Any]) - + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() - + if let type = try? container.decode([String: String].self), - let formatType = type["type"] { + let formatType = type["type"] + { switch formatType { case "text": self = .text @@ -46,10 +47,10 @@ public enum ResponseFormat: Codable, @unchecked Sendable { self = .text } } - + public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - + switch self { case .text: try container.encode(["type": "text"]) @@ -62,8 +63,8 @@ public enum ResponseFormat: Codable, @unchecked Sendable { "json_schema": [ "name": "response", "strict": true, - "schema": schema - ] + "schema": schema, + ], ] // Convert to AnyCodable for encoding try container.encode(AnyCodable(responseFormat)) @@ -76,14 +77,14 @@ public enum ResponseFormat: Codable, @unchecked Sendable { /// Helper type for encoding/decoding Any values private struct AnyCodable: Codable { let value: Any - + init(_ value: Any) { self.value = value } - + init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() - + if let bool = try? container.decode(Bool.self) { value = bool } else if let int = try? container.decode(Int.self) { @@ -100,10 +101,10 @@ private struct AnyCodable: Codable { value = NSNull() } } - + func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - + switch value { case let bool as Bool: try container.encode(bool) @@ -123,6 +124,7 @@ private struct AnyCodable: Codable { try container.encode(String(describing: value)) } } + } // MARK: - Chat Completion Request @@ -141,7 +143,7 @@ public struct ChatCompletionRequest: Codable, Sendable { public let toolChoice: ToolChoice? public let responseFormat: ResponseFormat? public let user: String? - + public init( model: String, messages: [ChatMessage], @@ -173,7 +175,7 @@ public struct ChatCompletionRequest: Codable, Sendable { self.responseFormat = responseFormat self.user = user } - + enum CodingKeys: String, CodingKey { case model, messages, temperature, stop, stream, tools, user case topP = "top_p" @@ -193,7 +195,7 @@ public struct ChatMessage: Codable, Sendable { public let name: String? public let toolCalls: [OpenAIToolCall]? public let toolCallId: String? - + public init( role: Role, content: Content? = nil, @@ -207,32 +209,33 @@ public struct ChatMessage: Codable, Sendable { self.toolCalls = toolCalls self.toolCallId = toolCallId } - + public enum Role: String, Codable, Sendable { case system, user, assistant, tool } - + public enum Content: Codable, Sendable { case text(String) case multimodal([ContentPart]) - + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() - + if let text = try? container.decode(String.self) { self = .text(text) } else if let parts = try? container.decode([ContentPart].self) { self = .multimodal(parts) } else { throw DecodingError.dataCorrupted( - DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid content format") + DecodingError.Context( + codingPath: decoder.codingPath, debugDescription: "Invalid content format") ) } } - + public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - + switch self { case .text(let text): try container.encode(text) @@ -240,7 +243,7 @@ public struct ChatMessage: Codable, Sendable { try container.encode(parts) } } - + public var text: String? { switch self { case .text(let text): @@ -255,7 +258,7 @@ public struct ChatMessage: Codable, Sendable { } } } - + enum CodingKeys: String, CodingKey { case role, content, name case toolCalls = "tool_calls" @@ -268,87 +271,88 @@ public enum ContentPart: Codable, Sendable { case text(TextPart) case image(ImagePart) case audio(AudioPart) - + public struct TextPart: Codable, Sendable { public let type: String public let text: String - + public init(text: String) { self.type = "text" self.text = text } - + private enum CodingKeys: String, CodingKey { case type, text } - + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.type = try container.decodeIfPresent(String.self, forKey: .type) ?? "text" self.text = try container.decode(String.self, forKey: .text) } } - + public struct ImagePart: Codable, Sendable { public let type = "image_url" public let imageUrl: ImageURL - + public init(imageUrl: ImageURL) { self.imageUrl = imageUrl } - + public struct ImageURL: Codable, Sendable { public let url: String public let detail: Detail? - + public init(url: String, detail: Detail? = nil) { self.url = url self.detail = detail } - + public enum Detail: String, Codable, Sendable { case auto, low, high } } - + enum CodingKeys: String, CodingKey { case type case imageUrl = "image_url" } } - + public struct AudioPart: Codable, Sendable { public let type = "input_audio" public let inputAudio: InputAudio - + public init(inputAudio: InputAudio) { self.inputAudio = inputAudio } - + public struct InputAudio: Codable, Sendable { - public let data: String // base64 encoded + public let data: String // base64 encoded public let format: Format - + public init(data: String, format: Format) { self.data = data self.format = format } - + public enum Format: String, Codable, Sendable { case wav, mp3, flac, m4a, ogg, oga, webm } } - + enum CodingKeys: String, CodingKey { case type case inputAudio = "input_audio" } } - + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: DynamicCodingKeys.self) - let type = try container.decode(String.self, forKey: DynamicCodingKeys(stringValue: "type")!) - + let type = try container.decode( + String.self, forKey: DynamicCodingKeys(stringValue: "type")!) + switch type { case "text": let textPart = try TextPart(from: decoder) @@ -361,11 +365,13 @@ public enum ContentPart: Codable, Sendable { self = .audio(audioPart) default: throw DecodingError.dataCorrupted( - DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unknown content part type: \(type)") + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown content part type: \(type)") ) } } - + public func encode(to encoder: Encoder) throws { switch self { case .text(let textPart): @@ -382,27 +388,27 @@ public enum ContentPart: Codable, Sendable { public struct Tool: Codable, Sendable { public let type: String public let function: Function - + public init(function: Function) { self.type = "function" self.function = function } - + private enum CodingKeys: String, CodingKey { case type, function } - + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.type = try container.decodeIfPresent(String.self, forKey: .type) ?? "function" self.function = try container.decode(Function.self, forKey: .function) } - + public struct Function: Codable, Sendable { public let name: String public let description: String? public let parameters: JSONSchema - + public init(name: String, description: String? = nil, parameters: JSONSchema) { self.name = name self.description = description @@ -416,10 +422,10 @@ public enum ToolChoice: Codable, Sendable { case auto case required case function(String) - + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() - + if let string = try? container.decode(String.self) { switch string { case "none": self = .none @@ -427,23 +433,27 @@ public enum ToolChoice: Codable, Sendable { case "required": self = .required default: throw DecodingError.dataCorrupted( - DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid tool choice: \(string)") + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Invalid tool choice: \(string)") ) } } else if let object = try? container.decode([String: [String: String]].self), - let function = object["function"], - let name = function["name"] { + let function = object["function"], + let name = function["name"] + { self = .function(name) } else { throw DecodingError.dataCorrupted( - DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid tool choice format") + DecodingError.Context( + codingPath: decoder.codingPath, debugDescription: "Invalid tool choice format") ) } } - + public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - + switch self { case .none: try container.encode("none") @@ -455,12 +465,12 @@ public enum ToolChoice: Codable, Sendable { struct FunctionChoice: Codable { let type: String let function: FunctionName - + init(function: FunctionName) { self.type = "function" self.function = function } - + struct FunctionName: Codable { let name: String } @@ -475,17 +485,17 @@ public struct OpenAIToolCall: Codable, Sendable { public let id: String public let type: String public let function: FunctionCall - + public init(id: String, type: String = "function", function: FunctionCall) { self.id = id self.type = type self.function = function } - + public struct FunctionCall: Codable, Sendable { public let name: String public let arguments: String - + public init(name: String, arguments: String) { self.name = name self.arguments = arguments @@ -499,7 +509,7 @@ public struct JSONSchema: Codable, Sendable { public let properties: [String: JSONSchemaProperty]? public let required: [String]? public let description: String? - + public init( type: String, properties: [String: JSONSchemaProperty]? = nil, @@ -520,7 +530,7 @@ public struct JSONSchemaProperty: Codable, Sendable { public let minimum: Double? public let maximum: Double? public let items: Box? - + public init( type: String, description: String? = nil, @@ -536,7 +546,7 @@ public struct JSONSchemaProperty: Codable, Sendable { self.maximum = maximum self.items = items.map(Box.init) } - + enum CodingKeys: String, CodingKey { case type, description, minimum, maximum, items case enumValues = "enum" @@ -552,24 +562,24 @@ public struct ChatCompletionResponse: Codable, Sendable { public let choices: [Choice] public let usage: Usage? public let systemFingerprint: String? - + public struct Choice: Codable, Sendable { public let index: Int public let message: ChatMessage public let finishReason: String? - + enum CodingKeys: String, CodingKey { case index, message case finishReason = "finish_reason" } } - + public struct Usage: Codable, Sendable { public let promptTokens: Int public let completionTokens: Int? public let totalTokens: Int - public let reasoningTokens: Int? // For reasoning models - + public let reasoningTokens: Int? // For reasoning models + enum CodingKeys: String, CodingKey { case promptTokens = "prompt_tokens" case completionTokens = "completion_tokens" @@ -577,7 +587,7 @@ public struct ChatCompletionResponse: Codable, Sendable { case reasoningTokens = "reasoning_tokens" } } - + enum CodingKeys: String, CodingKey { case id, object, created, model, choices, usage case systemFingerprint = "system_fingerprint" @@ -622,7 +632,8 @@ public struct StreamingToolCall: Codable, Sendable { public let type: String? public let function: StreamingFunctionCall - public init(index: Int, id: String? = nil, type: String? = nil, function: StreamingFunctionCall) { + public init(index: Int, id: String? = nil, type: String? = nil, function: StreamingFunctionCall) + { self.index = index self.id = id self.type = type @@ -644,12 +655,12 @@ public struct StreamingToolCall: Codable, Sendable { private struct DynamicCodingKeys: CodingKey { var stringValue: String var intValue: Int? - + init?(stringValue: String) { self.stringValue = stringValue self.intValue = nil } - + init?(intValue: Int) { self.stringValue = String(intValue) self.intValue = intValue @@ -661,24 +672,28 @@ extension ChatMessage { public static func system(_ content: String) -> ChatMessage { return ChatMessage(role: .system, content: .text(content)) } - + public static func user(_ content: String) -> ChatMessage { return ChatMessage(role: .user, content: .text(content)) } - + public static func assistant(_ content: String) -> ChatMessage { return ChatMessage(role: .assistant, content: .text(content)) } - + public static func tool(content: String, toolCallId: String) -> ChatMessage { return ChatMessage(role: .tool, content: .text(content), toolCallId: toolCallId) } - - public static func userWithImage(text: String, imageURL: String, detail: ContentPart.ImagePart.ImageURL.Detail = .auto) -> ChatMessage { + + public static func userWithImage( + text: String, imageURL: String, detail: ContentPart.ImagePart.ImageURL.Detail = .auto + ) -> ChatMessage { let parts: [ContentPart] = [ .text(ContentPart.TextPart(text: text)), - .image(ContentPart.ImagePart(imageUrl: ContentPart.ImagePart.ImageURL(url: imageURL, detail: detail))) + .image( + ContentPart.ImagePart( + imageUrl: ContentPart.ImagePart.ImageURL(url: imageURL, detail: detail))), ] return ChatMessage(role: .user, content: .multimodal(parts)) } -} \ No newline at end of file +} From a9f608096546bd74005c8f409b6fc190ded942cf Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 20 Jan 2026 17:42:36 +0800 Subject: [PATCH 4/4] Revert "add" This reverts commit 9578d29eaa5dc9c8c83ca138654c725988ff3942. --- .../API/OpenAIAPITypes.swift | 187 ++++++++---------- 1 file changed, 86 insertions(+), 101 deletions(-) diff --git a/Sources/OpenFoundationModelsOpenAI/API/OpenAIAPITypes.swift b/Sources/OpenFoundationModelsOpenAI/API/OpenAIAPITypes.swift index 4b10d4e..9b9df02 100644 --- a/Sources/OpenFoundationModelsOpenAI/API/OpenAIAPITypes.swift +++ b/Sources/OpenFoundationModelsOpenAI/API/OpenAIAPITypes.swift @@ -3,16 +3,16 @@ import Foundation // MARK: - Helper Types public final class Box: Codable, Sendable where T: Codable & Sendable { public let value: T - + public init(_ value: T) { self.value = value } - + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() self.value = try container.decode(T.self) } - + public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(value) @@ -26,13 +26,12 @@ public enum ResponseFormat: Codable, @unchecked Sendable { case text case json case jsonSchema([String: Any]) - + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() - + if let type = try? container.decode([String: String].self), - let formatType = type["type"] - { + let formatType = type["type"] { switch formatType { case "text": self = .text @@ -47,10 +46,10 @@ public enum ResponseFormat: Codable, @unchecked Sendable { self = .text } } - + public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - + switch self { case .text: try container.encode(["type": "text"]) @@ -63,8 +62,8 @@ public enum ResponseFormat: Codable, @unchecked Sendable { "json_schema": [ "name": "response", "strict": true, - "schema": schema, - ], + "schema": schema + ] ] // Convert to AnyCodable for encoding try container.encode(AnyCodable(responseFormat)) @@ -77,14 +76,14 @@ public enum ResponseFormat: Codable, @unchecked Sendable { /// Helper type for encoding/decoding Any values private struct AnyCodable: Codable { let value: Any - + init(_ value: Any) { self.value = value } - + init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() - + if let bool = try? container.decode(Bool.self) { value = bool } else if let int = try? container.decode(Int.self) { @@ -101,10 +100,10 @@ private struct AnyCodable: Codable { value = NSNull() } } - + func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - + switch value { case let bool as Bool: try container.encode(bool) @@ -124,7 +123,6 @@ private struct AnyCodable: Codable { try container.encode(String(describing: value)) } } - } // MARK: - Chat Completion Request @@ -143,7 +141,7 @@ public struct ChatCompletionRequest: Codable, Sendable { public let toolChoice: ToolChoice? public let responseFormat: ResponseFormat? public let user: String? - + public init( model: String, messages: [ChatMessage], @@ -175,7 +173,7 @@ public struct ChatCompletionRequest: Codable, Sendable { self.responseFormat = responseFormat self.user = user } - + enum CodingKeys: String, CodingKey { case model, messages, temperature, stop, stream, tools, user case topP = "top_p" @@ -195,7 +193,7 @@ public struct ChatMessage: Codable, Sendable { public let name: String? public let toolCalls: [OpenAIToolCall]? public let toolCallId: String? - + public init( role: Role, content: Content? = nil, @@ -209,33 +207,32 @@ public struct ChatMessage: Codable, Sendable { self.toolCalls = toolCalls self.toolCallId = toolCallId } - + public enum Role: String, Codable, Sendable { case system, user, assistant, tool } - + public enum Content: Codable, Sendable { case text(String) case multimodal([ContentPart]) - + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() - + if let text = try? container.decode(String.self) { self = .text(text) } else if let parts = try? container.decode([ContentPart].self) { self = .multimodal(parts) } else { throw DecodingError.dataCorrupted( - DecodingError.Context( - codingPath: decoder.codingPath, debugDescription: "Invalid content format") + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid content format") ) } } - + public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - + switch self { case .text(let text): try container.encode(text) @@ -243,7 +240,7 @@ public struct ChatMessage: Codable, Sendable { try container.encode(parts) } } - + public var text: String? { switch self { case .text(let text): @@ -258,7 +255,7 @@ public struct ChatMessage: Codable, Sendable { } } } - + enum CodingKeys: String, CodingKey { case role, content, name case toolCalls = "tool_calls" @@ -271,88 +268,87 @@ public enum ContentPart: Codable, Sendable { case text(TextPart) case image(ImagePart) case audio(AudioPart) - + public struct TextPart: Codable, Sendable { public let type: String public let text: String - + public init(text: String) { self.type = "text" self.text = text } - + private enum CodingKeys: String, CodingKey { case type, text } - + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.type = try container.decodeIfPresent(String.self, forKey: .type) ?? "text" self.text = try container.decode(String.self, forKey: .text) } } - + public struct ImagePart: Codable, Sendable { public let type = "image_url" public let imageUrl: ImageURL - + public init(imageUrl: ImageURL) { self.imageUrl = imageUrl } - + public struct ImageURL: Codable, Sendable { public let url: String public let detail: Detail? - + public init(url: String, detail: Detail? = nil) { self.url = url self.detail = detail } - + public enum Detail: String, Codable, Sendable { case auto, low, high } } - + enum CodingKeys: String, CodingKey { case type case imageUrl = "image_url" } } - + public struct AudioPart: Codable, Sendable { public let type = "input_audio" public let inputAudio: InputAudio - + public init(inputAudio: InputAudio) { self.inputAudio = inputAudio } - + public struct InputAudio: Codable, Sendable { - public let data: String // base64 encoded + public let data: String // base64 encoded public let format: Format - + public init(data: String, format: Format) { self.data = data self.format = format } - + public enum Format: String, Codable, Sendable { case wav, mp3, flac, m4a, ogg, oga, webm } } - + enum CodingKeys: String, CodingKey { case type case inputAudio = "input_audio" } } - + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: DynamicCodingKeys.self) - let type = try container.decode( - String.self, forKey: DynamicCodingKeys(stringValue: "type")!) - + let type = try container.decode(String.self, forKey: DynamicCodingKeys(stringValue: "type")!) + switch type { case "text": let textPart = try TextPart(from: decoder) @@ -365,13 +361,11 @@ public enum ContentPart: Codable, Sendable { self = .audio(audioPart) default: throw DecodingError.dataCorrupted( - DecodingError.Context( - codingPath: decoder.codingPath, - debugDescription: "Unknown content part type: \(type)") + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unknown content part type: \(type)") ) } } - + public func encode(to encoder: Encoder) throws { switch self { case .text(let textPart): @@ -388,27 +382,27 @@ public enum ContentPart: Codable, Sendable { public struct Tool: Codable, Sendable { public let type: String public let function: Function - + public init(function: Function) { self.type = "function" self.function = function } - + private enum CodingKeys: String, CodingKey { case type, function } - + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.type = try container.decodeIfPresent(String.self, forKey: .type) ?? "function" self.function = try container.decode(Function.self, forKey: .function) } - + public struct Function: Codable, Sendable { public let name: String public let description: String? public let parameters: JSONSchema - + public init(name: String, description: String? = nil, parameters: JSONSchema) { self.name = name self.description = description @@ -422,10 +416,10 @@ public enum ToolChoice: Codable, Sendable { case auto case required case function(String) - + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() - + if let string = try? container.decode(String.self) { switch string { case "none": self = .none @@ -433,27 +427,23 @@ public enum ToolChoice: Codable, Sendable { case "required": self = .required default: throw DecodingError.dataCorrupted( - DecodingError.Context( - codingPath: decoder.codingPath, - debugDescription: "Invalid tool choice: \(string)") + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid tool choice: \(string)") ) } } else if let object = try? container.decode([String: [String: String]].self), - let function = object["function"], - let name = function["name"] - { + let function = object["function"], + let name = function["name"] { self = .function(name) } else { throw DecodingError.dataCorrupted( - DecodingError.Context( - codingPath: decoder.codingPath, debugDescription: "Invalid tool choice format") + DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid tool choice format") ) } } - + public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - + switch self { case .none: try container.encode("none") @@ -465,12 +455,12 @@ public enum ToolChoice: Codable, Sendable { struct FunctionChoice: Codable { let type: String let function: FunctionName - + init(function: FunctionName) { self.type = "function" self.function = function } - + struct FunctionName: Codable { let name: String } @@ -485,17 +475,17 @@ public struct OpenAIToolCall: Codable, Sendable { public let id: String public let type: String public let function: FunctionCall - + public init(id: String, type: String = "function", function: FunctionCall) { self.id = id self.type = type self.function = function } - + public struct FunctionCall: Codable, Sendable { public let name: String public let arguments: String - + public init(name: String, arguments: String) { self.name = name self.arguments = arguments @@ -509,7 +499,7 @@ public struct JSONSchema: Codable, Sendable { public let properties: [String: JSONSchemaProperty]? public let required: [String]? public let description: String? - + public init( type: String, properties: [String: JSONSchemaProperty]? = nil, @@ -530,7 +520,7 @@ public struct JSONSchemaProperty: Codable, Sendable { public let minimum: Double? public let maximum: Double? public let items: Box? - + public init( type: String, description: String? = nil, @@ -546,7 +536,7 @@ public struct JSONSchemaProperty: Codable, Sendable { self.maximum = maximum self.items = items.map(Box.init) } - + enum CodingKeys: String, CodingKey { case type, description, minimum, maximum, items case enumValues = "enum" @@ -562,24 +552,24 @@ public struct ChatCompletionResponse: Codable, Sendable { public let choices: [Choice] public let usage: Usage? public let systemFingerprint: String? - + public struct Choice: Codable, Sendable { public let index: Int public let message: ChatMessage public let finishReason: String? - + enum CodingKeys: String, CodingKey { case index, message case finishReason = "finish_reason" } } - + public struct Usage: Codable, Sendable { public let promptTokens: Int public let completionTokens: Int? public let totalTokens: Int - public let reasoningTokens: Int? // For reasoning models - + public let reasoningTokens: Int? // For reasoning models + enum CodingKeys: String, CodingKey { case promptTokens = "prompt_tokens" case completionTokens = "completion_tokens" @@ -587,7 +577,7 @@ public struct ChatCompletionResponse: Codable, Sendable { case reasoningTokens = "reasoning_tokens" } } - + enum CodingKeys: String, CodingKey { case id, object, created, model, choices, usage case systemFingerprint = "system_fingerprint" @@ -632,8 +622,7 @@ public struct StreamingToolCall: Codable, Sendable { public let type: String? public let function: StreamingFunctionCall - public init(index: Int, id: String? = nil, type: String? = nil, function: StreamingFunctionCall) - { + public init(index: Int, id: String? = nil, type: String? = nil, function: StreamingFunctionCall) { self.index = index self.id = id self.type = type @@ -655,12 +644,12 @@ public struct StreamingToolCall: Codable, Sendable { private struct DynamicCodingKeys: CodingKey { var stringValue: String var intValue: Int? - + init?(stringValue: String) { self.stringValue = stringValue self.intValue = nil } - + init?(intValue: Int) { self.stringValue = String(intValue) self.intValue = intValue @@ -672,28 +661,24 @@ extension ChatMessage { public static func system(_ content: String) -> ChatMessage { return ChatMessage(role: .system, content: .text(content)) } - + public static func user(_ content: String) -> ChatMessage { return ChatMessage(role: .user, content: .text(content)) } - + public static func assistant(_ content: String) -> ChatMessage { return ChatMessage(role: .assistant, content: .text(content)) } - + public static func tool(content: String, toolCallId: String) -> ChatMessage { return ChatMessage(role: .tool, content: .text(content), toolCallId: toolCallId) } - - public static func userWithImage( - text: String, imageURL: String, detail: ContentPart.ImagePart.ImageURL.Detail = .auto - ) -> ChatMessage { + + public static func userWithImage(text: String, imageURL: String, detail: ContentPart.ImagePart.ImageURL.Detail = .auto) -> ChatMessage { let parts: [ContentPart] = [ .text(ContentPart.TextPart(text: text)), - .image( - ContentPart.ImagePart( - imageUrl: ContentPart.ImagePart.ImageURL(url: imageURL, detail: detail))), + .image(ContentPart.ImagePart(imageUrl: ContentPart.ImagePart.ImageURL(url: imageURL, detail: detail))) ] return ChatMessage(role: .user, content: .multimodal(parts)) } -} +} \ No newline at end of file