Skip to content

Latest commit

 

History

History
228 lines (182 loc) · 11.5 KB

File metadata and controls

228 lines (182 loc) · 11.5 KB

aimux · Swift API

Unified LLM service access layer — one API to access 325 AI providers

Swift wraps the Rust core through the aimux-ffi C ABI (module CAimuxFFI), with ARC-managed model handles.

Install

Add the package via SPM (the git tag is the version):

.package(url: "https://github.com/arcships/aimux", from: "0.3.0"),
.target(name: "YourApp", dependencies: [.product(name: "Aimux", package: "aimux")])

The binding loads libaimux_ffi.dylib at runtime — make it available via pkg-config (aimux-ffi.pc) or put the prebuilt library from GitHub Releases on the loader path (DYLD_LIBRARY_PATH / LD_LIBRARY_PATH).

Quick Start

import Aimux

let model = try Model.openai(apiKey: "sk-...", modelId: "gpt-4o", baseUrl: "http://localhost:3000")
let result = try model.generateText(prompt: "\"What is Rust?\"")
print(result)

Providers

All 251 registry-backed OpenAI-compatible providers are reachable by name; ProviderName is an enum with one case per provider:

Scope: provider(name) covers only the 251 registry OpenAI-compatible providers; Anthropic/Google/multimodal/local → typed factories (Model.anthropic(apiKey:modelId:)); custom endpoints → base-URL variant. Full list: providers.md.

// 推荐:ProviderName enum case(类型检查 + 补全)
let model = try Model.provider(name: ProviderName.Groq.rawValue, modelId: "llama-3.3-70b")
let result = try model.generateText(prompt: "\"Hello\"")

// 字符串形式同样可用 + 可选 config JSON ({"base_url": "..."}):
let model2 = try Model.provider(name: "groq", apiKey: "sk-...", modelId: "llama-3.3-70b")

Unknown names throw .noSuchProvider (payload: the provider id); valid names come from the generated ProviderName enum.

Text Generation

generateText(prompt:options:) returns the raw GenerateResult JSON string.

import Aimux

let model = try Model.openai(apiKey: "sk-...", modelId: "gpt-4o")
// prompt is a JSON string; JSON-quote plain text prompts
let result = try model.generateText(prompt: "\"What is Rust?\"")
// or pass multi-role messages
let result2 = try model.generateText(prompt: #"[{"role":"user","content":"Hello"}]"#)
print(result2)

Parameters, return value, and the raw.content variants are documented in the API overview.

Streaming Generation

streamText(prompt:options:onPart:onDone:onError:) delivers each part as a StreamPart JSON string.

model.streamText(prompt: "\"Write a haiku\"") { part in
    print(part) // StreamPart JSON string
} onDone: {
    print("[done]")
} onError: { error in
    print("error: \(error)")
}

API Surface

API Signature Description
Model.openai / Model.anthropic static func openai(apiKey: String, modelId: String) throws -> Model Create a model (official base URL)
Model.openai / Model.anthropic static func openai(apiKey: String, modelId: String, baseUrl: String) throws -> Model Create a model (custom base URL)
generateText func generateText(prompt: String, options: String? = nil) throws -> String Non-streaming; returns GenerateResult JSON
streamText func streamText(prompt: String, options: String? = nil, onPart: @escaping (String) -> Void, onDone: @escaping () -> Void, onError: @escaping (any Error) -> Void) Streaming via push callbacks
streamTextAsync func streamTextAsync(prompt: String, options: String? = nil) -> AsyncThrowingStream<String, Error> Streaming as an AsyncSequence
generate func generate(prompt: String, options: [String: Any]? = nil) throws -> [String: Any] Convenience: parses generateText into a dictionary
Model.initRecording static func initRecording(dir: String) throws Start JSONL recording; throws RecordingError (.initFailed / .openFile / .spawn) when the recorder cannot be constructed; the previous recorder stays in place
Model.recordingTryFlush static func recordingTryFlush() throws Checked recorder flush; throws RecordingError (own type, see below). Legacy recordingFlush() stays and never reports

Errors

Two independent aimux error types, mirroring two Rust types; neither inherits from the other, both share only Error: AimuxError (core AiMuxError) and RecordingError (recorder). C ABI codes 200...206 have no Swift type — see "C ABI failures" below.

Every fallible C function returns an opaque aimux_error_t * (OpaquePointer?): NULL = success (the result is in the trailing out-parameter), non-NULL = failure. One unified code selects AimuxError (1...13), RecordingError (100...105), or a C ABI failure (200...206). The three decoders enforce the range expected by each call and restore the Swift error type; 200...206 collapses to DecodingError.dataCorrupted. Every path copies its strings (freed with aimux_free_string) and then releases the returned error with aimux_error_free exactly once. A code outside the C enum is a header/library mismatch and yields the invariant DecodingError.dataCorrupted("aimux ffi: <context>: <message>"). Errors are not handles: aimux_drop_handle never sees one.

AimuxError is a structured Swift Error enum mapped from aimux_error_code_t (see aimux-error.h).

Case C code Notes
.jsonParse AIMUX_E_JSON_PARSE (2) JSON parse/serialize
.invalidResponseData AIMUX_E_INVALID_RESPONSE_DATA (3) Malformed response / stream data
.tool AIMUX_E_TOOL (4) Tool-related failure
.invalidArgument AIMUX_E_INVALID_ARGUMENT (5) Bad argument
.invalidPrompt AIMUX_E_INVALID_PROMPT (6) Bad prompt JSON
.tokenExpired AIMUX_E_TOKEN_EXPIRED (7) Expired token; status 401
.unsupportedFunctionality AIMUX_E_UNSUPPORTED_FUNCTIONALITY (8) Unsupported feature
.noSuchModel AIMUX_E_NO_SUCH_MODEL (9) Registry miss
.noSuchProvider AIMUX_E_NO_SUCH_PROVIDER (10) Unknown provider id
.apiCall AIMUX_E_API_CALL (11) Every HTTP-shaped failure; branch on status (401 auth, 404 model, 429 rate limit)
.timeout AIMUX_E_TIMEOUT (12) Request timed out
.aborted AIMUX_E_ABORTED (13) Request aborted
.other AIMUX_E_OTHER (1) Unclassified core error

There are no binding-local cases: only aimux-core produces an AimuxError. Un-encodable typed input (typed streamText prompt/options) surfaces as the EncodingError JSONEncoder threw, undecodable library output as the DecodingError JSONDecoder threw. A code outside the expected range is a header/library mismatch and fails with DecodingError.dataCorrupted from the decoder, not an error type.

Every case carries message, status (Int?nil when C reports no status), retryMs (Int64?nil if none; 0 = retry now) and retryable. Three cases carry a typed payload as extra associated values: .apiCall(providerCode:providerMessage:requestId:responseBody:) (all optional), .noSuchModel(modelId:modelType:) and .noSuchProvider(providerId:); the same-named computed properties return nil on every other case. e.code returns the mapped aimux_error_code_t constant as Int32.

Recording errors are a separate type. Model.initRecording(dir:) and Model.recordingTryFlush() throw RecordingErrorstruct RecordingError: Error, Equatable { code: Code; message: String } with Code = .initFailed, .openFile, .spawn, .writerGone, .flushTimeout, .write (mirrors aimux_error_code_t in aimux-error.h; the first three come from initRecording, the last three from a flush). It is not a case of AimuxError and never appears in the code table above; catch it with catch let e as RecordingError.

C ABI failures are native errors, not an aimux type. The binding is a consumer of the C ABI: it catches misuse before the C call and maps codes 200...206 onto Swift's own errors.

Failure Swift
Bad raw JSON (prompt / options / configJson / values / recordingsJsonl strings, on every raw-JSON entry) DecodingError.dataCorrupted naming the parameter, thrown by the binding (JSONSerialization pre-check) before the C call. Optional parameters follow the FFI's "blank means default" rule; required ones reject an empty string
Use-after-close (TranscriptionSession, the only closeable handle) DecodingError.dataCorrupted "aimux: transcription session is closed" from pushAudio / inputDone / nextPart. Catchable, like Go's ErrClosed and Dart's StateError; defer { session.close() } can never crash the host app. Deliberately not AimuxTranscriptionEndedError — a pump loop that breaks on "ended" must not read a transcript its own close() truncated as complete
initRecordingRing(cap: 0) AimuxError.invalidArgument — the value goes to C and aimux-core classifies it (AIMUX_E_INVALID_ARGUMENT, "cap: must be > 0"), so a Swift caller sees exactly what a C caller sees. The function is throws
router([]) DecodingError.dataCorrupted "aimux ffi: router: …" — C's zero-children failure, surfaced unchanged. Not a trap: the array is as likely to come from a .filter as from a literal, and router already throws
C code 200...206 (re-entrant call, NULL arg, invalid UTF-8, marshalling / callback / internal) DecodingError.dataCorrupted "aimux ffi: <context>: <message>" — a binding/library invariant, a correct binding never triggers it

No caller-supplied value traps the process. The binding contains exactly one preconditionFailure, in initLogging(level:), and it is unreachable: the only failure aimux_init_logging reports is a non-UTF-8 level, which a Swift String cannot produce (NULL and an unrecognized level string are both accepted — they fall back to the default).

Accessors:

do {
    _ = try model.generateText(prompt: "\"hi\"")
} catch let e as AimuxError {
    print(e.message, e.status, e.retryMs)
    if case .apiCall = e, e.status == 429, let ms = e.retryMs {
        // rate limited — back off `ms`
    }
}

Streaming: aimux_stream_text returns NULL after on_done, or an error object on failure (no on_done, no C onError callback). The Swift push API's onError is (any Error) -> Void and receives the decoded error unchanged (AimuxError, or DecodingError for bad raw JSON / a C ABI invariant); streamTextAsync throws the same.

Types

bindings/swift/Sources/Aimux/Types.swift declares lightweight Codable types mirroring the shared JSON shape — usable with the JSON-string APIs:

JSONValue (recursive JSON enum with stringValue / boolValue / doubleValue / intValue / arrayValue / objectValue accessors), Role, FinishReasonUnified, ReasoningEffort, FinishReason, TokenUsage, Usage, ResponseMetadata, Warning, FunctionTool, ProviderTool, Tool, ToolChoice, ResponseFormat, ContentPart, MessageContent, ModelMessage, ModelPrompt, ToolCall, FileBytes, FileData, GenerateContent, GenerateResult, GenerateTextResult, GenerateTextOptions, StreamPart (all Codable, Equatable).

Example:

let data = result.data(using: .utf8)!
let decoded = try JSONDecoder().decode(Usage.self, from: data)
print(decoded.inputTokens.total ?? 0)

Coverage

Text generation and streaming are supported. Multimodal features (embedding, TTS, STT, image, video, rerank, search, files) are reachable only through the raw C ABI until the wrappers are extended — see the coverage matrix.