Metadata-Version: 2.4
Name: jevlikedecisions
Version: 0.2.0
Summary: One typed decision abstraction over jev-like decision-model providers.
License-Expression: MIT
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: llm-spendguard>=0.16.30
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-timeout>=2.3; extra == "dev"
Dynamic: license-file

# jevlikedecisions

`jevlikedecisions.decide(...)` is one typed decision entrypoint over a family of provider adapters. Shared evidence is sent once, in full, and responses are checked against both the provider shape and the originating questions. Backend selection is mandatory: no call silently falls back to the deterministic `mock_jevlike` backend.

## Installation and spend-gate setup

Python 3.11 or newer is required.

Install the package and its sole runtime dependency, `llm-spendguard`, with pip:

```console
python -m pip install jevlikedecisions
```

> **Pending first release:** The package is not yet published on PyPI. Until the first release is published, clone this repository, change into the clone, and run `python -m pip install .`. Delete this note when publishing the first release.

Before making a model call, arm the spend gate in the virtual environment. This is required: `jevlikedecisions` deliberately refuses to call a model from an interpreter where spendguard is not enforcing. With the target virtual environment activated, run:

```console
spendguard install-hook --venv "$VIRTUAL_ENV" --no-pkg
```

> **Temporary upstream workaround:** `--no-pkg` avoids a `FileNotFoundError` for pip-installed, non-editable `llm-spendguard` environments where `install-hook` otherwise looks for a repository `pyproject.toml`. Remove this flag and note after that upstream defect is fixed.

Verify the activated interpreter:

```console
spendguard doctor
```

The successful verdict line is `ENFORCING HERE: 🟢 YES — calls from this interpreter are gated`.

> **Temporary upstream workaround:** `spendguard doctor` may currently exit non-zero after printing that successful verdict, with `ValueError: Pricing returned no token cost`. Read the verdict line rather than the exit code; remove this note after that upstream defect is fixed.

If gate setup is skipped, `decide()` refuses model calls through `require_enforcing_spendguard`.

## Request and question schema

Every surface accepts the same JSON object:

```json
{
  "input_parts": ["complete evidence"],
  "questions": [{"name": "safe", "type": "predicate", "description": "Is it safe?"}],
  "backend": "mock_jevlike",
  "intent": "documented-example",
  "prompt_cache_key": null
}
```

`intent` and `backend` are required. Every decision result identifies `backend`, `provider`, `model`, `route`, `via`, `ledger_id`, `usage`, `cost_complete`, and `known_input_cost_floor_usd`. Inspect `route` to distinguish in-process and metered results. `cost_complete` says whether the cost is fully settled; when a provider omits output-token usage, a metered result has `cost_complete: false`, no ledger id, and only the known input-cost floor. In-process results are settled at zero cost. Every question has `name`, `type`, and `description`. `type` is `predicate`, `choice`, or `score`. A choice also has `choices`, whose entries contain `value` and `description`. A score also has `levels`, whose entries contain `label` and `description`. Library validation is authoritative for every surface.

Backend names and aliases come from the package configuration. An alias is accepted only at selection; estimates, approvals, receipts, audit evidence, and returned provenance always use the canonical backend name. Omitting `backend` never selects a default and produces a typed `DecisionUnavailable` refusal.

| Canonical backend | Accepted aliases | Provider | Model | Usability and precondition |
|---|---|---|---|---|
| `openai_decisions` | `openai` | `openai` | `gpt-6-luna` | Usable only with `OPENAI_API_KEY`, an enforcing spendguard interpreter, and a matching estimate approval. The sole proven call is ledger `2bed00b5f31d4858`: 319 input tokens costing `$3.19e-05`; its output side is unproven. |
| `typesafe_jev` | `jev` | `typesafe` | `jev-1.14.0` | Usable only with `TYPESAFE_API_KEY`, an enforcing spendguard interpreter, and a matching estimate approval. Input costs `$0.042` per 1M tokens and output is free, as documented at `docs.typesafe.ai/models`; the wire schema is documented at `docs.typesafe.ai/api/system-one`. No live TypeSafe call has yet been proven. |
| `mock_jevlike` | none | `mock` | `deterministic` | Usable in-process and never selected implicitly. Predicate probability is exactly `0.5`; choice selects the first option with probability/confidence `1.0` and all others `0.0`; score selects the first level with score `0.0`, probability/confidence `1.0`, and all other levels `0.0`; reported input/output tokens are both `0`. |

## Estimate, approve, then spend

The shipped configuration contains exactly three backends: metered `typesafe_jev` and `openai_decisions`, plus the in-process `mock_jevlike` test double. Their only transport kinds are `http` and `in_process`. A backend that declares the removed `subscription` transport is refused when configuration loads; subscription-lane LLM access belongs in llm-spendguard.

Choice questions may declare `"calibrated_choice_distribution": "required"` or `"calibrated_choice_distribution": "omitted"`. Omitting the declaration preserves the current `required` contract: `choice`, `probabilities` for every declared choice in order, and `confidence`. Declaring `omitted` explicitly requests only `name`, `type`, and `choice`; returning either `probabilities` or `confidence` is a named nonconformance, even if null. Unknown or malformed declarations are refused with the permitted values from the packaged `choice_distribution_contract` configuration beside `limits`. Mixed declarations in one call are supported. The shared validator enforces the declared answer fields across Python, CLI, HTTP, and MCP. Score questions are unchanged.

Self-reported confidence is **not calibrated**. In the measured eight-question probe with twenty declared choices per question, gpt-6-luna returned top probability/confidence `1.0` on every item (entropy approximately zero; effective support `1.00`), while glm-5.3 returned top probability/confidence `0.99` on every item (entropy `0.0629`; effective support `1.06`). Those degenerate vectors do not establish calibrated uncertainty. At the working shape of 32 questions × 29 choices, the measured output grew from 2,149 to 23,270 tokens. The declaration allows callers to omit that unvalidated distribution rather than pay for it. The field describes the requested distribution contract; it does not certify calibration when required.

Paid decisions fail closed. First call `decide(..., estimate_only=True)`: it counts the complete provider request, prices it through `spendguard.pricing`, spends `$0`, and persists a matching approval. Only a later identical `decide(...)` may reach the gated metered transport. Provider usage remains authoritative for the real receipt. Configure approval storage with `JEVLIKEDECISIONS_APPROVAL_DIR`, approval signing with `JEVLIKEDECISIONS_APPROVAL_INTEGRITY_KEY`, receipt storage with `JEVLIKEDECISIONS_RECEIPT_DIR`, and an alternate package configuration with `JEVLIKEDECISIONS_CONFIG`. Credentials are read only from the environment variable named by the selected backend configuration.

## Python, CLI, HTTP, and MCP

Python:

```python
from jevlikedecisions import decide

estimate = decide(input_parts, questions, backend=backend, intent=intent, estimate_only=True)
result = decide(input_parts, questions, backend=backend, intent=intent)
```

CLI input comes from a JSON file or stdin. `estimate` prints the estimate and its `approval_path`; `decide` prints answers and receipt fields. The `backend` value accepts either a canonical name or an alias reported by `backends`.

```console
jevlikedecisions backends
jevlikedecisions estimate --backend "$JEVLIKEDECISIONS_BACKEND" request.json
jevlikedecisions decide --backend "$JEVLIKEDECISIONS_BACKEND" request.json
```

The CLI flag fills a missing request `backend`; if both the flag and JSON field are present they must agree or the command fails. CLI exit codes follow `sysexits`: `0` (`EX_OK`) for success, `64` (`EX_USAGE`) for malformed input, `65` (`EX_DATAERR`) for a typed decision refusal, and `70` (`EX_SOFTWARE`) for an internal fault. The CLI reports every failure as `TypeName: message` on stderr; catching internal faults gives automation a stable code without hiding the fault.

The standard-library HTTP server exposes `POST /backends`, `POST /estimate`, and `POST /decide`; its `backend` field accepts the same config-derived canonical names and aliases. HTTP `400` identifies a malformed request, `422` identifies a typed decision refusal, `503` specifically identifies `DecisionUnavailable`, and `500` identifies an internal fault. Keeping unavailable decisions at `503` preserves the established retry signal, while the JSON `error` field retains the exact typed refusal. Set `JEVLIKEDECISIONS_HTTP_PORT`; optionally set `JEVLIKEDECISIONS_HTTP_HOST`. With no `JEVLIKEDECISIONS_OPERATOR_TOKEN` it binds only to loopback. When that token is set, callers must send it as an operator bearer token. A paid caller must successfully call `/estimate` before `/decide` with the identical request.

```console
jevlikedecisions-http --host "$JEVLIKEDECISIONS_HTTP_HOST" --port "$JEVLIKEDECISIONS_HTTP_PORT"
```

The package's sole runtime dependency is `llm-spendguard`; its MCP surface is a standard-library JSON-RPC stdio server. The `list_decision_backends`, `estimate_decision`, and `make_decision` tools expose the same selection contract and send evidence whole. Enumeration is derived from configuration and reports canonical name, aliases, provider, model, usability, and every missing precondition; both decision tools accept exactly those canonical names and aliases. The approval from the estimate tool must exist before a paid call through the decision tool.

```console
jevlikedecisions-mcp
```

## Paid live-proof utility

`scripts/run_paid_decision_live_proof.py` preserves the reproducible paid-call proof harness in the repository. It takes a configuration path rather than embedding one; `examples/paid_decision_live_proof.example.json` shows the input shape. By default it estimates every configured backend, prints the per-backend and total token/USD estimates, and exits without making a paid call. After reviewing that complete estimate, rerun with the explicit `--authorize-paid-calls` flag to execute every configured proof. Its records include reported-output-token status, cost completeness, and the known input-cost floor alongside the ledger and receipt evidence.

```console
PYTHONPATH=src python scripts/run_paid_decision_live_proof.py examples/paid_decision_live_proof.example.json
PYTHONPATH=src python scripts/run_paid_decision_live_proof.py examples/paid_decision_live_proof.example.json --authorize-paid-calls
```

`assert_all_decisions_via_jevlikedecisions(...)` cross-checks package-owned receipts against spendguard rows by `ledger_id` and fails closed when there are no auditable receipts. Spendguard's calls table has no endpoint, route, or via columns, so this proves receipt-backed package calls; it cannot prove that no outside caller contacted a Decisions endpoint.

## Troubleshooting

A framework Python without a usable certificate bundle may need `SSL_CERT_FILE` set to the path of an installed CA bundle, such as the one supplied by certifi, before a real HTTPS call. Never put keys or secret values in configuration or examples.

`anyjev` remains a named v1 schema gap: its authoritative independent wire contract was unavailable, so no fields were invented. It can be added as another adapter without changing `decide()`.
## General LLM generation

This package wraps the jev and OpenAI Decisions APIs only. General text generation was removed; use `llm-spendguard` for general LLM calls.
