{"slug": "jeb-turn-any-openai-api-into-a-decision-model", "title": "Jeb: Turn any OpenAI API into a decision model", "summary": "Developer chand1012 released Jeb, an open-source tool that turns any OpenAI-compatible chat completions endpoint into a decision model by reading token log probabilities. Jeb accepts a shared `state` string plus Choice, Score, or Noul questions at `POST /v1/systemone`, sends each question as a separate chat completion request, and returns structured JSON with probabilities and a confidence value. The tool requires a provider that returns `choices[0].logprobs.content[0].top_logprobs` and accepts the fields `model`, `messages`, `logprobs`, `top_logprobs`, and `max_tokens`, and it is inspired by a NobodyWho article and TypeSafe's Jev decision model.", "body_md": "**Jev-compatible decisions from an OpenAI-compatible LLM with logprobs.**\n\n[Quick start](#quick-start) · [Question types](#request-format) · [HTTP API](#http-server) · [Examples](https://github.com/chand1012/jeb/blob/main/examples/README.md)\n\nSend Jeb a `state` and one or more Choice, Score, or Noul questions. It prompts\nthe configured model, reads its token log probabilities, and returns structured\nJSON at `POST /v1/systemone`. The same request format works through the CLI.\n\nInspired by this [NobodyWho article](https://www.nobodywho.ai/posts/jev-in-25-lines/). See [TypeSafe's introduction to Jev](https://docs.typesafe.ai/introduction)\nfor the original decision model and its three primitives.\n\n- An OpenAI-compatible `POST /v1/chat/completions` endpoint and a model that returns`choices[0].logprobs.content[0].top_logprobs` when asked for`logprobs` .\n- A provider that accepts the request fields Jeb sends: `model` ,`messages` ,`logprobs` ,`top_logprobs` , and`max_tokens` . Jeb also sends`reasoning_effort` unless it is configured as an empty string, and sends`temperature` when set.\n\nAn endpoint can be OpenAI-compatible for ordinary chat while lacking the token log probabilities Jeb needs. Check that capability before using a provider.\n\n```\ncurl -fsSL https://raw.githubusercontent.com/chand1012/jeb/main/install.sh -o install.sh\n# Optional. Set if you need an alternative installation directory\n# export JEB_INSTALL_DIR=/path/to/install\nsh install.sh # installs to ~/.local/bin by default\n```\n\nDownload the binary from [releases](https://github.com/chand1012/jeb/releases)\n\nInstall the latest version from source with Go:\n\n```\ngo install github.com/chand1012/jeb@latest\n```\n\nTo install the code in your current checkout instead, run this from the repository root:\n\n```\ngo install .\n```\n\n1. \n[Install Jeb](#installation) .\n2. \nIf you use another OpenAI-compatible provider, set its base URL and model name as described in [Configuration](#configuration) .\n\n```\nexport OPENAI_MODEL=qwen3.5:9b # or another model you have available\n```\n\n3. \nEvaluate an example request: \n\n```\njeb < examples/choice.json\n```\n\nThe CLI reads **one** JSON request from stdin and writes **one** JSON response to\nstdout. Model calls may vary between runs. Start with\n[the mixed example](https://github.com/chand1012/jeb/blob/main/examples/mixed.json) to exercise all three question types:\n\n```\njeb < examples/mixed.json\n```\n\nThe request contains a shared `state` string and a `questions` object. Each key\nin `questions` becomes a key in the response's `answers` object. Jeb sends each\nquestion as a separate chat completion request against the same state.\n\n```\n{\n  \"state\": \"The battery is at 3%. A firmware update requires at least 30%.\",\n  \"questions\": {\n    \"start_update\": {\n      \"type\": \"noul\",\n      \"instructions\": \"Should the update start now?\"\n    }\n  }\n}\n```\n\nJeb supports these question types:\n\n| Type | `criteria` | Returned fields | \n|---|---|---|\n| `choice` | Object mapping option names to descriptions | `type` ,`choice` ,`probabilities` ,`confidence` | \n| `score` | Ordered array of level descriptions | `type` ,`score` ,`legend` ,`probabilities` ,`confidence` | \n| `noul` | Optional object with `true` and`false` descriptions | `type` ,`noul` | \n\nThe object keys name the options. Write each description to explain when that option fits; Jeb builds the model prompt and handles option numbering for you.\n\n```\n{\n  \"type\": \"choice\",\n  \"instructions\": \"Choose the safer venue under the forecast.\",\n  \"criteria\": {\n    \"Outdoor\": \"Fits everyone but provides no shelter from heavy rain.\",\n    \"Indoor\": \"Provides shelter but requires limiting attendance.\"\n  }\n}\n```\n\nThe answer contains the selected option, probabilities, and a `confidence`\nvalue. Probability keys are zero-based numeric option labels assigned in\nalphabetical order of option name.\n\nPut rubric levels in order from low to high. Jeb returns a `legend` that maps\nlevel numbers back to their descriptions. Levels start at 0, so a three-level\nrubric has labels `0`, `1`, and `2`. The numeric `score` is a weighted average\nof those indices and can fall between levels.\n\n```\n{\n  \"type\": \"score\",\n  \"instructions\": \"Rate the weather risk for an outdoor event.\",\n  \"criteria\": [\"Low risk\", \"Moderate risk\", \"High risk\"]\n}\n```\n\nThe answer also includes a probability for each zero-based level and a\n`confidence` value.\n\nA Noul answer is a number from 0 to 1 representing the model's reported\nprobability for `Yes`. You may omit `criteria`, or supply descriptions for\n`true` and `false`:\n\n```\n{\n  \"type\": \"noul\",\n  \"instructions\": \"Should the refund be approved under the policy?\",\n  \"criteria\": {\n    \"true\": \"The request is within 30 days and the item is unused.\",\n    \"false\": \"The request is late or the item has been used.\"\n  }\n}\n```\n\nJeb uses the first token's `Yes` log probability when present. If only `No`\nis present and the model answered `No`, it returns `1 - P(No)`. It returns an\nerror when it cannot derive either value. Noul does not return a separate\n`confidence` field.\n\nThe response contains the configured model name, one answer per named question, and summed upstream token usage:\n\n```\n{\n  \"model\": \"qwen3.5:9b\",\n  \"answers\": {\n    \"start_update\": {\n      \"type\": \"noul\",\n      \"noul\": 0.02\n    }\n  },\n  \"usage\": {\n    \"input_tokens\": 87,\n    \"output_tokens\": 1\n  }\n}\n```\n\nThe numbers above illustrate the shape; they are not the result of a measured\nmodel run. See [all example requests](https://github.com/chand1012/jeb/blob/main/examples/README.md).\n\nStart the server with the same provider settings used by the CLI:\n\n```\njeb serve --host 127.0.0.1 --port 6102\n```\n\nThen send a JSON request:\n\n```\ncurl --fail-with-body \\\n  -H 'Content-Type: application/json' \\\n  --data-binary @examples/mixed.json \\\n  http://127.0.0.1:6102/v1/systemone\n```\n\nThe endpoint accepts `POST` only. Invalid JSON receives HTTP 400; method\nmismatches receive 405; processing and upstream errors currently receive 502.\nThe server logs method, path, status, duration, and remote address. It does not\nprovide authentication or TLS, and its default host is `0.0.0.0`; bind it to\n`127.0.0.1` for local use or place access controls in front of it.\n\nJeb reads optional `config.yaml` from the working directory, `./config/`, or\n`~/.config/jeb/`. Set `JEB_CONFIG_FILE` to use a specific path. Copy\n[config.example.yaml](https://github.com/chand1012/jeb/blob/main/config.example.yaml) as a starting point. The precedence\nis **explicit CLI flags > environment variables > config file > built-in\ndefaults**.\n\n| Setting | Environment variable | Default | Purpose | \n|---|---|---|---|\n| `server.host` | `JEB_HOST` | `0.0.0.0` | HTTP listen address | \n| `server.port` | `JEB_PORT` | `6102` | HTTP listen port | \n| `openai.base_url` | `OPENAI_BASE_URL` | `http://localhost:11434/v1` | Provider API base URL | \n| `openai.api_key` | `OPENAI_API_KEY` | Empty | Bearer token, if needed | \n| `openai.model` | `OPENAI_MODEL` | `qwen3.5:9b` | Upstream model ID | \n| `openai.max_tokens` | `OPENAI_MAX_TOKENS` | `10` | Maximum generated tokens per question | \n| `openai.reasoning_effort` | `OPENAI_REASONING_EFFORT` | `none` | Optional provider hint | \n| `openai.temperature` | `OPENAI_TEMPERATURE` | Unset | Optional sampling temperature | \n| `openai.timeout` | `OPENAI_TIMEOUT` | `60s` | Timeout per completion request | \n| `openai.max_retries` | `OPENAI_MAX_RETRIES` | `3` | Configured value; retries are not implemented yet | \n| `concurrency.max_requests` | `JEB_MAX_REQUESTS` | `1` | Maximum simultaneous model requests | \n\nFor providers that reject `reasoning_effort`, set `openai.reasoning_effort: \"\"`\nin your YAML config. For providers that need an API key, supply it through an\nenvironment variable or a local config file kept out of version control.\nThe current `.gitignore` does **not** exclude `config.yaml`.\n\nCLI flags include `--base-url`, `--api-key`, `--model` (`-m`), `--max-tokens`,\n`--reasoning-effort`, `--timeout`, `--max-retries`, and `--max-requests`.\n`serve` also accepts `--host` (`-H`) and `--port` (`-p`). Run `jeb --help`\nor `jeb serve --help` for the full flag list.\n\nBuild and run the image locally:\n\n```\ndocker build -t jeb .\ndocker run --rm -p 127.0.0.1:6102:6102 \\\n  --env OPENAI_BASE_URL \\\n  --env OPENAI_MODEL \\\n  --env OPENAI_API_KEY \\\n  jeb\n```\n\nSet those variables in your shell first. A container cannot use its own\n`localhost` to reach a provider on the host; configure a provider URL reachable\nfrom inside the container.\n\nGitHub Actions builds Linux `amd64` and `arm64` images for GHCR on pushes to\n`main` and `v*` tags. It also builds Linux, macOS, and Windows binaries for\n`amd64` and `arm64`. Tagged builds publish archives and `checksums.txt` to a\nGitHub release. Binaries and images record the version tag (or `dev`), commit\nSHA, and UTC build date; inspect a binary with `jeb version`.\n\nFor each question, Jeb sends a system prompt and a user prompt containing the\nstate, instructions, and numbered options. It asks the provider for one short\nanswer and top log probabilities for the first generated token. Choice and\nScore normalize the probabilities for the recognized numeric labels; Score\nthen calculates a weighted average. Noul derives the chance of `Yes` from its\nreported token probability. The configured concurrency limit controls how many\nquestion requests are in flight at once.\n\nThese values depend on the provider's tokenizer, token ranking, and\n`top_logprobs` cap. If a valid option is absent from the returned top tokens,\nthe normalized distribution is incomplete and may be misleading. Jeb currently\ndoes not calibrate those probabilities against outcome data. For decisions\nwith real consequences, evaluate the chosen model on your own labeled cases\nand keep application rules or human review in control of the final action.\n\n- **Missing token log probabilities:** Confirm the provider supports both`logprobs` and`top_logprobs` on chat completions for the selected model.\n- **Unexpected option or low confidence:** Inspect the provider's first token\nand its top log probabilities. Choice and Score expect one numeric option\nlabel; Noul expects`Yes` or`No` .\n- **Provider rejects a request field:** Check its support for`reasoning_effort` ,`temperature` ,`top_logprobs` , and`max_tokens` . Configure\nan empty reasoning effort in YAML when that field is unsupported.\n- **The container cannot reach a local provider:** Replace`localhost` in`OPENAI_BASE_URL` with an address reachable from the container.\n- **HTTP 502:** The handler uses 502 for processing failures as well as\nupstream failures. Check the response body and server logs.\n\n```\ngo test ./...\ngo vet ./...\ngo build ./...\n```\n\nThe [Justfile](https://github.com/chand1012/jeb/blob/main/Justfile) provides `just build`, `just serve`, and other local\ntasks. There are currently no Go test files; the CI checks compile packages\nand run `go vet`. The request examples in [` examples/`](https://github.com/chand1012/jeb/blob/main/examples/README.md)\nprovide manual integration cases for a compatible provider.", "url": "https://wpnews.pro/news/jeb-turn-any-openai-api-into-a-decision-model", "canonical_source": "https://github.com/chand1012/jeb/tree/main", "published_at": "2026-09-29 13:06:16+00:00", "updated_at": "2026-09-29 13:18:37.889806+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "large-language-models", "ai-agents"], "entities": ["Jeb", "chand1012", "OpenAI", "NobodyWho", "TypeSafe", "Jev", "Go"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/jeb-turn-any-openai-api-into-a-decision-model", "markdown": "https://wpnews.pro/news/jeb-turn-any-openai-api-into-a-decision-model.md", "text": "https://wpnews.pro/news/jeb-turn-any-openai-api-into-a-decision-model.txt", "jsonld": "https://wpnews.pro/news/jeb-turn-any-openai-api-into-a-decision-model.jsonld"}}