Skip to content
Merged
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 @@ -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()
Expand All @@ -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")
}
}
}
Expand Down
166 changes: 166 additions & 0 deletions Tests/OpenAITests/ResponseModelValidationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
Expand Down Expand Up @@ -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": {}
}
"""
}
Loading