From c36708ac2eb944dc68c9b878d60f467c6fe53608 Mon Sep 17 00:00:00 2001 From: Arturo Carretero Calvo <10163049+ArtCC@users.noreply.github.com> Date: Thu, 24 Sep 2026 07:29:51 +0200 Subject: [PATCH 1/2] Fix GPT image generation request formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Omit `response_format` for GPT Image models and aliased image IDs - Keep `response_format: "b64_json"` for explicit DALL·E 2 and 3 models - Update image generation request model to allow a missing response format - Add tests covering request payloads for GPT Image, alias, and DALL·E models - Update changelog, README version badge, TestFlight notes, and app versions - Adjust deployment targets to 26.6 in the Xcode project --- CHANGELOG.md | 6 ++ README.md | 2 +- TestFlight/WhatToTest.en-US.txt | 1 + TestFlight/WhatToTest.es-ES.txt | 1 + ...nversationListViewModelTests+Loading.swift | 24 +++++++- .../ImageGenerationRepositoryTests.swift | 57 ++++++++++++++++++- openclient-llm.xcodeproj/project.pbxproj | 44 +++++++------- .../Models/ImageGenerationRequest.swift | 2 +- .../ImageGenerationRepository.swift | 2 +- specs/litellm-api.instructions.md | 2 + 10 files changed, 114 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a96debd0..bbe26d62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. +## [1.7.15-build-117] - 2026-09-24 + +### Fixed + +- Image generation with GPT Image models no longer fails because of an unsupported response format parameter + ## [1.7.10-build-115] - 2026-09-19 ### Changed diff --git a/README.md b/README.md index 1b23aee9..99a9e8e6 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ License Platform Swift - Version 1.7.10 + Version 1.7.15

OpenClient connects directly to the AI server you configure, without an OpenClient-hosted proxy or subscription. diff --git a/TestFlight/WhatToTest.en-US.txt b/TestFlight/WhatToTest.en-US.txt index 38b16e0f..6068a460 100644 --- a/TestFlight/WhatToTest.en-US.txt +++ b/TestFlight/WhatToTest.en-US.txt @@ -5,6 +5,7 @@ Hi there! We've got some great new features for you in this update. • Announcements on the Home screen now animate into view on Mac with the same smooth transition used on iPhone and iPad. • Your conversation list no longer shows a duplicate separator above your chats. • The Models screen no longer repeats requests while loading, so it stays responsive. +• Image generation now works with GPT Image models through LiteLLM, without requests failing because of an unsupported setting. • Minor bug fixes and improvements for a smoother experience. Thanks for your continued support and for helping us build the best possible LLM client together. diff --git a/TestFlight/WhatToTest.es-ES.txt b/TestFlight/WhatToTest.es-ES.txt index a714c407..f8912195 100644 --- a/TestFlight/WhatToTest.es-ES.txt +++ b/TestFlight/WhatToTest.es-ES.txt @@ -5,6 +5,7 @@ • Los anuncios de la pantalla de Inicio ahora aparecen con una animación fluida en Mac, con la misma transición que en iPhone e iPad. • La lista de conversaciones ya no muestra un separador duplicado encima de tus chats. • La pantalla de Modelos ya no repite solicitudes mientras se carga, para mantenerse ágil. +• La generación de imágenes ya funciona con modelos GPT Image a través de LiteLLM, sin que las solicitudes fallen por un ajuste no compatible. • Pequeñas correcciones de errores y mejoras para disfrutar de una experiencia más fluida. Gracias por seguir apoyándonos y por ayudarnos a crear juntos el mejor cliente posible para modelos de lenguaje. diff --git a/openclient-llm-test/Features/Chat/ConversationListViewModelTests+Loading.swift b/openclient-llm-test/Features/Chat/ConversationListViewModelTests+Loading.swift index 7dd3dd2f..e754b51c 100644 --- a/openclient-llm-test/Features/Chat/ConversationListViewModelTests+Loading.swift +++ b/openclient-llm-test/Features/Chat/ConversationListViewModelTests+Loading.swift @@ -6,6 +6,7 @@ // Copyright © 2026 Arturo Carretero Calvo. All rights reserved. // +import Observation import XCTest @testable import openclient_llm @@ -21,10 +22,12 @@ extension ConversationListViewModelTests { ] mockLoadConversations.result = .success(conversations) mockFetchModels.result = .success([LLMModel(id: "gpt-4")]) + let modelsLoaded = expectation(description: "Available models loaded") // When sut.send(.viewAppeared) - for _ in 0..<10 { await Task.yield() } + Self.observeModelsLoaded(sut, expectation: modelsLoaded) + await fulfillment(of: [modelsLoaded], timeout: 2) // Then guard case .loaded(let loadedState) = sut.state else { @@ -94,3 +97,22 @@ extension ConversationListViewModelTests { XCTAssertTrue(loadedState.availableModels.isEmpty) } } + +// MARK: - Private + +private extension ConversationListViewModelTests { + static func observeModelsLoaded(_ sut: ConversationListViewModel, expectation: XCTestExpectation) { + if case .loaded(let loadedState) = sut.state, !loadedState.availableModels.isEmpty { + expectation.fulfill() + return + } + withObservationTracking { + _ = sut.state + } onChange: { [weak sut] in + Task { @MainActor in + guard let sut else { return } + observeModelsLoaded(sut, expectation: expectation) + } + } + } +} diff --git a/openclient-llm-test/Features/ImageGeneration/ImageGenerationRepositoryTests.swift b/openclient-llm-test/Features/ImageGeneration/ImageGenerationRepositoryTests.swift index 9689d3eb..7b3c0394 100644 --- a/openclient-llm-test/Features/ImageGeneration/ImageGenerationRepositoryTests.swift +++ b/openclient-llm-test/Features/ImageGeneration/ImageGenerationRepositoryTests.swift @@ -30,6 +30,55 @@ final class ImageGenerationRepositoryTests: XCTestCase { XCTAssertEqual(apiClient.lastRequestEndpoint, "images/generations") XCTAssertEqual(apiClient.lastRequestTimeoutInterval, 600) XCTAssertNil(apiClient.lastMultipartEndpoint) + let body = try XCTUnwrap(apiClient.lastRequestBody) + let json = try JSONEncoder().encode(body) + let fields = try XCTUnwrap(JSONSerialization.jsonObject(with: json) as? [String: Any]) + XCTAssertEqual(fields["model"] as? String, "gpt-image-2") + XCTAssertEqual(fields["prompt"] as? String, "A cat") + XCTAssertEqual(fields["n"] as? Int, 1) + XCTAssertNil(fields["response_format"]) + } + + func test_generateImage_gptImageAndAlias_omitUnsupportedResponseFormat() async throws { + for model in ["gpt-image-1.5", "custom-image-alias"] { + // Given + let apiClient = MockAPIClient() + apiClient.requestResult = ImageGenerationResponse(data: [ + .init(url: nil, b64Json: Data([1, 2, 3]).base64EncodedString(), revisedPrompt: nil) + ]) + let sut = ImageGenerationRepository(apiClient: apiClient) + + // When + _ = try await sut.generateImage(prompt: "A cat", model: model, images: []) + + // Then + let body = try XCTUnwrap(apiClient.lastRequestBody) + let json = try JSONEncoder().encode(body) + let fields = try XCTUnwrap(JSONSerialization.jsonObject(with: json) as? [String: Any]) + XCTAssertEqual(fields["model"] as? String, model) + XCTAssertNil(fields["response_format"]) + } + } + + func test_generateImage_dallEModels_requestBase64ResponseFormat() async throws { + for model in ["dall-e-2", "dall-e-3"] { + // Given + let apiClient = MockAPIClient() + apiClient.requestResult = ImageGenerationResponse(data: [ + .init(url: nil, b64Json: Data([1, 2, 3]).base64EncodedString(), revisedPrompt: nil) + ]) + let sut = ImageGenerationRepository(apiClient: apiClient) + + // When + _ = try await sut.generateImage(prompt: "A cat", model: model, images: []) + + // Then + let body = try XCTUnwrap(apiClient.lastRequestBody) + let json = try JSONEncoder().encode(body) + let fields = try XCTUnwrap(JSONSerialization.jsonObject(with: json) as? [String: Any]) + XCTAssertEqual(fields["model"] as? String, model) + XCTAssertEqual(fields["response_format"] as? String, "b64_json") + } } func test_generateImage_multipleImages_sendsOrderedMultipartFilesAndFields() async throws { @@ -87,7 +136,7 @@ final class ImageGenerationRepositoryTests: XCTestCase { let sut = ImageGenerationRepository(apiClient: apiClient) // When - let result = try await sut.generateImage(prompt: "A cat", model: "gpt-image-2", images: images) + let result = try await sut.generateImage(prompt: "A cat", model: "custom-image-alias", images: images) // Then XCTAssertEqual(result.data, Data([9, 8, 7])) @@ -95,6 +144,12 @@ final class ImageGenerationRepositoryTests: XCTestCase { XCTAssertEqual(result.revisedPrompt, "A blue cat") XCTAssertEqual(apiClient.lastRequestEndpoint, images.isEmpty ? "images/generations" : nil) XCTAssertEqual(apiClient.lastMultipartEndpoint, images.isEmpty ? nil : "images/edits") + if images.isEmpty { + let body = try XCTUnwrap(apiClient.lastRequestBody) + let json = try JSONEncoder().encode(body) + let fields = try XCTUnwrap(JSONSerialization.jsonObject(with: json) as? [String: Any]) + XCTAssertNil(fields["response_format"]) + } } } diff --git a/openclient-llm.xcodeproj/project.pbxproj b/openclient-llm.xcodeproj/project.pbxproj index ca560948..de460381 100644 --- a/openclient-llm.xcodeproj/project.pbxproj +++ b/openclient-llm.xcodeproj/project.pbxproj @@ -791,8 +791,8 @@ "@executable_path/../Frameworks", "@executable_path/../../../../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 26.0; - MARKETING_VERSION = 1.7.10; + MACOSX_DEPLOYMENT_TARGET = 26.6; + MARKETING_VERSION = 1.7.15; PRODUCT_BUNDLE_IDENTIFIER = "com.artcc.openclient-llm.macos-widgets"; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -838,8 +838,8 @@ "@executable_path/../Frameworks", "@executable_path/../../../../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 26.0; - MARKETING_VERSION = 1.7.10; + MACOSX_DEPLOYMENT_TARGET = 26.6; + MARKETING_VERSION = 1.7.15; PRODUCT_BUNDLE_IDENTIFIER = "com.artcc.openclient-llm.macos-widgets"; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -1010,12 +1010,12 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 26.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.7.10; + MARKETING_VERSION = 1.7.15; PRODUCT_BUNDLE_IDENTIFIER = "com.artcc.openclient-llm"; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -1059,12 +1059,12 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 26.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.7.10; + MARKETING_VERSION = 1.7.15; PRODUCT_BUNDLE_IDENTIFIER = "com.artcc.openclient-llm"; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -1122,8 +1122,8 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 26.0; - MARKETING_VERSION = 1.7.10; + MACOSX_DEPLOYMENT_TARGET = 26.6; + MARKETING_VERSION = 1.7.15; PRODUCT_BUNDLE_IDENTIFIER = "com.artcc.openclient-llm"; PRODUCT_NAME = OpenClient; REGISTER_APP_GROUPS = YES; @@ -1179,8 +1179,8 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 26.0; - MARKETING_VERSION = 1.7.10; + MACOSX_DEPLOYMENT_TARGET = 26.6; + MARKETING_VERSION = 1.7.15; PRODUCT_BUNDLE_IDENTIFIER = "com.artcc.openclient-llm"; PRODUCT_NAME = OpenClient; REGISTER_APP_GROUPS = YES; @@ -1203,7 +1203,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = Q4X55987WE; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 26.6; MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = "com.artcc.openclient-llm-test"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1226,7 +1226,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = Q4X55987WE; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 26.6; MARKETING_VERSION = 1.0.0; PRODUCT_BUNDLE_IDENTIFIER = "com.artcc.openclient-llm-test"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1252,13 +1252,13 @@ INFOPLIST_FILE = ShareExtension/Resources/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = OpenClient; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 Arturo Carretero Calvo. All rights reserved."; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 26.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.7.10; + MARKETING_VERSION = 1.7.15; PRODUCT_BUNDLE_IDENTIFIER = "com.artcc.openclient-llm.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -1283,13 +1283,13 @@ INFOPLIST_FILE = ShareExtension/Resources/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = OpenClient; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 Arturo Carretero Calvo. All rights reserved."; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 26.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.7.10; + MARKETING_VERSION = 1.7.15; PRODUCT_BUNDLE_IDENTIFIER = "com.artcc.openclient-llm.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -1316,13 +1316,13 @@ INFOPLIST_FILE = "WidgetsExtension-iOS/Resources/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = OpenClient; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 Arturo Carretero Calvo. All rights reserved."; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 26.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.7.10; + MARKETING_VERSION = 1.7.15; PRODUCT_BUNDLE_IDENTIFIER = "com.artcc.openclient-llm.widgets"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -1349,13 +1349,13 @@ INFOPLIST_FILE = "WidgetsExtension-iOS/Resources/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = OpenClient; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 Arturo Carretero Calvo. All rights reserved."; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 26.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.7.10; + MARKETING_VERSION = 1.7.15; PRODUCT_BUNDLE_IDENTIFIER = "com.artcc.openclient-llm.widgets"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; diff --git a/openclient-llm/Shared/Core/Networking/Models/ImageGenerationRequest.swift b/openclient-llm/Shared/Core/Networking/Models/ImageGenerationRequest.swift index 6a17770b..afabbf00 100644 --- a/openclient-llm/Shared/Core/Networking/Models/ImageGenerationRequest.swift +++ b/openclient-llm/Shared/Core/Networking/Models/ImageGenerationRequest.swift @@ -12,7 +12,7 @@ nonisolated struct ImageGenerationRequest: Encodable, Sendable { let model: String let prompt: String let numberOfImages: Int - let responseFormat: String + let responseFormat: String? enum CodingKeys: String, CodingKey { case model diff --git a/openclient-llm/Shared/Features/ImageGeneration/Repositories/ImageGenerationRepository.swift b/openclient-llm/Shared/Features/ImageGeneration/Repositories/ImageGenerationRepository.swift index 954d2e03..f6c8caa1 100644 --- a/openclient-llm/Shared/Features/ImageGeneration/Repositories/ImageGenerationRepository.swift +++ b/openclient-llm/Shared/Features/ImageGeneration/Repositories/ImageGenerationRepository.swift @@ -71,7 +71,7 @@ struct ImageGenerationRepository: ImageGenerationRepositoryProtocol { model: model, prompt: prompt, numberOfImages: 1, - responseFormat: "b64_json" + responseFormat: ["dall-e-2", "dall-e-3"].contains(model) ? "b64_json" : nil ) return try await apiClient.request( endpoint: "images/generations", diff --git a/specs/litellm-api.instructions.md b/specs/litellm-api.instructions.md index 0c1c1356..4edf60dd 100644 --- a/specs/litellm-api.instructions.md +++ b/specs/litellm-api.instructions.md @@ -51,6 +51,8 @@ description: "Use when changing OpenAI-compatible or LiteLLM networking, model d - Dedicated `.imageGeneration` models use `POST /images/generations`; the existing dedicated edit flow uses multipart `POST /images/edits`. Dedicated responses may contain bounded base64 data or an HTTP(S) URL handled by the existing image repository. +- Dedicated generation requests include `response_format: "b64_json"` only for explicit `dall-e-2` and `dall-e-3` IDs; + omit it for GPT image models and unknown or aliased IDs, which may return base64 data or an HTTP(S) URL. - Chat image generation accepts text only, returns the first native image, supplies no tools, and has no fallback to a dedicated endpoint. Dedicated generation and editing must not be described as the same capability. - Generated output is capped at 25 MiB. Preserve typed image data out of model-facing tool text and persist it according to From a6fb01cfb41326c112dcad2acb56b82cc75ea531 Mon Sep 17 00:00:00 2001 From: Arturo Carretero Calvo <10163049+ArtCC@users.noreply.github.com> Date: Thu, 24 Sep 2026 07:38:31 +0200 Subject: [PATCH 2/2] Use xcode-27 for iOS CI - Update the iOS test job runner from macos-26 to xcode-27 - Preserve the workflow file newline at end of file --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98ffc541..3a4305d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ concurrency: jobs: test-ios: name: Build & Test (iOS) - runs-on: macos-26 + runs-on: xcode-27 steps: - uses: actions/checkout@v5 @@ -92,4 +92,4 @@ jobs: CODE_SIGN_IDENTITY="" \ CODE_SIGNING_REQUIRED=NO \ CODE_SIGNING_ALLOWED=NO \ - | xcpretty && exit ${PIPESTATUS[0]} \ No newline at end of file + | xcpretty && exit ${PIPESTATUS[0]}