Skip to content

Commit a236a50

Browse files
Alex-ai-futurehuangjihui511
authored andcommitted
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: Jihui <huangjihui511@hotmail.com>
1 parent 2839ce4 commit a236a50

3 files changed

Lines changed: 282 additions & 13 deletions

File tree

Sources/OpenFoundationModelsOpenAI/Internal/TranscriptConverter.swift

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -435,46 +435,58 @@ internal struct TranscriptConverter {
435435
}
436436

437437
/// Extract response format with full JSON Schema from the most recent prompt
438-
static func extractResponseFormatWithSchema(from transcript: Transcript) -> ResponseFormat? {
438+
static func extractResponseFormatWithSchema(from transcript: Transcript, for model: OpenAIModel) -> ResponseFormat? {
439439
// Try JSON-based extraction to get complete schema
440-
return extractResponseFormatFromJSON(transcript)
440+
return extractResponseFormatFromJSON(transcript, for: model)
441441
}
442442

443443
/// Extract response format by encoding Transcript to JSON
444444
private static func extractResponseFormatFromJSON(_ transcript: Transcript) -> ResponseFormat? {
445+
// For backward compatibility, default to GPT model behavior
446+
// This method is used by extractResponseFormat which doesn't have model context
447+
return extractResponseFormatFromJSON(transcript, for: OpenAIModel("gpt-4o"))
448+
}
449+
450+
/// Extract response format by encoding Transcript to JSON
451+
private static func extractResponseFormatFromJSON(_ transcript: Transcript, for model: OpenAIModel) -> ResponseFormat? {
445452
do {
446453
// Encode transcript to JSON
447454
let encoder = JSONEncoder()
448455
let data = try encoder.encode(transcript)
449-
456+
450457
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
451458
let entries = json["entries"] as? [[String: Any]] else {
452459
return nil
453460
}
454-
461+
455462
// Look for the most recent prompt with responseFormat
456463
for entry in entries.reversed() {
457464
if entry["type"] as? String == "prompt",
458465
let responseFormat = entry["responseFormat"] as? [String: Any] {
459-
466+
460467
#if DEBUG
461468
print("Found response format in JSON: \(responseFormat)")
462469
#endif
463-
470+
464471
// Check if there's a schema (now available with updated OpenFoundationModels)
465472
if let schema = responseFormat["schema"] as? [String: Any] {
466-
// Transform schema to OpenAI's expected format
467-
let transformedSchema = transformToOpenAIJSONSchema(schema)
468-
return .jsonSchema(transformedSchema)
473+
// For models that don't support json_schema (like DeepSeek), fallback to json mode
474+
if model.modelType == .deepseek {
475+
return .json
476+
} else {
477+
// Transform schema to OpenAI's expected format
478+
let transformedSchema = transformToOpenAIJSONSchema(schema)
479+
return .jsonSchema(transformedSchema)
480+
}
469481
}
470-
482+
471483
// If there's a name or type field, we know JSON is expected
472484
if responseFormat["name"] != nil || responseFormat["type"] != nil {
473485
return .json
474486
}
475487
}
476488
}
477-
489+
478490
return nil
479491
} catch {
480492
#if DEBUG

Sources/OpenFoundationModelsOpenAI/OpenAILanguageModel.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public final class OpenAILanguageModel: LanguageModel, @unchecked Sendable {
5656
// Use TranscriptConverter for all conversions
5757
let messages = TranscriptConverter.buildMessages(from: transcript)
5858
let tools = TranscriptConverter.extractTools(from: transcript)
59-
let responseFormat = TranscriptConverter.extractResponseFormatWithSchema(from: transcript)
59+
let responseFormat = TranscriptConverter.extractResponseFormatWithSchema(from: transcript, for: model)
6060
let finalOptions = options ?? TranscriptConverter.extractOptions(from: transcript)
6161

6262
// Build request with response format if present
@@ -101,7 +101,7 @@ public final class OpenAILanguageModel: LanguageModel, @unchecked Sendable {
101101
// Use TranscriptConverter for all conversions
102102
let messages = TranscriptConverter.buildMessages(from: transcript)
103103
let tools = TranscriptConverter.extractTools(from: transcript)
104-
let responseFormat = TranscriptConverter.extractResponseFormatWithSchema(from: transcript)
104+
let responseFormat = TranscriptConverter.extractResponseFormatWithSchema(from: transcript, for: model)
105105
let finalOptions = options ?? TranscriptConverter.extractOptions(from: transcript)
106106

107107
let request = try buildChatRequestWithFormat(
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import Testing
2+
import Foundation
3+
@testable import OpenFoundationModelsOpenAI
4+
import OpenFoundationModels
5+
6+
/// Tests specifically for DeepSeek API compatibility
7+
@Suite("DeepSeek Compatibility Tests")
8+
struct DeepSeekCompatibilityTests {
9+
10+
// MARK: - Model Identification Tests
11+
12+
@Test("DeepSeek model identification works correctly")
13+
func testDeepSeekModelIdentification() {
14+
// Test various DeepSeek model names
15+
let deepSeekModels = ["deepseek-chat", "deepseek-coder", "deepseek-v3", "deepseek-r1"]
16+
17+
for modelName in deepSeekModels {
18+
let model = OpenAIModel(modelName)
19+
#expect(model.modelType == .deepseek, "Model \(modelName) should be identified as DeepSeek")
20+
#expect(model.apiName == modelName, "API name should match model name")
21+
}
22+
23+
// Test that non-DeepSeek models are not identified as DeepSeek
24+
let nonDeepSeekModels = ["gpt-4o", "gpt-4-turbo", "o1", "claude-3"]
25+
26+
for modelName in nonDeepSeekModels {
27+
let model = OpenAIModel(modelName)
28+
#expect(model.modelType != .deepseek, "Model \(modelName) should not be identified as DeepSeek")
29+
}
30+
}
31+
32+
@Test("DeepSeek models have correct capabilities")
33+
func testDeepSeekModelCapabilities() {
34+
let deepSeekModel = OpenAIModel("deepseek-chat")
35+
36+
// DeepSeek should support text generation, function calling, and streaming
37+
#expect(deepSeekModel.supportsVision == false, "DeepSeek should not support vision")
38+
#expect(deepSeekModel.supportsFunctionCalling == true, "DeepSeek should support function calling")
39+
#expect(deepSeekModel.supportsStreaming == true, "DeepSeek should support streaming")
40+
#expect(deepSeekModel.isReasoningModel == false, "DeepSeek should not be a reasoning model")
41+
42+
// Check capabilities set
43+
#expect(deepSeekModel.capabilities.contains(.textGeneration), "DeepSeek should support text generation")
44+
#expect(deepSeekModel.capabilities.contains(.functionCalling), "DeepSeek should support function calling")
45+
#expect(deepSeekModel.capabilities.contains(.streaming), "DeepSeek should support streaming")
46+
#expect(deepSeekModel.capabilities.contains(.toolAccess), "DeepSeek should support tool access")
47+
#expect(!deepSeekModel.capabilities.contains(.vision), "DeepSeek should not support vision")
48+
#expect(!deepSeekModel.capabilities.contains(.reasoning), "DeepSeek should not support reasoning")
49+
}
50+
51+
@Test("DeepSeek models have correct context window and tokens")
52+
func testDeepSeekModelLimits() {
53+
let deepSeekModel = OpenAIModel("deepseek-chat")
54+
55+
#expect(deepSeekModel.contextWindow == 32768, "DeepSeek should have 32K context window")
56+
#expect(deepSeekModel.maxOutputTokens == 8192, "DeepSeek should have 8K max output tokens")
57+
}
58+
59+
// MARK: - Response Format Compatibility Tests
60+
61+
@Test("DeepSeek models use JSON response format instead of JSON Schema")
62+
func testDeepSeekResponseFormatCompatibility() throws {
63+
// Create a simple schema for testing
64+
let schema = GenerationSchema(
65+
type: String.self,
66+
description: "Test response",
67+
properties: []
68+
)
69+
70+
// Test with GPT model (should use json_schema)
71+
let gptModel = OpenAIModel("gpt-4o")
72+
let gptResponseFormat = convertSchemaToResponseFormat(schema, for: gptModel)
73+
74+
// Test with DeepSeek model (should use json)
75+
let deepSeekModel = OpenAIModel("deepseek-chat")
76+
let deepSeekResponseFormat = convertSchemaToResponseFormat(schema, for: deepSeekModel)
77+
78+
// GPT should use jsonSchema format
79+
switch gptResponseFormat {
80+
case .jsonSchema:
81+
// Expected for GPT models
82+
break
83+
default:
84+
#expect(Bool(false), "GPT model should use jsonSchema format")
85+
}
86+
87+
// DeepSeek should use json format
88+
switch deepSeekResponseFormat {
89+
case .json:
90+
// Expected for DeepSeek models
91+
break
92+
case .jsonSchema:
93+
#expect(Bool(false), "DeepSeek model should not use jsonSchema format")
94+
case .text:
95+
#expect(Bool(false), "DeepSeek model should not use text format")
96+
}
97+
}
98+
99+
@Test("TranscriptConverter respects model type for response format extraction")
100+
func testTranscriptConverterDeepSeekResponseFormat() throws {
101+
// Create a transcript with a prompt that has a response format
102+
let schema = GenerationSchema(
103+
type: String.self,
104+
description: "Response schema",
105+
properties: []
106+
)
107+
108+
let responseFormat = Transcript.ResponseFormat(schema: schema)
109+
110+
let transcript = Transcript(
111+
entries: [
112+
.prompt(
113+
Transcript.Prompt(
114+
segments: [.text(Transcript.TextSegment(content: "Generate a response"))],
115+
responseFormat: responseFormat
116+
)
117+
)
118+
]
119+
)
120+
121+
// Test with GPT model
122+
let gptModel = OpenAIModel("gpt-4o")
123+
let gptExtractedFormat = TranscriptConverter.extractResponseFormatWithSchema(from: transcript, for: gptModel)
124+
125+
// Test with DeepSeek model
126+
let deepSeekModel = OpenAIModel("deepseek-chat")
127+
let deepSeekExtractedFormat = TranscriptConverter.extractResponseFormatWithSchema(from: transcript, for: deepSeekModel)
128+
129+
// GPT should extract jsonSchema format
130+
switch gptExtractedFormat {
131+
case .jsonSchema:
132+
// Expected
133+
break
134+
default:
135+
#expect(Bool(false), "GPT model should extract jsonSchema format")
136+
}
137+
138+
// DeepSeek should extract json format
139+
switch deepSeekExtractedFormat {
140+
case .json:
141+
// Expected
142+
break
143+
case .jsonSchema:
144+
#expect(Bool(false), "DeepSeek model should extract json format, not jsonSchema")
145+
default:
146+
#expect(Bool(false), "DeepSeek model should extract json format")
147+
}
148+
}
149+
150+
// MARK: - Request Builder Compatibility Tests
151+
152+
@Test("DeepSeek models use correct request builder")
153+
func testDeepSeekRequestBuilder() {
154+
let deepSeekModel = OpenAIModel("deepseek-chat")
155+
156+
// DeepSeek should use GPTRequestBuilder, not ReasoningRequestBuilder
157+
switch deepSeekModel.modelType {
158+
case .deepseek:
159+
// This should work without throwing
160+
let builder = GPTRequestBuilder()
161+
#expect(type(of: builder) == GPTRequestBuilder.self)
162+
default:
163+
#expect(Bool(false), "DeepSeek model should have deepseek type")
164+
}
165+
}
166+
167+
@Test("DeepSeek models use correct response handler")
168+
func testDeepSeekResponseHandler() {
169+
let deepSeekModel = OpenAIModel("deepseek-chat")
170+
171+
// DeepSeek should use GPTResponseHandler, not ReasoningResponseHandler
172+
switch deepSeekModel.modelType {
173+
case .deepseek:
174+
// This should work without throwing
175+
let handler = GPTResponseHandler()
176+
#expect(type(of: handler) == GPTResponseHandler.self)
177+
default:
178+
#expect(Bool(false), "DeepSeek model should have deepseek type")
179+
}
180+
}
181+
}
182+
183+
// MARK: - Helper Functions for Testing
184+
185+
/// Convert GenerationSchema to ResponseFormat for a specific model (test helper)
186+
private func convertSchemaToResponseFormat(_ schema: GenerationSchema, for model: OpenAIModel) -> ResponseFormat {
187+
// This replicates the logic from OpenAILanguageModel.convertSchemaToResponseFormat
188+
// For models that don't support json_schema (like DeepSeek), fallback to json mode
189+
if model.modelType == .deepseek {
190+
return .json
191+
}
192+
193+
// For other models, try to create jsonSchema format
194+
do {
195+
let encoder = JSONEncoder()
196+
let schemaData = try encoder.encode(schema)
197+
198+
// Convert to JSON dictionary
199+
if let schemaJson = try JSONSerialization.jsonObject(with: schemaData) as? [String: Any] {
200+
// Transform to OpenAI's expected JSON Schema format
201+
let transformedSchema = transformToOpenAIJSONSchema(schemaJson)
202+
return .jsonSchema(transformedSchema)
203+
}
204+
} catch {
205+
// Ignore encoding errors in test
206+
}
207+
208+
// Fallback to JSON mode
209+
return .json
210+
}
211+
212+
/// Transform GenerationSchema JSON to OpenAI's JSON Schema format (test helper)
213+
private func transformToOpenAIJSONSchema(_ json: [String: Any]) -> [String: Any] {
214+
var schema: [String: Any] = [:]
215+
216+
// Extract type (default to "object")
217+
schema["type"] = json["type"] as? String ?? "object"
218+
219+
// Extract and transform properties
220+
if let properties = json["properties"] as? [String: [String: Any]] {
221+
var transformedProperties: [String: [String: Any]] = [:]
222+
223+
for (key, propJson) in properties {
224+
var prop: [String: Any] = [:]
225+
prop["type"] = propJson["type"] as? String ?? "string"
226+
227+
if let description = propJson["description"] as? String {
228+
prop["description"] = description
229+
}
230+
231+
if let enumValues = propJson["enum"] as? [String] {
232+
prop["enum"] = enumValues
233+
}
234+
235+
if prop["type"] as? String == "array",
236+
let items = propJson["items"] as? [String: Any] {
237+
prop["items"] = items
238+
}
239+
240+
transformedProperties[key] = prop
241+
}
242+
243+
schema["properties"] = transformedProperties
244+
}
245+
246+
// Extract required fields
247+
if let required = json["required"] as? [String] {
248+
schema["required"] = required
249+
}
250+
251+
// Add description if present
252+
if let description = json["description"] as? String {
253+
schema["description"] = description
254+
}
255+
256+
return schema
257+
}

0 commit comments

Comments
 (0)