Jev for Elixir
jev_elixir is a small, idiomatic interface for making typed decisions with
Jev. It exposes application-level functions such as Jev.ask?/2 and
Jev.analyze/3, while provider adapters handle the external API wire format.
The first release supports TypeSafe's System One API and OpenRouter's Decisions API. Both providers support all three System One question types: Noul, Choice, and Score.
Installation
Add the package to your dependencies:
def deps do
[
{:jev_elixir, "~> 0.1.0"}
]
end
Configure the provider and API key at runtime:
# config/runtime.exs
import Config
config :jev_elixir,
provider: :typesafe,
api_key: System.fetch_env!("TYPESAFE_API_KEY")
Runtime configuration keeps the API key out of compiled releases. A call can
override :provider, :api_key, :model, :endpoint, or :timeout through
its options when needed.
OpenRouter
OpenRouter serves Jev through its dedicated Decisions API. Configure it with an OpenRouter key:
# config/runtime.exs
import Config
config :jev_elixir,
provider: :openrouter,
api_key: System.fetch_env!("OPENROUTER_API_KEY")
The adapter sends requests to
https://openrouter.ai/api/alpha/decisions using typesafe/jev-1.13 by
default. The Decisions API is currently an alpha OpenRouter API, so its wire
contract may change. Override the model globally or for one call:
config :jev_elixir,
provider: :openrouter,
api_key: System.fetch_env!("OPENROUTER_API_KEY"),
endpoint: "https://openrouter.ai/api/alpha/decisions",
model: "typesafe/jev-1.13"
Jev.ask?("Is this spam?",
state: message,
provider: :openrouter,
api_key: System.fetch_env!("OPENROUTER_API_KEY"),
model: "typesafe/jev-1.13",
threshold: 0.95
)
Jev is a decisions model on OpenRouter. The adapter intentionally uses the
Decisions endpoint rather than /chat/completions.
Boolean decisions
ask?/2 returns a real boolean, which makes it straightforward to match in a
case expression or use in a Phoenix controller:
def create(conn, %{"comment" => params}) do
case Jev.ask?("Is this spam?", state: params["body"], threshold: 0.95) do
true ->
conn
|> put_status(:unprocessable_entity)
|> json(%{error: "spam"})
false ->
create_comment(conn, params)
end
end
The threshold is the minimum probability of "yes" required for ask?/2 to
return true. It defaults to 0.5.
Use ask/2 when the answer and its probability are both useful:
Jev.ask("Does this message require urgent attention?", state: message)
#=> {:yes, 0.91}
Choice and Score
Choice selects one supplied option. The second tuple value is the probability TypeSafe assigned to the selected option:
Jev.choose(
"Which team should handle this ticket?",
[:billing, :sales, :technical],
state: ticket.body
)
#=> {:technical, 0.94}
Options can also be a map when descriptions make the categories clearer:
Jev.choose(
"Which team should handle this ticket?",
%{
billing: "Payments, invoices, and refunds",
sales: "Pricing and purchase questions",
technical: "Bugs, outages, and integrations"
},
state: ticket.body
)
Score evaluates ordered levels and returns TypeSafe's weighted score and confidence:
Jev.score(
"How urgent is this ticket?",
["Can wait", "Needs attention today", "Immediate response"],
state: ticket.body
)
#=> {1.82, 0.88}
Analyze several questions in one request
analyze/3 sends every question with the same state in one provider request:
Jev.analyze(ticket.body,
spam: {:boolean, "Is this spam?"},
urgent: {:noul, "Does this require urgent attention?"},
department: {:choice, [:billing, :sales, :technical]},
sentiment: {:choice, [:positive, :neutral, :negative]},
urgency: {:score, ["low", "medium", "high"]}
)
#=> %{
#=> spam: {:no, 0.98},
#=> urgent: {:yes, 0.91},
#=> department: {:technical, 0.94},
#=> sentiment: {:negative, 0.87},
#=> urgency: {1.82, 0.88}
#=> }
:boolean is the friendly Jev name for TypeSafe's Noul question. :noul is
also accepted. Boolean questions may include TypeSafe criteria:
{:noul, "Does this request a refund?",
%{true: "The customer explicitly asks for money back", false: "No refund requested"}}
Choice and Score specifications without instructions derive a short instruction from their question ID. For production decisions, explicit instructions usually produce a clearer contract:
%{
department:
{:choice, [:billing, :sales, :technical],
"Which department should handle this ticket?"}
}
Common use cases
Moderate user-generated content
Block high-confidence spam while allowing the normal comment flow to remain simple:
case Jev.ask?("Is this comment spam?", state: comment.body, threshold: 0.95) do
true -> {:error, :spam}
false -> Comments.publish(comment)
end
Several moderation checks can share one request:
signals =
Jev.analyze(post.body,
spam: {:boolean, "Is this unsolicited advertising?"},
harassment: {:boolean, "Does this attack or intimidate a person?"},
adult_content: {:boolean, "Does this contain explicit adult content?"}
)
case signals do
%{harassment: {:yes, probability}} when probability >= 0.9 ->
Moderation.hold_for_review(post)
%{adult_content: {:yes, probability}} when probability >= 0.9 ->
Moderation.apply_age_gate(post)
_ ->
Moderation.allow(post)
end
Route customer-support tickets
Use Choice to send work to the most appropriate queue:
case Jev.choose(
"Which team should own this customer request?",
%{
billing: "Charges, invoices, refunds, and payouts",
sales: "Pricing, plans, and purchasing",
technical: "Bugs, outages, and integrations"
},
state: ticket.body
) do
{:billing, probability} when probability >= 0.8 -> Tickets.route(ticket, :billing)
{:sales, probability} when probability >= 0.8 -> Tickets.route(ticket, :sales)
{:technical, probability} when probability >= 0.8 -> Tickets.route(ticket, :technical)
{_team, _probability} -> Tickets.route(ticket, :manual_triage)
end
Prioritize an inbox or work queue
Use Score when the decision is an ordered scale rather than a category:
case Jev.score(
"How urgently should an agent respond?",
["Can wait", "Respond today", "Respond immediately"],
state: ticket.body
) do
{score, confidence} when score >= 1.5 and confidence >= 0.75 -> :high_priority
{score, _confidence} when score >= 0.75 -> :normal_priority
{_score, _confidence} -> :low_priority
end
Send suspicious transactions to review
Jev can provide one signal in a wider risk policy. Keep deterministic limits and account rules in application code:
state = %{
amount: payment.amount,
currency: payment.currency,
description: payment.description,
account_age_days: account.age_days
}
case Jev.ask?(
"Does this transaction description and account context look suspicious?",
state: state,
threshold: 0.9
) do
true -> Payments.send_to_review(payment)
false -> Payments.continue_policy_checks(payment)
end
Qualify and route leads
Analyze intent and destination together, then keep uncertain results in a general queue:
result =
Jev.analyze(inquiry,
purchase_intent: {:boolean, "Is the sender actively evaluating a purchase?"},
segment:
{:choice, [:self_service, :small_business, :enterprise],
"Which customer segment best fits this inquiry?"}
)
case result do
%{purchase_intent: {:yes, intent}, segment: {segment, confidence}}
when intent >= 0.85 and confidence >= 0.8 ->
Leads.route(inquiry, segment)
_ ->
Leads.route(inquiry, :nurture)
end
Other useful applications include email triage, marketplace listing review, survey classification, sentiment tracking, request categorization, and escalation detection. Choose thresholds from examples drawn from your own application, then retain a manual-review path for uncertain decisions.
Return values and errors
The public API normalizes provider responses:
| Question | Result |
|---|---|
| Boolean / Noul | {:yes, probability} or {:no, probability} |
| Choice | {selected_option, selected_probability} |
| Score | {weighted_score, confidence} |
For a :no result, the probability is the probability of no (1 - noul).
Provider, HTTP, and invalid-response failures raise Jev.Error. Invalid local
arguments raise ArgumentError. This lets ask?/2 keep its useful boolean
contract instead of returning a truthy {:ok, false} tuple.
Provider architecture
Provider modules implement Jev.Provider.evaluate/3. :typesafe resolves to
Jev.Provider.TypeSafe; :openrouter resolves to
Jev.Provider.OpenRouter. Both share the same question encoder and answer
normalizer. A custom adapter module can be configured directly:
config :jev_elixir, provider: MyApp.JevProvider
This boundary lets future providers translate the common question DSL and normalize their results without changing application code.
Current scope
Version 0.1.1 provides synchronous TypeSafe and OpenRouter calls, runtime
configuration, per-request overrides, the three typed question forms, batch
analysis, and a custom provider behaviour. Future work can add non-raising API
variants, telemetry, retry policy controls, and richer response metadata.
License
MIT. See LICENSE.