From 0258af84a0860dfa783cbd54b8817f0fb8b43a4c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 27 May 2026 04:05:59 +0000 Subject: [PATCH 1/2] Map ChatEngine errors to HTTP status on Anthropic and Open Responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The non-streaming /v1/chat/completions endpoint was already taught (PR #863) to translate ChatEngine.EngineError into its intended HTTP status: 404 when the requested model isn't installed, 503 when the provider that handles it is unavailable, etc. The matching Anthropic /messages and Open Responses /responses endpoints, both of which run the same ChatEngine, were still hardcoded to 500 on every error. That meant API clients calling those endpoints couldn't distinguish 'install the model' from 'restart the local server' from 'the remote provider is offline,' and the WorkView error classifier — which uses the HTTP status to decide what to show the user — defaulted to the catch-all 5xx message. Mirror the /v1/chat/completions pattern in both handlers: * Translate EngineError -> httpStatus on the wire response head. * Pick the appropriate provider-native error_type/code (Anthropic's invalid_request_error vs api_error, Open Responses' two-tier code field) based on whether the status is 4xx or 5xx. * Log responseStatus: Int(status.code) instead of hardcoded 500 so the Insights tab matches the wire response. Other failure modes (network errors, decoder errors) still fall through to .internalServerError exactly as before; only EngineError gets the new mapping. The wire body shape is unchanged — same AnthropicError / OpenResponsesErrorResponse types, same JSON keys. Co-authored-by: Michael Meding --- .../OsaurusCore/Networking/HTTPHandler.swift | 50 ++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/Packages/OsaurusCore/Networking/HTTPHandler.swift b/Packages/OsaurusCore/Networking/HTTPHandler.swift index 0f8877549..008642f71 100644 --- a/Packages/OsaurusCore/Networking/HTTPHandler.swift +++ b/Packages/OsaurusCore/Networking/HTTPHandler.swift @@ -4089,7 +4089,25 @@ final class HTTPHandler: ChannelInboundHandler, Sendable { finishReason: .toolCalls ) } catch { - let errorResp = AnthropicError(message: error.localizedDescription, errorType: "api_error") + // Mirror the /v1/chat/completions mapping from PR #863: an + // unknown model id is a 404 client error, a missing provider + // is 503, etc. Without this, every misconfigured request + // looks like a 500 server fault and the WorkView error + // classifier / API consumers can't give actionable feedback. + let status: HTTPResponseStatus + let anthropicErrorType: String + if let engineError = error as? ChatEngine.EngineError { + status = HTTPResponseStatus(statusCode: engineError.httpStatus) + anthropicErrorType = engineError.httpStatus >= 500 ? "api_error" : "invalid_request_error" + } else { + status = .internalServerError + anthropicErrorType = "api_error" + } + let errorMessage = + (error as? ChatEngine.EngineError)?.errorDescription + ?? error.localizedDescription + let errorResp = AnthropicError( + message: errorMessage, errorType: anthropicErrorType) let errorJson = (try? JSONEncoder().encode(errorResp)) .map { String(decoding: $0, as: UTF8.self) } @@ -4098,9 +4116,10 @@ final class HTTPHandler: ChannelInboundHandler, Sendable { headers.append(contentsOf: cors) let headersCopy = headers let body = errorJson + let wireStatus = status hop { - var responseHead = HTTPResponseHead(version: head.version, status: .internalServerError) + var responseHead = HTTPResponseHead(version: head.version, status: wireStatus) var buffer = ctx.value.channel.allocator.buffer(capacity: body.utf8.count) buffer.writeString(body) var nioHeaders = HTTPHeaders() @@ -4121,7 +4140,7 @@ final class HTTPHandler: ChannelInboundHandler, Sendable { path: "/messages", userAgent: logUserAgent, requestBody: logRequestBody, - responseStatus: 500, + responseStatus: Int(status.code), startTime: logStartTime, model: logModel, errorMessage: error.localizedDescription @@ -4696,7 +4715,25 @@ final class HTTPHandler: ChannelInboundHandler, Sendable { finishReason: .toolCalls ) } catch { - let errorResp = OpenResponsesErrorResponse(code: "api_error", message: error.localizedDescription) + // Mirror the /v1/chat/completions mapping from PR #863: + // unknown-model is 404, service-unavailable is 503, etc., + // so API consumers and the WorkView error classifier see + // the same actionable status codes here as on the OpenAI + // chat-completions endpoint. + let status: HTTPResponseStatus + let errorCode: String + if let engineError = error as? ChatEngine.EngineError { + status = HTTPResponseStatus(statusCode: engineError.httpStatus) + errorCode = engineError.httpStatus >= 500 ? "api_error" : "invalid_request_error" + } else { + status = .internalServerError + errorCode = "api_error" + } + let errorMessage = + (error as? ChatEngine.EngineError)?.errorDescription + ?? error.localizedDescription + let errorResp = OpenResponsesErrorResponse( + code: errorCode, message: errorMessage) let errorJson = (try? JSONEncoder().encode(errorResp)) .map { String(decoding: $0, as: UTF8.self) } @@ -4705,9 +4742,10 @@ final class HTTPHandler: ChannelInboundHandler, Sendable { headers.append(contentsOf: cors) let headersCopy = headers let body = errorJson + let wireStatus = status hop { - var responseHead = HTTPResponseHead(version: head.version, status: .internalServerError) + var responseHead = HTTPResponseHead(version: head.version, status: wireStatus) var buffer = ctx.value.channel.allocator.buffer(capacity: body.utf8.count) buffer.writeString(body) var nioHeaders = HTTPHeaders() @@ -4728,7 +4766,7 @@ final class HTTPHandler: ChannelInboundHandler, Sendable { path: "/responses", userAgent: logUserAgent, requestBody: logRequestBody, - responseStatus: 500, + responseStatus: Int(status.code), startTime: logStartTime, model: logModel, errorMessage: error.localizedDescription From dae28589d29c773181f1a8d3c7b2fb2913639458 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 27 May 2026 05:08:34 +0000 Subject: [PATCH 2/2] Fix flake: skip ModelManager launch-time HF fetch under xctest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ModelManager.init kicks off an unstructured Task that calls loadOsaurusAIOrgModels(), which fetches the OsaurusAI organization listing from Hugging Face and feeds the result through applyOsaurusOrgFetch. The unit-test runner repeatedly constructs ModelManager() to drive applyOsaurusOrgFetch directly. The background launch-time fetch races with those test calls — whichever finishes last wins, and the merge result is non-deterministic. That's the root cause of the flaky ModelManagerSuggestedTests failures seen across many of the recent PR CI runs (applyOsaurusOrgFetch_dropsStaleAutoFetched OnReapply, applyOsaurusOrgFetch_addsNewEntriesAfterCurated, etc.). Gate the launch-time fetch on a small isRunningInTestEnvironment helper that checks for any of XCTestConfigurationFilePath, XCTestBundlePath, or XCTestSessionIdentifier in the process environment. Those variables are only present inside an xctest host process; production app launches still get the HF fetch exactly as before. This is a network call, so removing it under tests also has the side benefit of making the test suite work offline / on hermetic CI runners. Co-authored-by: Michael Meding --- .../Managers/Model/ModelManager.swift | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/Packages/OsaurusCore/Managers/Model/ModelManager.swift b/Packages/OsaurusCore/Managers/Model/ModelManager.swift index c87d515f6..dc6595695 100644 --- a/Packages/OsaurusCore/Managers/Model/ModelManager.swift +++ b/Packages/OsaurusCore/Managers/Model/ModelManager.swift @@ -188,7 +188,27 @@ final class ModelManager: NSObject, ObservableObject { // Pull the OsaurusAI HF org listing once on launch so newly published // models surface in the Recommended tab without requiring a code push. - Task { [weak self] in await self?.loadOsaurusAIOrgModels() } + // + // The unit-test runner constructs `ModelManager()` repeatedly to drive + // `applyOsaurusOrgFetch` directly. If the launch-time HF fetch races + // with those test calls, whichever finishes last wins and the merge + // result is non-deterministic — that's the regression class behind + // `ModelManagerSuggestedTests/applyOsaurusOrgFetch_*` flaking in CI. + // Skip the background fetch under XCTest; production launches still + // get it because `XCTestConfigurationFilePath` is only set inside + // a test host. + if !Self.isRunningInTestEnvironment { + Task { [weak self] in await self?.loadOsaurusAIOrgModels() } + } + } + + /// True when the current process was launched by xctest. Used to gate + /// network-touching launch-time side effects so tests can drive the + /// affected code paths deterministically. + nonisolated private static var isRunningInTestEnvironment: Bool { + ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil + || ProcessInfo.processInfo.environment["XCTestBundlePath"] != nil + || ProcessInfo.processInfo.environment["XCTestSessionIdentifier"] != nil } // MARK: - Public Methods