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.