diff --git a/Sources/OpenAI/Public/ResponseModels/Response/ResponseModel.swift b/Sources/OpenAI/Public/ResponseModels/Response/ResponseModel.swift index 062969b7..8173a3e7 100644 --- a/Sources/OpenAI/Public/ResponseModels/Response/ResponseModel.swift +++ b/Sources/OpenAI/Public/ResponseModels/Response/ResponseModel.swift @@ -54,10 +54,11 @@ public struct ResponseModel: Decodable { } } - /// Instructions type - can be a string or an array of strings + /// Instructions type - can be a string, an array of strings, or an array of messages (for reusable prompts) public enum InstructionsType: Decodable { case string(String) case array([String]) + case messages([InputMessage]) public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() @@ -66,10 +67,12 @@ public struct ResponseModel: Decodable { self = .string(stringValue) } else if let arrayValue = try? container.decode([String].self) { self = .array(arrayValue) + } else if let messagesValue = try? container.decode([InputMessage].self) { + self = .messages(messagesValue) } else { throw DecodingError.dataCorruptedError( in: container, - debugDescription: "Expected String or [String] for instructions") + debugDescription: "Expected String, [String], or [InputMessage] for instructions") } } } diff --git a/Tests/OpenAITests/ResponseModelValidationTests.swift b/Tests/OpenAITests/ResponseModelValidationTests.swift index 1dac6d2d..dba2a5bf 100644 --- a/Tests/OpenAITests/ResponseModelValidationTests.swift +++ b/Tests/OpenAITests/ResponseModelValidationTests.swift @@ -194,6 +194,62 @@ final class ResponseModelValidationTests: XCTestCase { XCTAssertEqual(responseModel.usage?.outputTokens, 1035) } + // MARK: - InstructionsType Tests + + func testInstructionsTypeStringDecoding() throws { + let decoder = JSONDecoder() + let responseModel = try decoder.decode(ResponseModel.self, from: instructionsStringJSON.data(using: .utf8)!) + + XCTAssertNotNil(responseModel.instructions) + if case .string(let value) = responseModel.instructions { + XCTAssertEqual(value, "You are a helpful assistant.") + } else { + XCTFail("Expected string instructions type") + } + } + + func testInstructionsTypeArrayOfStringsDecoding() throws { + let decoder = JSONDecoder() + let responseModel = try decoder.decode(ResponseModel.self, from: instructionsArrayOfStringsJSON.data(using: .utf8)!) + + XCTAssertNotNil(responseModel.instructions) + if case .array(let values) = responseModel.instructions { + XCTAssertEqual(values.count, 2) + XCTAssertEqual(values[0], "Be helpful.") + XCTAssertEqual(values[1], "Be concise.") + } else { + XCTFail("Expected array of strings instructions type") + } + } + + func testInstructionsTypeMessagesDecoding() throws { + // This tests the fix for issue #187 - reusable prompts return instructions as message objects + let decoder = JSONDecoder() + let responseModel = try decoder.decode(ResponseModel.self, from: instructionsMessagesJSON.data(using: .utf8)!) + + XCTAssertNotNil(responseModel.instructions) + if case .messages(let messages) = responseModel.instructions { + XCTAssertEqual(messages.count, 2) + XCTAssertEqual(messages[0].role, "developer") + XCTAssertEqual(messages[0].type, "message") + XCTAssertEqual(messages[1].role, "assistant") + + // Validate content of first message + if case .array(let contentItems) = messages[0].content { + XCTAssertEqual(contentItems.count, 1) + if case .text(let textContent) = contentItems[0] { + XCTAssertEqual(textContent.text, "You are a helpful assistant for {{customer_name}}.") + } else { + XCTFail("Expected text content item") + } + } else { + XCTFail("Expected array content in message") + } + } else { + XCTFail("Expected messages instructions type") + } + } + // MARK: - Test Data private let textInputResponseJSON = """ @@ -679,4 +735,114 @@ final class ResponseModelValidationTests: XCTestCase { "metadata": {} } """ + + // MARK: - InstructionsType Test Data + + private let instructionsStringJSON = """ + { + "id": "resp_test_string_instructions", + "object": "response", + "created_at": 1741476542, + "status": "completed", + "error": null, + "incomplete_details": null, + "instructions": "You are a helpful assistant.", + "max_output_tokens": null, + "model": "gpt-4.1-2025-04-14", + "output": [], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": null, + "store": true, + "temperature": 1.0, + "text": null, + "tool_choice": "auto", + "tools": [], + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 10, + "output_tokens": 10, + "total_tokens": 20 + }, + "user": null, + "metadata": {} + } + """ + + private let instructionsArrayOfStringsJSON = """ + { + "id": "resp_test_array_instructions", + "object": "response", + "created_at": 1741476542, + "status": "completed", + "error": null, + "incomplete_details": null, + "instructions": ["Be helpful.", "Be concise."], + "max_output_tokens": null, + "model": "gpt-4.1-2025-04-14", + "output": [], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": null, + "store": true, + "temperature": 1.0, + "text": null, + "tool_choice": "auto", + "tools": [], + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 10, + "output_tokens": 10, + "total_tokens": 20 + }, + "user": null, + "metadata": {} + } + """ + + /// This JSON represents the response format when using reusable prompts with variables (issue #187) + private let instructionsMessagesJSON = """ + { + "id": "resp_test_messages_instructions", + "object": "response", + "created_at": 1741476542, + "status": "completed", + "error": null, + "incomplete_details": null, + "instructions": [ + { + "type": "message", + "content": [{"type": "input_text", "text": "You are a helpful assistant for {{customer_name}}."}], + "role": "developer" + }, + { + "type": "message", + "content": [{"type": "input_text", "text": ""}], + "role": "assistant" + } + ], + "max_output_tokens": null, + "model": "gpt-4.1-2025-04-14", + "output": [], + "parallel_tool_calls": true, + "previous_response_id": null, + "reasoning": null, + "store": true, + "temperature": 1.0, + "text": null, + "tool_choice": "auto", + "tools": [], + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 10, + "output_tokens": 10, + "total_tokens": 20 + }, + "user": null, + "metadata": {} + } + """ }