Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 24 additions & 1 deletion Sources/OpenFoundationModelsOpenAI/Models/OpenAIModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand All @@ -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
}
}

Expand All @@ -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
}
}

Expand Down Expand Up @@ -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
)
}
}

Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -255,4 +278,4 @@ extension OpenAIModel {
public static func == (lhs: OpenAIModel, rhs: OpenAIModel) -> Bool {
return lhs.id == rhs.id
}
}
}
17 changes: 13 additions & 4 deletions Sources/OpenFoundationModelsOpenAI/OpenAILanguageModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -52,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
Expand Down Expand Up @@ -97,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(
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand Down
Loading