Unified LLM service access layer — one API to access 325 AI providers
The Java binding goes through the aimux-ffi C ABI via JNA — no native
toolchain is needed at build time. Artifact: ai.arcships:aimux-java:0.2.1,
Java 8+ (compiled with --release 8). See RFC-0013
for the design.
Maven Central (publishing):
implementation("ai.arcships:aimux-java:0.2.1")JNA loads aimux_ffi by name — provide the native library
(libaimux_ffi.so / libaimux_ffi.dylib / aimux_ffi.dll) from
GitHub Releases on the JNA search
path (-Djava.library.path=... or LD_LIBRARY_PATH).
Shared reference — parameter tables, result shapes, factory functions, and the feature coverage matrix — lives in the API overview.
try (Model model = Model.openaiWithBase("sk-...", "gpt-4o", "http://localhost:3000")) {
String result = model.generateText("\"What is Rust?\"");
}All 250 registry-backed OpenAI-compatible providers are reachable by name;
ProviderName holds the constants:
Scope:
provider(name)covers only the 250 registry OpenAI-compatible providers; Anthropic/Google/multimodal/local → typed factories (Model.anthropic(apiKey, modelId)); custom endpoints → base-URL variant. Full list: providers.md.
import ai.arcships.aimux.Model;
import ai.arcships.aimux.ProviderName;
// 推荐:ProviderName.GROQ 常量(类型检查 + 补全)
try (Model model = Model.providerFromEnv(ProviderName.GROQ, "llama-3.3-70b")) {
String result = model.generateText("\"Hello\"");
}
// 字符串形式同样可用 + 可选 config JSON ({"base_url": "..."}):
try (Model model = Model.provider("groq", "sk-...", "llama-3.3-70b", null)) {
String result = model.generateText("\"Hello\"");
}deepseek(apiKey, modelId) remains as a shortcut (registry-backed).
Unknown names throw an error listing the available providers.
Engine and binding failures throw an AimuxException subclass hierarchy
(OpenAI Java / Vercel AI SDK style — instanceof, not stringly code checks):
RuntimeException
└── AimuxException
├── ProviderError / HttpError / JsonError / StreamError / ToolError
├── InvalidArgumentError / InvalidPromptError
├── RateLimitedError // status 429, retryMs
├── AuthenticationError // status 401
├── TokenExpiredError
├── ModelNotFoundError / NoSuchModelError
├── UnsupportedError / UnknownProviderError
├── APICallError / TimeoutError
├── RequestAbortedError
└── OtherError
Every instance has:
| Field | Meaning |
|---|---|
getMessage() |
human-readable text from C |
getCode() |
AimuxErrorCode value 0–19 (matches aimux-error.h) |
getStatus() |
HTTP status, or -1 |
getRetryMs() |
rate-limit hint, or -1 (0 = retry now) |
Transport: fallible C calls take a trailing AimuxError *err and return
0 / NULL on failure. The Java binding maps that into the hierarchy via
AimuxException.fromC(AimuxCError) — not JSON error envelopes on the
main path. Subclasses are nested under AimuxException (e.g.
AimuxException.RateLimitedError).
import ai.arcships.aimux.AimuxException;
import ai.arcships.aimux.AimuxException.AuthenticationError;
import ai.arcships.aimux.AimuxException.RateLimitedError;
import ai.arcships.aimux.Model;
try (Model model = Model.openai("sk-...", "gpt-4o")) {
model.generateText("\"hi\"");
} catch (RateLimitedError e) {
// e.getRetryMs(), e.getStatus() == 429
} catch (AuthenticationError e) {
// e.getStatus() == 401
} catch (AimuxException e) {
// any engine / binding failure
}Stream terminal failures also throw (there is no C on_error callback). The
raw streamText onError parameter is retained for API compatibility (e.g.
typed decode issues) but is not used for C-level failures.
Two layers, mirroring the Kotlin binding:
- Raw layer (
Model,EmbeddingModel,SpeechModel, …) — JNA → C ABI, JSON strings in and out. Failures throw typedAimuxExceptionsubclasses from the CAimuxErrorout-param (handle0/ pointerNULL). - Typed layer (
TypedModel,Types,MultimodalTypes) — Jackson POJOs with the same wire format as all other bindings.TypedModeldecodes results into typed objects; engine errors propagate asAimuxException.
Serialization uses JsonInclude.Include.NON_NULL (null fields omitted on
encode; zero values and empty collections are retained).
// raw layer — JSON in, JSON out
try (Model model = Model.openai("sk-...", "gpt-4o")) {
String result = model.generateText("\"What is Rust?\"");
}
// typed layer — typed objects
try (TypedModel model = TypedModel.openai("sk-...", "gpt-4o")) {
Types.GenerateTextResult result = model.generateText("What is Rust?");
System.out.println(result.getText());
System.out.println(result.getUsage().getInputTokens().getTotal());
}TypedModel.generateText overloads: (String prompt),
(String prompt, GenerateTextOptions options),
(List<ModelMessage> messages),
(List<ModelMessage> messages, GenerateTextOptions options).
Parameters, return value, and the
raw.contentvariants are documented in the API overview.
// raw layer — callback-based (JSON parts), blocks the calling thread
try (Model model = Model.openai("sk-...", "gpt-4o")) {
model.streamText("\"Write a haiku\"", null,
part -> System.out.println(part), // onPart (String JSON)
() -> {}, // onDone
err -> System.err.println(err)); // onError
}
// raw layer — pull-based (lazy Stream<String>)
model.streamTextStream("\"Write a haiku\"").forEach(System.out::println);
// typed layer — typed StreamPart objects
try (TypedModel model = TypedModel.openai("sk-...", "gpt-4o")) {
model.streamTextStream("\"Write a haiku\"")
.forEach(part -> System.out.println(part)); // Types.StreamPart
}TypedModel.streamText (callback) and streamTextStream (pull) both have
(String prompt), (String prompt, options), (List<ModelMessage> messages),
and (List<ModelMessage> messages, options) overloads.
Stream part variants are documented in the API overview.
| API | Signature |
|---|---|
TypedModel.openai / TypedModel.anthropic |
static TypedModel openai(String apiKey, String modelId) (+ openaiWithBase / anthropicWithBase with baseUrl) |
TypedModel.of |
static TypedModel of(Model model) — wrap an existing raw Model (does not own the handle) |
generateText |
GenerateTextResult generateText(...) — 4 overloads (see above) |
streamText |
callback-based: (prompt/options, Consumer<StreamPart> onPart, Runnable onDone, Consumer<String> onError) |
streamTextStream |
Stream<StreamPart> streamTextStream(...) — 4 overloads, pull-based |
TypedModel is Closeable (owning factories must be closed with
try-with-resources); engine errors surface as AimuxException.
try (EmbeddingModel model = EmbeddingModel.openai("sk-...", "text-embedding-3-small")) {
String result = model.embed("[\"hello\", \"world\"]");
}
// result: {"embeddings":[[0.1,0.2,...],[0.3,0.4,...]],"usage":{"tokens":5}, ...}| Factory | Providers |
|---|---|
EmbeddingModel.openai / openaiWithBase |
OpenAI |
EmbeddingModel.cohere / cohereWithBase |
Cohere |
EmbeddingModel.google / googleWithBase |
embed(String valuesJson) / embed(String valuesJson, String optsJson).
try (SpeechModel model = SpeechModel.openai("sk-...", "tts-1")) {
String opts = new JSONObject()
.put("text", "Hello")
.put("voice", "alloy")
.put("output_format", "mp3")
.toString();
String result = model.generate(opts);
}Factories: SpeechModel.openai / openaiWithBase.
try (TranscriptionModel model = TranscriptionModel.openai("sk-...", "whisper-1")) {
String result = model.generate(base64Audio, "audio/wav");
}Factories: TranscriptionModel.openai / openaiWithBase.
generate(String audioBase64, String mediaType) /
generate(String audioBase64, String mediaType, String optsJson).
try (ImageModel model = ImageModel.openai("sk-...", "dall-e-3")) {
String result = model.generate("{\"prompt\":\"an otter\",\"n\":1}");
}Factories: ImageModel.openai / openaiWithBase, ImageModel.google /
googleWithBase.
try (VideoModel model = VideoModel.google("sk-...", "veo-3.0")) {
String result = model.generate("{\"prompt\":\"a sunset\",\"n\":1}");
}Factories: VideoModel.google / googleWithBase.
try (RerankingModel model = RerankingModel.cohere("sk-...", "rerank-v3.0")) {
String result = model.rerank("{\"query\":\"...\",\"documents\":{...},\"top_n\":2}");
}Factories: RerankingModel.cohere / cohereWithBase.
try (SearchModel model = SearchModel.tavily("sk-...")) {
String result = model.search("{\"query\":\"What is Rust?\",\"max_results\":5}");
}
// result: {"results":[{"title":"Rust",...}],"answer":"Rust is a systems language."}Factories: SearchModel.tavily / tavilyWithBase.
try (Files files = Files.openai("sk-...")) {
String result = files.uploadFile(base64Data, "application/pdf");
}
// result: {"provider_reference":{"openai":"file-abc"}, ...}Factories: Files.openai / openaiWithBase.
uploadFile(String dataBase64, String mediaType) /
uploadFile(String dataBase64, String mediaType, String optsJson).
Types.java declares the typed text/tool surface (all types nested in
Types): TokenUsage, Usage, FinishReason, ResponseMetadata, ToolCall,
FunctionTool, ProviderTool, Tool (sealed), ToolChoice (sealed, with
custom serializer for the scalar-or-object wire form), ContentPart (sealed),
MessageContent (sealed), ModelMessage, GenerateTextOptions,
FileBytes / FileData, GenerateContent, GenerateResult,
GenerateTextResult, StreamPart (sealed). All sealed hierarchies serialize
in the wrapper-object wire form (e.g. {"TextDelta":{...}}).
MultimodalTypes.java declares the typed multimodal surface:
EmbeddingCallOptions / EmbeddingResult, SpeechCallOptions / SpeechResult,
ImageCallOptions / ImageResult, TranscriptionCallOptions /
TranscriptionResult, RerankingCallOptions / RerankingResult,
VideoCallOptions / VideoResult, SearchCallOptions / SearchResult,
UploadFileCallOptions / UploadFileResult. Sealed unions (AudioData,
ImageOutputs, VideoData) use custom Jackson serializers with the
externally-tagged wire form. Response types (EmbeddingResponse, etc.) match
the aimux-core .ts wire definitions exactly ({headers, body, ...}).
- All fallible raw APIs (
Model.generateText, multimodal, …): C sentinel failure fillsAimuxError *; Java throwsAimuxException.fromC(typed subclasses — see Errors above). Success payloads are plain result JSON strings, not error envelopes. - Typed text layer (
TypedModel): engine failures throw the same hierarchy; local decode failures throwAimuxException/InvalidArgumentError.
Full multimodal surface — text generation, streaming, embedding, TTS, STT, image, video, reranking, search, and file upload. Verified by 58 tests across 7 suites (mock-server E2E + contract wire-format round-trips, no real network). See the coverage matrix.
# First build the aimux-ffi .so
cargo build -p aimux-ffi --release
cd bindings/java
export JAVA_HOME=... # JDK 9+ (bytecode targets Java 8 via --release 8)
export LD_LIBRARY_PATH="$(pwd)/../../target/release:${LD_LIBRARY_PATH}"
gradle test