{"slug": "jev4j", "title": "Jev4j", "summary": "TypeSafe released jev4j, an MIT-licensed Java 17+ library that turns plain-English questions into ordinary Java values — booleans, enum constants, or points on a developer-defined scale — through TypeSafe's structured decision model Jev. The library sends one request per evaluation to either TypeSafe or OpenRouter, returns probabilities with every answer so callers can set thresholds, supports up to eight questions per request, and offers blocking or CompletableFuture calls plus a Spring Boot 4 starter. jev4j is available on Maven Central as io.github.maxsumrall.jev4j:jev4j-core, and its API may still change.", "body_md": "jev4j lets you ask [Jev](https://docs.typesafe.ai/introduction), TypeSafe's structured decision\nmodel, a question in plain English and use the answer as ordinary Java: a boolean in an `if`, one\nof your own enum constants in a `switch`, or a point on a scale you define.\n\n```\nJev.NoulQuestion isRefundRequest = Jev.noul(\"Is this a refund request?\");\n\nif (jev.test(\"I want my money back!\", isRefundRequest)) {\n    System.out.println(\"Start the refund workflow\");\n}\n```\n\nGive it an enum and it picks a constant, so the compiler checks that you handled every case:\n\n```\nenum Team {\n    BILLING,\n    DELIVERY,\n    SUPPORT\n}\n\nString inbox =\n        switch (jev.evaluate(\n                        \"My parcel never arrived.\",\n                        Jev.choice(Team.class, \"Which team can help?\"))\n                .value()) {\n            case BILLING -> \"billing-support\";\n            case DELIVERY -> \"delivery-support\";\n            case SUPPORT -> \"general-support\";\n        };\n```\n\nOr ask it to place the input on an ordered scale:\n\n```\nenum Mood {\n    CALM,\n    FRUSTRATED,\n    FURIOUS\n}\n\nMood mood =\n        jev.evaluate(\n                        \"This is the third failed delivery!\",\n                        Jev.score(Mood.class, \"How frustrated is the customer?\"))\n                .nearestLevel();\n```\n\n`nearestLevel()` rounds to a `Mood`. If you want the fractional score, call `.value()` instead.\n\nThese snippets use an evaluator called `jev`, which [Get started](#get-started) shows you how to\nbuild. Each evaluation sends one request to your provider, and providers can charge for it. Every\nanswer comes with its probabilities, so you can [set a threshold](#you-decide-how-sure-is-sure-enough)\nand only act when the model is sure enough.\n\nYou can also ask up to eight questions in one request and\n[map the answers into a record](#several-questions-in-one-request), or pass\n[a snapshot of your own data](#structured-input) in place of text. Calls can block or return a\n`CompletableFuture`, and jev4j talks to either TypeSafe or OpenRouter. It works in plain Java and\nhas a [Spring Boot 4 starter](#spring-boot). For tests, you can build answers yourself and never\ntouch the network.\n\njev4j needs Java 17 or newer and uses the MIT license.\n\njev4j is young, and the API may still change. Add the core library from\n[Maven Central](https://central.sonatype.com/artifact/io.github.maxsumrall.jev4j/jev4j-core),\nputting the latest release in place of `YOUR_VERSION`:\n\n```\n<dependency>\n  <groupId>io.github.maxsumrall.jev4j</groupId>\n  <artifactId>jev4j-core</artifactId>\n  <version>YOUR_VERSION</version>\n</dependency>\n```\n\nBuild one evaluator with your OpenRouter key and reuse it for every request:\n\n``` python\nimport io.github.maxsumrall.jev4j.Jev;\nimport io.github.maxsumrall.jev4j.JevEvaluator;\n\nJevEvaluator jev =\n        JevEvaluator.builder(System.getenv(\"OPENROUTER_API_KEY\"))\n                .openRouter()\n                .model(\"jev-latest\")\n                .build();\n```\n\nTo call TypeSafe directly, pass a TypeSafe key and use `.typeSafe()` in place of `.openRouter()`.\nEither way, choose a Jev model such as `jev-latest` rather than a chat model.\n\nThe examples below build on each other and share this evaluator. Put the fields, enums, and records in your class, and run the rest from a method.\n\nJev answers three kinds of question. A **Noul** is a yes-or-no question, answered with the\nprobability of yes. A **Choice** picks one constant from your enum. A **Score** rates the input on\nan ordered scale that you define.\n\n`test(...)` gives you a boolean. `evaluate(...)` gives you the whole answer, probability included,\nso you can check a second cutoff without paying for a second call:\n\n``` python\nimport io.github.maxsumrall.jev4j.Jev.NoulAnswer;\nimport io.github.maxsumrall.jev4j.Jev.NoulQuestion;\n\nNoulQuestion refundRequested =\n        Jev.noul(\"Is the customer asking for money back?\")\n                .describe(true, \"Requests a refund or reversal of a charge\")\n                .threshold(0.8);\n\nNoulAnswer refund = jev.evaluate(\"Please refund the duplicate charge.\", refundRequested);\nboolean requested = refund.isTrue(); // probability >= 0.8\nboolean clearRequest =\n        refund.isTrueAt(0.95); // check a stricter cutoff without another call\n```\n\nA `false` from `isTrue()` only tells you the probability fell below your threshold. The model may\nstill be unsure. When a confident no matters to you, leave a band in the middle for a person to\nreview:\n\n```\ndouble p = refund.probabilityTrue();\nString decision = p >= 0.9 ? \"YES\" : p <= 0.1 ? \"NO\" : \"REVIEW\";\n```\n\nYour enum defines the options. You can describe each constant on the question, as below, or\nimplement `Jev.Described` on the enum so each description sits next to its constant.\n\n``` python\nimport io.github.maxsumrall.jev4j.Jev.ChoiceAnswer;\nimport io.github.maxsumrall.jev4j.Jev.ChoiceQuestion;\n\nenum Department {\n    BILLING,\n    DELIVERY,\n    OTHER\n}\nChoiceQuestion<Department> department =\n        Jev.choice(Department.class, \"Which team should handle this message?\")\n                .describe(Department.BILLING, \"Charges, payments, and refunds\")\n                .describe(Department.DELIVERY, \"Late, missing, or damaged deliveries\")\n                .minConfidence(0.85);\n\nChoiceAnswer<Department> classification =\n        jev.evaluate(\"My groceries never arrived.\", department);\nString route = classification.acceptedValue().map(Department::name).orElse(\"MANUAL_REVIEW\");\n```\n\nCheck `acceptedValue()` or `meetsThresholds()` before you route anything. `value()` returns the\nmodel's pick even when it falls short of your threshold, and `probabilities()` and `confidence()`\nreturn the provider's raw numbers. `classification.is(Department.DELIVERY)` checks the pick and its\nacceptance in one call.\n\nDescriptions guide the model, but every constant stays a possible answer. On the wire, jev4j names\neach option with `Enum.name()` and ignores any `toString()` override.\n\nDeclare your enum constants from lowest to highest. jev4j treats the first constant as zero and\ncounts up from there, so reordering the constants changes the scale. Describe the levels with\n`.describe(level, text)` or by implementing `Jev.ScoreLevel`.\n\nA score answer offers a few views of the same result:\n\n| Answer method | Result | \n|---|---|\n| `value()` /`acceptedValue()` | Fractional score / optional score after the confidence check | \n| `nearestLevel()` /`acceptedLevel()` | Nearest enum / optional enum after the confidence check; midpoints round up | \n| `mostLikelyLevel()` | Enum with the highest probability; ties choose the first declared level | \n| `probabilities()` /`confidence()` | Provider's distribution / confidence | \n\nThe score and the distribution can point at different levels, so `nearestLevel()` and\n`mostLikelyLevel()` may disagree. Neither one checks confidence. Use `acceptedLevel()` when you\nplan to act on the rounded score.\n\nIf an enum feels like too much, build the rubric inline:\n\n```\nJev.ScoreQuestion quality =\n        Jev.score(\"How useful is this response?\")\n                .level(\"Unhelpful\")\n                .level(\"Partly useful\")\n                .level(\"Useful and complete\")\n                .build();\n```\n\nA Score takes 2 to 10 levels, and a Choice takes up to 255 options. The provider has to return a\nfull probability distribution that sums to 1 within `1e-6`. jev4j rejects any answer that misses,\nand it won't normalize the numbers for you.\n\njev4j leaves the acceptance rules to you. Set them per question:\n\n| Type | Configuration | Accept when | \n|---|---|---|\n| Noul | `.threshold(0.8)` | Probability of yes ≥ 0.8; default is 0.5 | \n| Choice | `.minConfidence(0.85)` | Jev confidence ≥ 0.85 | \n| Choice | `.minProbability(0.9)` | Selected option's probability ≥ 0.9 | \n| Score | `.minConfidence(0.85)` | Jev confidence ≥ 0.85 | \n\nChoice and Score accept any answer until you set a minimum. If you set both Choice minimums, an\nanswer has to clear both. Confidence and probability measure different things, and a high number\non either one can still come with a wrong answer. TypeSafe covers the difference in\n[confidence versus probability](https://docs.typesafe.ai/confidence).\n\njev4j checks thresholds in your process, so trying a new cutoff costs nothing. The fluent methods\nreturn a new immutable question each time, so keep the return value. A rejected answer still holds\nits original values. When the network or provider fails, you get an exception. jev4j never turns a\nfailure into `false`, `OTHER`, or an empty result.\n\nWhen you have two to eight questions about the same input, send them together. Two questions\nreturn an `Evaluation2` with a type parameter for each answer. Read the typed answers or map them\ninto your own record:\n\n```\nrecord RoutingDecision(NoulAnswer refund, ChoiceAnswer<Department> department) {}\nJevEvaluator.Evaluation2<NoulAnswer, ChoiceAnswer<Department>> answers =\n        jev.evaluate(\"Please refund this order.\", refundRequested, department);\n\nNoulAnswer refundAnswer = answers.answer1();\nChoiceAnswer<Department> departmentAnswer = answers.answer2();\nRoutingDecision routing = answers.map(RoutingDecision::new);\n```\n\nThree questions return `Evaluation3<A1, A2, A3>`, and so on through `Evaluation8`. Each result\nexposes `answer1()` through `answerN()` in question order, with one set of request metadata.\n`map` runs locally. Check each answer's acceptance on its own before you act on it.\n\nJev can read your data as well as text. `Jev.State.from(...)` takes an immutable snapshot of a\npublic record, a map with string keys, a list, or an array:\n\n```\npublic record SupportTicket(String message, int failedPayments) {}\nJev.State ticket = Jev.State.from(new SupportTicket(\"Please refund this order.\", 2));\nRoutingDecision ticketDecision =\n        jev.evaluate(ticket, refundRequested, department).map(RoutingDecision::new);\n```\n\nReuse the snapshot as often as you like. Changes you make to the source object afterwards won't\nreach it, but don't mutate the source while `from(...)` runs. A `String` goes in as literal text,\nand jev4j won't parse it. If you already have JSON, or want your own serializer, pass the JSON to\n`State.fromJson(...)`. Core keeps Jackson out of its public API, so you never configure a mapper.\n\nThe root of a State must be a string, an object, or an array. Numbers, booleans, and null can\nappear inside containers. jev4j checks your input before it sends anything. A null argument\nthrows `NullPointerException`. Invalid JSON, duplicate keys, cycles, unsupported values,\nnon-finite numbers, and input past the size limits throw `IllegalArgumentException`. Nesting stops\nat 128 containers, and the other limits match Jackson's defaults.\n\n`evaluateAsync`, `evaluateWithMetadataAsync`, and `testAsync` return a `CompletableFuture`. They\ntake text or a State, and `evaluateAsync` also accepts two to eight questions:\n\n``` python\nimport java.util.concurrent.CompletableFuture;\n\nCompletableFuture<JevEvaluator.Evaluation2<NoulAnswer, ChoiceAnswer<Department>>>\n        operation = jev.evaluateAsync(ticket, refundRequested, department);\nCompletableFuture<RoutingDecision> asyncRouting =\n        operation.thenApply(result -> result.map(RoutingDecision::new));\n\n// Cancel the original operation if you no longer need it.\noperation.cancel(true);\n```\n\nBad arguments still throw right away. Provider failures complete the future with a\n`JevEvaluationException`, which `join()` wraps in `CompletionException` and `get()` wraps in\n`ExecutionException`. A cancelled future throws `CancellationException`.\n\nTo cancel, call `cancel` on the **original future** (`operation` above), because cancelling a\ndependent stage like `asyncRouting` won't reach the request. `cancel(false)` and `cancel(true)` both\nask the transport to stop before your completion callbacks run. The provider may have started the\nwork already and can still bill you for it. Let jev4j complete its own futures, and don't call\n`complete` or `obtrude` on them.\n\n`.timeout(...)` limits the HTTP request, and on Java 17 it stops counting once the headers arrive,\nso a slow response body can run past it. `get(timeout, unit)`, interrupting the waiting thread, and\n`orTimeout` leave the request running too. For a real deadline, put the timeout on a copy such as\n`operation.copy()`, and cancel the original when the copy times out.\n\njev4j starts no executor of its own and closes nothing you hand it. Reuse the evaluator and your\n`HttpClient`, and run expensive callbacks on your own executor with\n`thenApplyAsync(..., yourExecutor)`.\n\nThe evaluator builder takes `.timeout(Duration)`, `.baseUri(URI)`, and `.httpClient(HttpClient)`.\nBoth provider presets call `/v1/systemone`, and the OpenRouter preset uses\n`https://openrouter.ai/api` as its base URI. jev4j sends your API key and your input to whatever\nURI you configure, so point it at HTTPS endpoints you trust. Save plain HTTP for local tests.\n\nCall `evaluateWithMetadata` when you want to see what a request used:\n\n```\nJevEvaluator.Evaluation<NoulAnswer> evaluation =\n        jev.evaluateWithMetadata(\"Please refund this order.\", refundRequested);\nNoulAnswer result = evaluation.answer();\nlong inputTokens = evaluation.usage().inputTokens();\n```\n\nAn evaluation also reports `model()`, `provider()`, an optional `id()`, and an optional\n`usage().cost()`. Results and exceptions both have `requestId()`, which comes from the optional\n`x-typesafe-request-id` response header and has nothing to do with the `id()` in the body.\nOpenRouter may leave that header out.\n\nWhen a call fails, catch `JevEvaluationException` and look at `category()`: `HTTP`, `TIMEOUT`,\n`IO`, `INTERRUPTED`, `MALFORMED_RESPONSE`, or `UNKNOWN`. HTTP failures also carry\n`httpStatusCode()`. If you interrupt a blocking call, jev4j leaves the thread's interrupt flag set.\n\nExceptions from the evaluator leave out provider response bodies, API keys, and raw transport or parser causes. Request IDs come from the provider, so treat them as provider data before you log them. jev4j won't retry, stream, or split requests for you.\n\nIn a Spring Boot 4 application, depend on `jev4j-spring-boot-starter` in place of `jev4j-core`,\nat the same version. Set your provider and key, and inject `JevEvaluator` through your\nconstructor:\n\n```\njev.provider=openrouter\njev.api-key=${OPENROUTER_API_KEY}\n```\n\nThe [starter README](https://github.com/maxsumrall/jev4j/blob/main/jev4j-spring-boot-starter/README.md) lists the defaults and shows how to\noverride the beans.\n\nBoth examples are standalone Maven projects. They run offline with synthetic answers until you set a key and opt in to live requests:\n\n| Example | Try | \n|---|---|\n| [Plain Java](https://github.com/maxsumrall/jev4j/blob/main/examples/plain-java/README.md) | Thresholds, enum routing, and scores | \n| [Spring Boot triage](https://github.com/maxsumrall/jev4j/blob/main/examples/spring-boot-triage/README.md) | `POST /triage` , validation, review routing, and provider errors | \n\nYou can do the same in your own tests. `question.answer(...)` builds an answer without calling the\nmodel:\n\n```\nNoulAnswer synthetic = refundRequested.answer(0.84);\nassert synthetic.isTrue();\nassert !synthetic.isTrueAt(0.95);\n```\n\nThe [contributor guide](https://github.com/maxsumrall/jev4j/blob/main/CONTRIBUTING.md) covers development, and the\n[release guide](https://github.com/maxsumrall/jev4j/blob/main/docs/releasing.md) covers publishing.", "url": "https://wpnews.pro/news/jev4j", "canonical_source": "https://github.com/maxsumrall/jev4j", "published_at": "2026-09-26 07:28:21+00:00", "updated_at": "2026-09-26 08:01:36.741591+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "large-language-models", "ai-products"], "entities": ["TypeSafe", "jev4j", "Jev", "OpenRouter", "Maven Central", "Spring Boot 4", "Java 17", "io.github.maxsumrall.jev4j"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/jev4j", "markdown": "https://wpnews.pro/news/jev4j.md", "text": "https://wpnews.pro/news/jev4j.txt", "jsonld": "https://wpnews.pro/news/jev4j.jsonld"}}