Skip to content
Draft
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
22 changes: 21 additions & 1 deletion Packages/OsaurusCore/Managers/Model/ModelManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 44 additions & 6 deletions Packages/OsaurusCore/Networking/HTTPHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand All @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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) }
Expand All @@ -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()
Expand All @@ -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
Expand Down
Loading