From 781153222c32c55170c57a18b3fec23bf7c02329 Mon Sep 17 00:00:00 2001 From: jamesrochabrun Date: Fri, 26 Dec 2025 21:53:19 -0800 Subject: [PATCH] Fix InstructionsType decoding for reusable prompts (issue #187) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When using OpenAI's reusable prompts with variables, the `instructions` field returns as an array of message objects instead of String or [String]. This adds support for decoding [InputMessage] in InstructionsType. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../Response/ResponseModel.swift | 7 +- .../ResponseModelValidationTests.swift | 166 ++++++++++++++++++ 2 files changed, 171 insertions(+), 2 deletions(-) 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": {} + } + """ }