Files
Jarvis/packages/AgentCore/Tests/OllamaProviderTests/OllamaImageBlockEncodingTests.swift
james.maes 3c7370fe95 test(07-05): add failing Ollama image_url encoding tests (Task 3 RED)
- Native /api/chat path: data-URL image_url block
- OpenAI-compat /v1/chat/completions path: same data-URL shape
- Text precedes image_url; deterministic ordering
- Empty images byte-identical to single-modal encoders
2026-04-29 17:41:41 -05:00

132 lines
5.4 KiB
Swift

import XCTest
import Foundation
@testable import OllamaProvider
import AgentCore
/// Plan 07-05 / Task 3 — OpenAI-compat image_url JSON encoding (T1 + T2 paths).
///
/// Both `gemma4:31b` via Ollama (`/api/chat` native and `/v1/chat/completions`
/// OpenAI-compat) AND `Qwen3.5-35B-A3B-VL` via vllm-mlx (which speaks
/// OpenAI-compat) consume this same encoding. The shape:
///
/// `{"type":"image_url","image_url":{"url":"data:<mediaType>;base64,<b64>"}}`
final class OllamaImageBlockEncodingTests: XCTestCase {
private let fixtureBytes = Data([0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46])
private var fixtureBase64: String { fixtureBytes.base64EncodedString() }
// MARK: - Test 1: data-URL shape (native /api/chat)
func testNativeMultimodalBodyContainsDataURLImageBlock() throws {
let imageBlock = ImageBlock(mediaType: "image/jpeg", data: fixtureBytes)
let body = try OllamaRequestBody.encodeNativeMultimodal(
messages: [LLMMessage(role: .user, content: [.text("describe")])],
images: [imageBlock],
tools: [],
toolChoice: .auto,
model: ModelID(rawValue: "gemma4:31b"),
maxOutputTokens: 256
)
let json = try JSONSerialization.jsonObject(with: body) as! [String: Any]
let messages = json["messages"] as! [[String: Any]]
let userMsg = messages.last!
let content = userMsg["content"] as! [[String: Any]]
let imgBlocks = content.filter { ($0["type"] as? String) == "image_url" }
XCTAssertEqual(imgBlocks.count, 1)
let imgUrl = imgBlocks[0]["image_url"] as! [String: Any]
let url = imgUrl["url"] as! String
XCTAssertEqual(url, "data:image/jpeg;base64,\(fixtureBase64)",
"OpenAI-compat data-URL shape: data:<mediaType>;base64,<b64>")
}
// MARK: - Test 2: OpenAI-compat path produces identical image_url shape
func testOpenAICompatMultimodalBodyContainsDataURLImageBlock() throws {
let imageBlock = ImageBlock(mediaType: "image/png", data: fixtureBytes)
let body = try OllamaRequestBody.encodeOpenAICompatMultimodal(
messages: [LLMMessage(role: .user, content: [.text("describe")])],
images: [imageBlock],
tools: [],
toolChoice: .auto,
model: ModelID(rawValue: "Qwen/Qwen3.5-35B-A3B-VL"),
maxOutputTokens: 256
)
let json = try JSONSerialization.jsonObject(with: body) as! [String: Any]
let messages = json["messages"] as! [[String: Any]]
let userMsg = messages.last!
let content = userMsg["content"] as! [[String: Any]]
let imgBlocks = content.filter { ($0["type"] as? String) == "image_url" }
XCTAssertEqual(imgBlocks.count, 1)
let imgUrl = imgBlocks[0]["image_url"] as! [String: Any]
let url = imgUrl["url"] as! String
XCTAssertEqual(url, "data:image/png;base64,\(fixtureBase64)",
"media_type derived from ImageBlock.mediaType, not hard-coded")
}
// MARK: - Test 3: text precedes image_url
func testTextContentPrecedesImageURL() throws {
let imageBlock = ImageBlock(mediaType: "image/jpeg", data: fixtureBytes)
let body = try OllamaRequestBody.encodeOpenAICompatMultimodal(
messages: [LLMMessage(role: .user, content: [.text("question")])],
images: [imageBlock],
tools: [],
toolChoice: .auto,
model: ModelID(rawValue: "gemma4:31b"),
maxOutputTokens: 256
)
let json = try JSONSerialization.jsonObject(with: body) as! [String: Any]
let messages = json["messages"] as! [[String: Any]]
let content = messages.last!["content"] as! [[String: Any]]
XCTAssertEqual(content.count, 2)
XCTAssertEqual(content[0]["type"] as? String, "text")
XCTAssertEqual(content[1]["type"] as? String, "image_url")
}
// MARK: - Test 4: empty images regression-protect
func testEmptyImagesNativeMatchesSingleModal() throws {
let messages: [LLMMessage] = [
LLMMessage(role: .user, content: [.text("hello")])
]
let single = try OllamaRequestBody.encodeNative(
messages: messages,
tools: [],
toolChoice: .auto,
model: ModelID(rawValue: "qwen2.5-coder:32b"),
maxOutputTokens: 256
)
let multi = try OllamaRequestBody.encodeNativeMultimodal(
messages: messages,
images: [],
tools: [],
toolChoice: .auto,
model: ModelID(rawValue: "qwen2.5-coder:32b"),
maxOutputTokens: 256
)
XCTAssertEqual(single, multi)
}
func testEmptyImagesOpenAICompatMatchesSingleModal() throws {
let messages: [LLMMessage] = [
LLMMessage(role: .user, content: [.text("hello")])
]
let single = try OllamaRequestBody.encodeOpenAICompat(
messages: messages,
tools: [],
toolChoice: .auto,
model: ModelID(rawValue: "qwen2.5-coder:32b"),
maxOutputTokens: 256
)
let multi = try OllamaRequestBody.encodeOpenAICompatMultimodal(
messages: messages,
images: [],
tools: [],
toolChoice: .auto,
model: ModelID(rawValue: "qwen2.5-coder:32b"),
maxOutputTokens: 256
)
XCTAssertEqual(single, multi)
}
}