Skip to content

Commit b274520

Browse files
committed
server: simplify ServerLauncher dispatch to one primitive; rename flag to --openai-compat
Per review: collapse the dispatch to a single pure helper. withoutFlag(args, flag) strips the selector and returns a (possibly shorter) array; main() selects the mode purely by whether that shortened the list (present iff result is smaller), so the separate selectsOpenAiCompat method + its baked-in constant are gone. The helper takes the flag as a parameter, so it is general and testable independent of the flag's meaning. Also rename the selector --open-ai-compat -> --openai-compat ("OpenAI" is one word, matching the brand and the codebase's oaicompat / OpenAiCompatServer); constant OPEN_AI_COMPAT_FLAG -> OPENAI_COMPAT_FLAG. Tests rewritten around withoutFlag: the length-change selection signal (shorter iff present, same length iff absent, position-independent) plus stripping behaviour (strips all occurrences, preserves order, no-op when absent, empty). 7 pass. Verified at runtime: `ServerLauncher --openai-compat -m model --port 8974` routes to OpenAiCompatServer (/ -> invalid_request_error) and shuts down cleanly; no-flag routes to NativeServer. README + CLAUDE.md + pom updated; spotless + javadoc clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HL7d4uQ3cKR5HwYFPvZvv7
1 parent c288e43 commit b274520

5 files changed

Lines changed: 51 additions & 56 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -851,10 +851,10 @@ If the local check passes (`BUILD SUCCESS`), the `mvn package` job in
851851

852852
### Two server modes (`OpenAiCompatServer` vs `NativeServer`)
853853

854-
The library exposes **two** ways to serve a model over HTTP, on two different transports. The fat jar's `Main-Class` is `server.ServerLauncher`, a tiny dispatcher: it runs `OpenAiCompatServer` when `--open-ai-compat` is present (that marker is stripped, the rest forwarded) and the default `NativeServer` otherwise. Both mains are also runnable directly by class name via `java -cp`. The two modes:
854+
The library exposes **two** ways to serve a model over HTTP, on two different transports. The fat jar's `Main-Class` is `server.ServerLauncher`, a tiny dispatcher: it runs `OpenAiCompatServer` when `--openai-compat` is present (that marker is stripped, the rest forwarded) and the default `NativeServer` otherwise. Both mains are also runnable directly by class name via `java -cp`. The two modes:
855855

856856
1. **`server.OpenAiCompatServer` (Java transport).** OpenAI/Ollama/Anthropic-compatible JSON API on the JDK's `com.sun.net.httpserver`, driving the compiled server *core* over JNI. Embeddable, no extra dependency, and it can share/reuse a `LlamaModel`. It serves **no** static assets — its `/` route is a 404, so **no WebUI**. It has its own `main` (run via `java -cp <jar> net.ladenthin.llama.server.OpenAiCompatServer …`); its CLI (`OpenAiServerCli`) maps a curated flag subset (`-m/-c/-b/-ub/-ngl/-t/-tb/-ctk/-ctv/--jinja/--chat-template-kwargs/--host/--port/--parallel/--mmproj/--api-key/--embedding/--reranking`).
857-
2. **`server.NativeServer` (native transport) — the default fat-jar server (when `--open-ai-compat` is absent).** Runs the **full upstream `llama_server`** (via `patches/0006` + `native_server.cpp`) inside `libjllama`, forwarding the raw llama-server argv verbatim — so **every** llama-server flag works and the **embedded WebUI is served** (when the assets are compiled in; CI's released jars have them, local `cmake` builds use the empty-asset stub). It is an **independent lifecycle** (loads its own model from the argv, like `llama-server.exe`; owns the process's llama backend + stderr logging while running), **single-instance per process** (upstream keeps shutdown state in file-scope globals), and **not available on Android** (the `subprocess.h` guard). Reusing an already-loaded `LlamaModel`'s context is a documented TODO. `libjllama` loading anywhere a JVM runs is what makes this "no separate `llama-server.exe`" possible.
857+
2. **`server.NativeServer` (native transport) — the default fat-jar server (when `--openai-compat` is absent).** Runs the **full upstream `llama_server`** (via `patches/0006` + `native_server.cpp`) inside `libjllama`, forwarding the raw llama-server argv verbatim — so **every** llama-server flag works and the **embedded WebUI is served** (when the assets are compiled in; CI's released jars have them, local `cmake` builds use the empty-asset stub). It is an **independent lifecycle** (loads its own model from the argv, like `llama-server.exe`; owns the process's llama backend + stderr logging while running), **single-instance per process** (upstream keeps shutdown state in file-scope globals), and **not available on Android** (the `subprocess.h` guard). Reusing an already-loaded `LlamaModel`'s context is a documented TODO. `libjllama` loading anywhere a JVM runs is what makes this "no separate `llama-server.exe`" possible.
858858

859859
### Native Helper Architecture
860860

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ Inference of Meta's LLaMA model (and others) in pure C/C++.
107107
- **Infilling** (fill-in-the-middle) for code models.
108108
- **Tokenize / detokenize** and **JSON-schema → grammar** conversion.
109109
- **Raw JSON endpoint handlers** mirroring the upstream llama.cpp HTTP server (`/completions`, `/v1/completions`, `/embeddings`, `/infill`, `/tokenize`, `/detokenize`).
110-
- **Two runnable HTTP server modes, one fat-jar entry.** The fat jar's `Main-Class` is `ServerLauncher`, which dispatches on the `--open-ai-compat` flag. Without it, `java -jar …-jar-with-dependencies.jar -m model.gguf --port 8080` runs the full upstream llama.cpp server (embedded **WebUI**, every llama-server flag forwarded) hosted inside `libjllama` over JNI — no separate `llama-server.exe`. With it, `java -jar … --open-ai-compat --model model.gguf --port 8080` runs the Java-transport, zero-extra-dependency **OpenAI-compatible** server (`OpenAiCompatServer`, streaming SSE) instead. Both are also runnable directly by class name via `java -cp … net.ladenthin.llama.server.{NativeServer,OpenAiCompatServer}`.
110+
- **Two runnable HTTP server modes, one fat-jar entry.** The fat jar's `Main-Class` is `ServerLauncher`, which dispatches on the `--openai-compat` flag. Without it, `java -jar …-jar-with-dependencies.jar -m model.gguf --port 8080` runs the full upstream llama.cpp server (embedded **WebUI**, every llama-server flag forwarded) hosted inside `libjllama` over JNI — no separate `llama-server.exe`. With it, `java -jar … --openai-compat --model model.gguf --port 8080` runs the Java-transport, zero-extra-dependency **OpenAI-compatible** server (`OpenAiCompatServer`, streaming SSE) instead. Both are also runnable directly by class name via `java -cp … net.ladenthin.llama.server.{NativeServer,OpenAiCompatServer}`.
111111
- **Model metadata** access (`getModelMeta()`) and **server management** (metrics, slot save/restore, runtime thread reconfiguration).
112112
- Pre-built native binaries for Linux (x86-64, aarch64), macOS (x86-64, arm64), and Windows (x86-64, x86); CUDA, Metal, and Vulkan supported via local build.
113113

@@ -649,12 +649,12 @@ try (LlamaModel model = new LlamaModel(modelParams);
649649
```
650650

651651
…or run it standalone. The fat jar's `Main-Class` is the `ServerLauncher` dispatcher, so add
652-
`--open-ai-compat` to select this Java server (the launcher strips that flag and forwards the rest);
652+
`--openai-compat` to select this Java server (the launcher strips that flag and forwards the rest);
653653
or name the class explicitly via `-cp`:
654654

655655
```bash
656-
# fat jar (bundles the native lib + Java deps) — select the Java server with --open-ai-compat
657-
java -jar target/llama-<version>-jar-with-dependencies.jar --open-ai-compat \
656+
# fat jar (bundles the native lib + Java deps) — select the Java server with --openai-compat
657+
java -jar target/llama-<version>-jar-with-dependencies.jar --openai-compat \
658658
--model models/Qwen3-0.6B-Q4_K_M.gguf --host 0.0.0.0 --port 8080 --n-gpu-layers 99
659659

660660
# or name the class explicitly (fat jar or plain library jar)
@@ -719,7 +719,7 @@ the **full upstream llama.cpp server, including its bundled Svelte WebUI**, use
719719
`net.ladenthin.llama.server.NativeServer`. It runs the real `llama_server` inside `libjllama` over
720720
JNI — no separate `llama-server.exe` — and **forwards the raw llama-server arguments verbatim**, so
721721
every flag works exactly as it does for the standalone binary. The fat jar runs it **by default**
722-
(when `--open-ai-compat` is absent), forwarding its args to the native server (pass `--help` for the
722+
(when `--openai-compat` is absent), forwarding its args to the native server (pass `--help` for the
723723
full llama-server option list):
724724

725725
```bash

llama/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1297,7 +1297,7 @@ SPDX-License-Identifier: MIT
12971297
Builds the fat jar-with-dependencies uber JAR: the library classes, the
12981298
default-platform native libs from src/main/resources, and all runtime Java
12991299
dependencies in one drop-on-classpath JAR, with ServerLauncher as the fat-jar
1300-
Main-Class (set below), which dispatches on an `open-ai-compat` selector flag: with it, runs
1300+
Main-Class (set below), which dispatches on an `openai-compat` selector flag: with it, runs
13011301
OpenAiCompatServer (Java OpenAI API); without it, the default NativeServer (native
13021302
server, embedded WebUI, all flags forwarded). Both mains stay runnable by class name via `java -cp <jar> …`. Off by
13031303
default; the CI `package` job activates it so the uber JAR rides along in the

llama/src/main/java/net/ladenthin/llama/server/ServerLauncher.java

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,63 +9,52 @@
99

1010
/**
1111
* Fat-jar entry point that dispatches to one of the two server modes based on a single selector
12-
* flag. With {@value #OPEN_AI_COMPAT_FLAG} present it runs {@link OpenAiCompatServer} (the
12+
* flag. With {@value #OPENAI_COMPAT_FLAG} present it runs {@link OpenAiCompatServer} (the
1313
* Java-transport, OpenAI-compatible JSON API); without it, {@link NativeServer} (the full native
1414
* llama.cpp server with embedded WebUI, the default).
1515
*
16-
* <p>Every other argument is forwarded verbatim to the chosen server; the {@value
17-
* #OPEN_AI_COMPAT_FLAG} marker itself is stripped so it never reaches either parser (it is not a
18-
* llama.cpp flag, and {@code llama_server} rejects unknown flags).</p>
16+
* <p>The dispatch uses a single primitive, {@link #withoutFlag(String[], String)}: it strips the
17+
* selector from the arguments (the flag is not a llama.cpp flag, and {@code llama_server} rejects
18+
* unknown flags), and the mode is chosen purely by whether that shortened the list — present iff the
19+
* result is smaller. Every other argument is forwarded verbatim.</p>
1920
*
2021
* <p><strong>Flag sets differ.</strong> {@link NativeServer} forwards <em>every</em> llama-server
2122
* flag to {@code llama_server}, whereas {@link OpenAiCompatServer}'s CLI ({@link OpenAiServerCli})
2223
* accepts a curated subset and rejects unknown flags — so native-only flags (e.g. {@code --ui},
23-
* {@code -fa}) cannot be combined with {@value #OPEN_AI_COMPAT_FLAG}.</p>
24+
* {@code -fa}) cannot be combined with {@value #OPENAI_COMPAT_FLAG}.</p>
2425
*
2526
* <p>Both underlying mains remain directly runnable by class name via {@code java -cp}; this
2627
* launcher is purely a convenience so a single {@code java -jar} covers both.</p>
2728
*/
2829
public final class ServerLauncher {
2930

3031
/** Selector flag: when present, run {@link OpenAiCompatServer} instead of the default {@link NativeServer}. */
31-
public static final String OPEN_AI_COMPAT_FLAG = "--open-ai-compat";
32+
public static final String OPENAI_COMPAT_FLAG = "--openai-compat";
3233

3334
private ServerLauncher() {}
3435

3536
/**
36-
* Dispatches to {@link OpenAiCompatServer#main(String[])} when {@value #OPEN_AI_COMPAT_FLAG} is
37-
* present (with that marker removed from the arguments), otherwise to
38-
* {@link NativeServer#main(String[])} with all arguments forwarded unchanged.
37+
* Dispatches to {@link OpenAiCompatServer#main(String[])} when {@value #OPENAI_COMPAT_FLAG} is
38+
* present (with that marker removed), otherwise to {@link NativeServer#main(String[])} with all
39+
* arguments forwarded unchanged. Selection is derived from whether stripping the flag shortened
40+
* the argument list.
3941
*
4042
* @param args the process arguments
4143
* @throws Exception if the selected server's {@code main} throws (it blocks until shutdown)
4244
*/
4345
public static void main(String[] args) throws Exception {
44-
if (selectsOpenAiCompat(args)) {
45-
OpenAiCompatServer.main(withoutFlag(args, OPEN_AI_COMPAT_FLAG));
46+
final String[] forwarded = withoutFlag(args, OPENAI_COMPAT_FLAG);
47+
if (forwarded.length != args.length) {
48+
OpenAiCompatServer.main(forwarded);
4649
} else {
4750
NativeServer.main(args);
4851
}
4952
}
5053

51-
/**
52-
* Whether the arguments request the OpenAI-compatible server via {@value #OPEN_AI_COMPAT_FLAG}.
53-
*
54-
* @param args the process arguments
55-
* @return {@code true} if the selector flag is present
56-
*/
57-
static boolean selectsOpenAiCompat(String[] args) {
58-
for (final String arg : args) {
59-
if (OPEN_AI_COMPAT_FLAG.equals(arg)) {
60-
return true;
61-
}
62-
}
63-
return false;
64-
}
65-
6654
/**
6755
* Returns a copy of {@code args} with every occurrence of {@code flag} removed, preserving the
68-
* order of the remaining arguments.
56+
* order of the remaining arguments. The result is shorter than {@code args} exactly when
57+
* {@code flag} was present — which is how {@link #main(String[])} selects the server mode.
6958
*
7059
* @param args the arguments
7160
* @param flag the flag token to strip

llama/src/test/java/net/ladenthin/llama/server/ServerLauncherTest.java

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,50 +12,56 @@
1212
import org.junit.jupiter.api.Test;
1313

1414
/**
15-
* Pure-Java unit tests for {@link ServerLauncher}'s dispatch logic (selector detection + flag
16-
* stripping). No server is started and no native library is required.
15+
* Pure-Java unit tests for {@link ServerLauncher}'s single dispatch primitive,
16+
* {@link ServerLauncher#withoutFlag(String[], String)}. Selection is derived from the length change
17+
* (result shorter iff the flag was present), so these tests cover both the stripping behaviour and
18+
* that selection signal. No server is started and no native library is required.
1719
*/
1820
public class ServerLauncherTest {
1921

22+
private static final String FLAG = ServerLauncher.OPENAI_COMPAT_FLAG;
23+
24+
// --- selection signal: shorter iff the flag was present ---
25+
2026
@Test
21-
public void selectsNativeByDefault() {
22-
assertThat(ServerLauncher.selectsOpenAiCompat(new String[] {"-m", "m.gguf", "--port", "8080"}), is(false));
27+
public void resultIsShorterWhenFlagPresent() {
28+
String[] in = {FLAG, "-m", "m.gguf", "--port", "8080"};
29+
assertThat(ServerLauncher.withoutFlag(in, FLAG).length < in.length, is(true));
2330
}
2431

2532
@Test
26-
public void selectsOpenAiCompatWhenFlagPresent() {
27-
assertThat(ServerLauncher.selectsOpenAiCompat(new String[] {"--open-ai-compat", "-m", "m.gguf"}), is(true));
33+
public void resultKeepsLengthWhenFlagAbsent() {
34+
String[] in = {"-m", "m.gguf", "--port", "8080"};
35+
assertThat(ServerLauncher.withoutFlag(in, FLAG).length == in.length, is(true));
2836
}
2937

3038
@Test
31-
public void selectorFlagPositionDoesNotMatter() {
32-
assertThat(ServerLauncher.selectsOpenAiCompat(new String[] {"-m", "m.gguf", "--open-ai-compat"}), is(true));
39+
public void flagPositionDoesNotMatter() {
40+
String[] in = {"-m", "m.gguf", FLAG};
41+
assertThat(ServerLauncher.withoutFlag(in, FLAG).length < in.length, is(true));
3342
}
3443

44+
// --- stripping behaviour ---
45+
3546
@Test
36-
public void withoutFlagStripsTheSelectorAndPreservesTheRest() {
37-
String[] out = ServerLauncher.withoutFlag(
38-
new String[] {"--open-ai-compat", "-m", "m.gguf", "--port", "8080"},
39-
ServerLauncher.OPEN_AI_COMPAT_FLAG);
47+
public void stripsTheSelectorAndPreservesTheRest() {
48+
String[] out = ServerLauncher.withoutFlag(new String[] {FLAG, "-m", "m.gguf", "--port", "8080"}, FLAG);
4049
assertThat(out, arrayContaining("-m", "m.gguf", "--port", "8080"));
4150
}
4251

4352
@Test
44-
public void withoutFlagRemovesEveryOccurrence() {
45-
String[] out = ServerLauncher.withoutFlag(
46-
new String[] {"--open-ai-compat", "-m", "m.gguf", "--open-ai-compat"},
47-
ServerLauncher.OPEN_AI_COMPAT_FLAG);
53+
public void removesEveryOccurrence() {
54+
String[] out = ServerLauncher.withoutFlag(new String[] {FLAG, "-m", "m.gguf", FLAG}, FLAG);
4855
assertThat(out, arrayContaining("-m", "m.gguf"));
4956
}
5057

5158
@Test
52-
public void withoutFlagIsNoOpWhenAbsent() {
53-
String[] in = new String[] {"-m", "m.gguf"};
54-
assertThat(ServerLauncher.withoutFlag(in, ServerLauncher.OPEN_AI_COMPAT_FLAG), arrayContaining("-m", "m.gguf"));
59+
public void isNoOpWhenAbsent() {
60+
assertThat(ServerLauncher.withoutFlag(new String[] {"-m", "m.gguf"}, FLAG), arrayContaining("-m", "m.gguf"));
5561
}
5662

5763
@Test
58-
public void withoutFlagOnEmptyArgsIsEmpty() {
59-
assertThat(ServerLauncher.withoutFlag(new String[] {}, ServerLauncher.OPEN_AI_COMPAT_FLAG), is(emptyArray()));
64+
public void emptyArgsStayEmpty() {
65+
assertThat(ServerLauncher.withoutFlag(new String[] {}, FLAG), is(emptyArray()));
6066
}
6167
}

0 commit comments

Comments
 (0)