Skip to content

Commit 8c8a8cc

Browse files
dfa1claude
andcommitted
cli: --endpoint for any TypeSafe-compatible server
Points the CLI at a self-hosted/local server instead of api.typesafe.ai, and makes ~/.typesafe.apikey optional once --endpoint is given (part of #36; the --local flag for client-local stays open pending the jar-size and flag-shape questions). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 068b460 commit 8c8a8cc

4 files changed

Lines changed: 70 additions & 13 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
- **`cli`: `--endpoint`** — point the CLI at any TypeSafe-compatible server instead of `api.typesafe.ai`; with it, `~/.typesafe.apikey` becomes optional.
11+
1012
## [0.7.0] - 2026-10-04
1113

1214
- **Run TypeSafe on your own machine: `client-local`** — in-process clients for [Cloudflare's Clef-flash](https://huggingface.co/Cloudflare/clef-flash) (`LocalClefTypeSafeClient`, the closest to Jev, 4-bit and on the Apple GPU via `loadOnGpu`) and [Laya](https://huggingface.co/convaiinnovations/laya-typed-decisions) (`LocalLayaTypeSafeClient`, about 0.1 s per request), plus Qwen2.5 as a baseline, all on ONNX Runtime behind the same `TypeSafeClient`; sizes and speeds are in the README. (#14, #19)

‎cli/src/main/java/io/github/dfa1/typesafe/cli/Main.java‎

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import io.github.dfa1.typesafe.client.http.jdk.JdkHttpTransport;
1414
import io.github.dfa1.typesafe.codec.Codec;
1515

16+
import java.io.IOException;
17+
import java.net.URI;
1618
import java.nio.charset.StandardCharsets;
1719
import java.io.PrintStream;
1820
import java.util.ArrayList;
@@ -33,12 +35,15 @@
3335
public final class Main {
3436

3537
private static final String USAGE = "Usage: typesafe --state <text> [--model <id>] "
38+
+ "[--endpoint <url>] "
3639
+ "[--noul [<name>=]<instructions>]... "
3740
+ "[--choice [<name>=]<instructions>|<option1,option2,...>]... "
3841
+ "[--score [<name>=]<instructions>|<level1,level2,...>]... "
3942
+ "[--min <name>=<threshold>]... [--print <name>]... "
4043
+ "[--verbose] [--timing] | --version | --help "
4144
+ "(name defaults to noul/choice/score, so name it explicitly if you use more than one; "
45+
+ "--endpoint points at any TypeSafe-compatible server instead of api.typesafe.ai, and "
46+
+ "makes ~/.typesafe.apikey optional (no key means no Authorization header); "
4247
+ "--min compares a noul/score answer's value, exits 1 if any is below its threshold; "
4348
+ "--print prints just that answer's value; without --print, stdout is silent unless "
4449
+ "--verbose, which prints the full response as JSON)";
@@ -69,14 +74,35 @@ static int run(String[] args, PrintStream out, PrintStream err) throws Exception
6974
}
7075

7176
Jackson3Codec codec = new Jackson3Codec();
72-
try (TypeSafeClient client = DefaultTypeSafeClient.builder().apiKey(ApiKey.fromDefaultFile())
77+
DefaultTypeSafeClient.Builder builder = DefaultTypeSafeClient.builder()
7378
.codec(codec)
74-
.httpTransport(new JdkHttpTransport())
75-
.build()) {
79+
.httpTransport(new JdkHttpTransport());
80+
ApiKey apiKey = loadApiKey(parsed.endpoint());
81+
if (apiKey != null) {
82+
builder.apiKey(apiKey);
83+
}
84+
if (parsed.endpoint() != null) {
85+
builder.endpoint(parsed.endpoint());
86+
}
87+
try (TypeSafeClient client = builder.build()) {
7688
return run(client, codec, parsed, out, err);
7789
}
7890
}
7991

92+
/** The key file is required against the default endpoint (the real API needs auth), but
93+
* optional against any {@code --endpoint} override (a local/self-hosted server may need
94+
* none). */
95+
private static ApiKey loadApiKey(URI endpoint) throws IOException {
96+
if (endpoint == null) {
97+
return ApiKey.fromDefaultFile();
98+
}
99+
try {
100+
return ApiKey.fromDefaultFile();
101+
} catch (IOException e) {
102+
return null;
103+
}
104+
}
105+
80106
/** The evaluate-and-print flow, taking an already-built client so it's testable without a
81107
* network call. */
82108
static int run(TypeSafeClient client, Codec codec, ParsedArgs parsed, PrintStream out, PrintStream err)
@@ -115,7 +141,7 @@ static int run(TypeSafeClient client, Codec codec, ParsedArgs parsed, PrintStrea
115141
}
116142

117143
record ParsedArgs(String state, Model model, Map<String, Question> questions, List<String> minSpecs,
118-
List<String> printNames, boolean verbose, boolean timing) {
144+
List<String> printNames, boolean verbose, boolean timing, URI endpoint) {
119145
}
120146

121147
static ParsedArgs parse(String[] args) {
@@ -126,6 +152,7 @@ static ParsedArgs parse(String[] args) {
126152
List<String> printNames = new ArrayList<>();
127153
boolean verbose = false;
128154
boolean timing = false;
155+
URI endpoint = null;
129156

130157
try {
131158
for (int i = 0; i < args.length; i++) {
@@ -136,6 +163,7 @@ static ParsedArgs parse(String[] args) {
136163
case "--timing" -> timing = true;
137164
case "--state" -> state = args[++i];
138165
case "--model" -> model = new Model(args[++i]);
166+
case "--endpoint" -> endpoint = URI.create(args[++i]);
139167
case "--min" -> minSpecs.add(args[++i]);
140168
case "--print" -> printNames.add(args[++i]);
141169
case "--noul", "--choice", "--score" -> {
@@ -159,7 +187,7 @@ static ParsedArgs parse(String[] args) {
159187
throw new IllegalArgumentException("At least one --noul/--choice/--score question is required");
160188
}
161189

162-
return new ParsedArgs(state, model, questions, minSpecs, printNames, verbose, timing);
190+
return new ParsedArgs(state, model, questions, minSpecs, printNames, verbose, timing, endpoint);
163191
}
164192

165193
static String answerValue(EvaluateResponse response, String name) {

‎cli/src/test/java/io/github/dfa1/typesafe/cli/MainTest.java‎

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import java.io.ByteArrayOutputStream;
1616
import java.io.PrintStream;
17+
import java.net.URI;
1718
import java.util.List;
1819
import java.util.Map;
1920

@@ -68,6 +69,26 @@ void parseDefaultsQuestionNameToTheFlagWhenNoNameIsGiven() {
6869
assertThat(result.questions()).containsOnlyKeys("noul");
6970
}
7071

72+
@Test
73+
void parseReadsEndpoint() {
74+
// When
75+
Main.ParsedArgs result = Main.parse(new String[] {
76+
"--state", "hi", "--endpoint", "http://localhost:8000/v1/systemone", "--noul", "Is this urgent?"
77+
});
78+
79+
// Then
80+
assertThat(result.endpoint()).isEqualTo(URI.create("http://localhost:8000/v1/systemone"));
81+
}
82+
83+
@Test
84+
void parseLeavesEndpointNullByDefault() {
85+
// When
86+
Main.ParsedArgs result = Main.parse(new String[] {"--state", "hi", "--noul", "Is this urgent?"});
87+
88+
// Then
89+
assertThat(result.endpoint()).isNull();
90+
}
91+
7192
@Test
7293
void parseRejectsUnknownFlag() {
7394
// When / Then
@@ -251,7 +272,7 @@ void runPrintsNothingToStdoutByDefault() throws Exception {
251272
// Given
252273
given(client.evaluate(any())).willReturn(response(Map.of("urgent", new Answer.Noul(0.5))));
253274
Main.ParsedArgs parsed = new Main.ParsedArgs("hi", Model.LATEST,
254-
Map.of("urgent", Question.noul("Is this urgent?")), List.of(), List.of(), false, false);
275+
Map.of("urgent", Question.noul("Is this urgent?")), List.of(), List.of(), false, false, null);
255276

256277
// When
257278
int result = Main.run(client, codec, parsed, out, err);
@@ -266,7 +287,7 @@ void runPrintsTheFullResponseAsJsonToStdoutWhenVerbose() throws Exception {
266287
// Given
267288
given(client.evaluate(any())).willReturn(response(Map.of("urgent", new Answer.Noul(0.5))));
268289
Main.ParsedArgs parsed = new Main.ParsedArgs("hi", Model.LATEST,
269-
Map.of("urgent", Question.noul("Is this urgent?")), List.of(), List.of(), true, false);
290+
Map.of("urgent", Question.noul("Is this urgent?")), List.of(), List.of(), true, false, null);
270291

271292
// When
272293
int result = Main.run(client, codec, parsed, out, err);
@@ -281,7 +302,7 @@ void runPrintsOnlyTheRequestedAnswersWhenPrintNamesAreGiven() throws Exception {
281302
// Given
282303
given(client.evaluate(any())).willReturn(response(Map.of("urgent", new Answer.Noul(0.5))));
283304
Main.ParsedArgs parsed = new Main.ParsedArgs("hi", Model.LATEST,
284-
Map.of("urgent", Question.noul("Is this urgent?")), List.of(), List.of("urgent"), false, false);
305+
Map.of("urgent", Question.noul("Is this urgent?")), List.of(), List.of("urgent"), false, false, null);
285306

286307
// When
287308
int result = Main.run(client, codec, parsed, out, err);
@@ -296,7 +317,7 @@ void runPrintsOnlyTheRequestedAnswersEvenWhenVerboseIsAlsoGiven() throws Excepti
296317
// Given
297318
given(client.evaluate(any())).willReturn(response(Map.of("urgent", new Answer.Noul(0.5))));
298319
Main.ParsedArgs parsed = new Main.ParsedArgs("hi", Model.LATEST,
299-
Map.of("urgent", Question.noul("Is this urgent?")), List.of(), List.of("urgent"), true, false);
320+
Map.of("urgent", Question.noul("Is this urgent?")), List.of(), List.of("urgent"), true, false, null);
300321

301322
// When
302323
int result = Main.run(client, codec, parsed, out, err);
@@ -311,7 +332,7 @@ void runReturns1AndReportsMinFailuresOnStderr() throws Exception {
311332
// Given
312333
given(client.evaluate(any())).willReturn(response(Map.of("urgent", new Answer.Noul(0.2))));
313334
Main.ParsedArgs parsed = new Main.ParsedArgs("hi", Model.LATEST,
314-
Map.of("urgent", Question.noul("Is this urgent?")), List.of("urgent=0.5"), List.of(), false, false);
335+
Map.of("urgent", Question.noul("Is this urgent?")), List.of("urgent=0.5"), List.of(), false, false, null);
315336

316337
// When
317338
int result = Main.run(client, codec, parsed, out, err);
@@ -326,7 +347,7 @@ void runPrintsRequestAndResponseToStderrWhenVerbose() throws Exception {
326347
// Given
327348
given(client.evaluate(any())).willReturn(response(Map.of("urgent", new Answer.Noul(0.5))));
328349
Main.ParsedArgs parsed = new Main.ParsedArgs("hi", Model.LATEST,
329-
Map.of("urgent", Question.noul("Is this urgent?")), List.of(), List.of(), true, false);
350+
Map.of("urgent", Question.noul("Is this urgent?")), List.of(), List.of(), true, false, null);
330351

331352
// When
332353
Main.run(client, codec, parsed, out, err);
@@ -340,7 +361,7 @@ void runPrintsTimingToStderrWhenTiming() throws Exception {
340361
// Given
341362
given(client.evaluate(any())).willReturn(response(Map.of("urgent", new Answer.Noul(0.5))));
342363
Main.ParsedArgs parsed = new Main.ParsedArgs("hi", Model.LATEST,
343-
Map.of("urgent", Question.noul("Is this urgent?")), List.of(), List.of(), false, true);
364+
Map.of("urgent", Question.noul("Is this urgent?")), List.of(), List.of(), false, true, null);
344365

345366
// When
346367
Main.run(client, codec, parsed, out, err);
@@ -354,7 +375,7 @@ void runReturns1AndPrintsUsageWhenAPrintNameIsUnknown() throws Exception {
354375
// Given
355376
given(client.evaluate(any())).willReturn(response(Map.of("urgent", new Answer.Noul(0.5))));
356377
Main.ParsedArgs parsed = new Main.ParsedArgs("hi", Model.LATEST,
357-
Map.of("urgent", Question.noul("Is this urgent?")), List.of(), List.of("bogus"), false, false);
378+
Map.of("urgent", Question.noul("Is this urgent?")), List.of(), List.of("bogus"), false, false, null);
358379

359380
// When
360381
int result = Main.run(client, codec, parsed, out, err);

‎docs/how-to.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,12 @@ type, since unnamed ones of the same type overwrite each other. `--model <id>` (
583583
`jev-preview`) overrides the default `jev-latest`; `jev-latest`/`jev-preview` resolve to their
584584
alias, any other id is pinned directly. Reads the token from `~/.typesafe.apikey`.
585585

586+
`--endpoint <url>` points at any TypeSafe-compatible server instead of `api.typesafe.ai` — e.g.
587+
a server running [Open-Jev](https://huggingface.co/com-kotobalabs/open-jev-deberta-v3-large), or
588+
the MLX Clef-flash server from [Run Clef-flash on a Mac with MLX](#run-clef-flash-on-a-mac-with-mlx)
589+
(`http://localhost:8000/v1/systemone`). With `--endpoint`, `~/.typesafe.apikey` becomes optional:
590+
no key file means no `Authorization` header.
591+
586592
Stdout is silent by default — reach for `--print`/`--verbose` below to see anything. `--verbose`
587593
prints the full `EvaluateResponse` as compact JSON to stdout (pipe it into `jq` to read it), plus the
588594
outgoing request and the response's request id to stderr; `--timing` prints how long the

0 commit comments

Comments
 (0)