| | #!/usr/bin/env python3 | | | """jev: calibrated, typed decisions from TypeSafe's Jev model, for agents. | | | | | | Jev is a "System One" model. It does not generate text. You hand it a STATE | | | (text or JSON) and typed QUESTIONS, and it returns typed answers with | | | calibrated probabilities in ~250 ms for $0.042 per million input tokens. | | | | | | noul yes/no -> probability that the answer is yes (0..1) | | | choice pick one -> chosen option + probability per option + confidence | | | score rate on rubric-> position on your ordered levels + confidence | | | | | | Progressive disclosure: | | | jev short help (this) | | | jev guide the agent playbook: when to use it, how to ask, how to read | | | jev guide <topic> depth on one topic (jev guide --list) | | | jev examples copy-paste examples for the common jobs | | | jev docs live index of the vendor docs | | | | | | Zero dependencies. Importable as a library: | | | sys.path.insert(0, "/path/to/dir/containing/jev.py") | | | from jev import JevClient, noul, choice, score | | | | | | Auth: --api-key, else $TYPESAFE_API_KEY, else ~/.config/typesafe/api_key (0600). | | | """ | | | | | | from future import annotations | | | | | | import argparse | | | import concurrent.futures as cf | | | import json | | | import os | | | import random | | | import sys | | | import time | | | import urllib.error | | | import urllib.request | | | from datetime import datetime, timedelta | | | from pathlib import Path | | | | | | VERSION = "1.0.0" |
| | API_BASE = os.environ.get("TYPESAFE_BASE_URL", "https://api.typesafe.ai").rstrip("/") |
| | DEFAULT_MODEL = os.environ.get("TYPESAFE_DEFAULT_MODEL", "jev-latest") |
| | CONFIG_DIR = Path.home() / ".config" / "typesafe" |
| | KEY_FILE = CONFIG_DIR / "api_key" | | | USAGE_FILE = CONFIG_DIR / "usage.jsonl" | | | CONFIG_FILE = CONFIG_DIR / "config.json" | | | PRICE_PER_MTOK_IN = 0.042 # USD per million input tokens (jev-1.13 list price); output tokens are free | | | RATE_LIMIT_RPM = 1200 # vendor-published, adjusts without notice | | | RATE_LIMIT_TPS = 250_000 | | | DOCS = "https://docs.typesafe.ai" | | | MAX_CHOICE_OPTIONS = 255 | | | MAX_SCORE_LEVELS = 10 | | | STATE_CHAR_BUDGET = 120_000 # ~30k tokens; the state+longest-question budget is 32k tokens | | | | | | # --------------------------------------------------------------------------- errors | | | | | | | | | class JevError(Exception): | | | exit_code = 4 # API error after retries | | | | | | | | | class UsageError(JevError): | | | exit_code = 2 | | | | | | | | | class AuthError(JevError): | | | exit_code = 3 | | | | | | | | | class NetworkError(JevError): | | | exit_code = 5 | | | | | | | | | # --------------------------------------------------------------------------- auth | | | | | | | | | def resolve_key(explicit: str | None = None) -> tuple[str, str]: | | | """Return (key, source). Order: explicit, $TYPESAFE_API_KEY, key file.""" | | | if explicit: |
| | return explicit.strip(), "--api-key" |
| | env = os.environ.get("TYPESAFE_API_KEY", "").strip() |
| | if env: | | | return env, "$TYPESAFE_API_KEY" |
| | if KEY_FILE.exists(): |
| | key = KEY_FILE.read_text().strip() |
| | if key: |
| | return key, str(KEY_FILE) |
| | raise AuthError( |
| | "no API key. Run jev auth set <key> (stored 0600 in " |
| | f"{KEY_FILE}) or export TYPESAFE_API_KEY. Keys: https://console.typesafe.ai/keys" |
| | ) |
| | |
| | |
| | def mask(key: str) -> str: |
| | return key[:10] + "…" + key[-4:] if len(key) > 16 else "…" |
| | | | | | | | # --------------------------------------------------------------------------- question builders | | | | | | | | | def noul(instructions, true=None, false=None) -> dict: | | | """Yes/no question. Returns P(yes). Optional criteria say what yes and no mean.""" | | | q = {"type": "noul", "instructions": instructions} | | | if true is not None or false is not None: | | | crit = {} | | | if true is not None: | | | crit["true"] = true | | | if false is not None: |
| | crit["false"] = false |
| | q["criteria"] = crit |
| | return q |
| | |
| | |
| | def choice(instructions, options) -> dict: |
| | """Pick one option. options is a dict {name: description|None} or a list of names.""" |
| | if isinstance(options, (list, tuple)): |
| | options = {str(o): None for o in options} |
| | return {"type": "choice", "instructions": instructions, "criteria": dict(options)} |
| | |
| | |
| | def score(instructions, levels) -> dict: |
| | """Rate on an ordered rubric. levels is an ordered list, low to high, 2..10 entries.""" |
| | return {"type": "score", "instructions": instructions, "criteria": list(levels)} |
| | |
| | |
| | def validate_questions(questions: dict) -> None: |
| | if not isinstance(questions, dict) or not questions: |
| | raise UsageError("questions must be a non-empty map of id -> question") |
| | for qid, q in questions.items(): |
| | if not isinstance(q, dict) or "type" not in q: |
| | raise UsageError(f"question {qid!r}: must be an object with a `type`") |
| | t = q["type"] |
| | if t not in ("noul", "choice", "score"): |
| | raise UsageError(f"question {qid!r}: type must be noul, choice or score, not {t!r}") |
| | if "instructions" not in q: |
| | raise UsageError(f"question {qid!r}: missing `instructions`") |
| | crit = q.get("criteria") |
| | if t == "choice": |
| | if not isinstance(crit, dict) or len(crit) < 2: |
| | raise UsageError(f"choice {qid!r}: criteria must map at least 2 options to descriptions (null allowed)") |
| | if len(crit) > MAX_CHOICE_OPTIONS: |
| | raise UsageError(f"choice {qid!r}: at most {MAX_CHOICE_OPTIONS} options") |
| | elif t == "score": |
| | if not isinstance(crit, list) or not 2 <= len(crit) <= MAX_SCORE_LEVELS: |
| | raise UsageError(f"score {qid!r}: criteria must be an ordered list of 2..{MAX_SCORE_LEVELS} level descriptions") |
| | elif t == "noul" and crit is not None: |
| | if not isinstance(crit, dict) or not set(crit) <= {"true", "false"}: |
| | raise UsageError(f"noul {qid!r}: criteria may only have `true` and `false` keys") |
| | |
| | |
| | # --------------------------------------------------------------------------- client |
| | |
| | |
| | class JevClient: |
| | """Thin HTTP client with retries. ask() returns the raw API response dict.""" |
| | |
| | def init(self, api_key=None, model=None, timeout=30.0, retries=5, base_url=None, record=True, label="lib"): |
| | self.api_key, self.key_source = resolve_key(api_key) |
| | self.model = model or DEFAULT_MODEL |
| | self.timeout = timeout |
| | self.retries = retries |
| | self.base_url = (base_url or API_BASE).rstrip("/") |
| | self.record = record |
| | self.label = label |
| | self.last_ms = 0.0 |
| | self.last_request_id = "" |
| | self.last_attempts = 0 |
| | |
| | def _request(self, method: str, path: str, body=None) -> dict: |
| | data = json.dumps(body).encode() if body is not None else None |
| | headers = { |
| | "Authorization": f"Bearer {self.api_key}", |
| | "Content-Type": "application/json", | | | "User-Agent": f"jev-cli/{VERSION}", | | | } | | | attempt = 0 | | | while True: | | | attempt += 1 |
| | req = urllib.request.Request(self.base_url + path, data=data, method=method, headers=headers) |
| | t0 = time.time() |
| | try: |
| | with urllib.request.urlopen(req, timeout=self.timeout) as r: |
| | self.last_ms = (time.time() - t0) * 1000 |
| | self.last_request_id = r.headers.get("x-typesafe-request-id", "") |
| | self.last_attempts = attempt | | | return json.loads(r.read().decode()) | | | except urllib.error.HTTPError as e: |
| | self.last_ms = (time.time() - t0) * 1000 |
| | text = e.read().decode(errors="replace") |
| | if e.code in (401, 403): |
| | raise AuthError(f"HTTP {e.code}: API key rejected ({self.key_source}). {text[:300]}") |
| | if e.code in (429, 529) or e.code >= 500: |
| | if attempt <= self.retries: |
| | ra = e.headers.get("retry-after") |
| | delay = float(ra) if ra and ra.replace(".", "", 1).isdigit() else min(8.0, 0.5 * 2 ** (attempt - 1)) |
| | time.sleep(delay + random.uniform(0, 0.25)) |
| | continue |
| | raise JevError(f"HTTP {e.code} after {attempt} attempts: {text[:300]}") |
| | raise JevError(f"HTTP {e.code}: {text[:600]}") |
| | except (urllib.error.URLError, TimeoutError, OSError) as e: |
| | if attempt <= self.retries: |
| | time.sleep(min(8.0, 0.5 * 2 ** (attempt - 1))) |
| | continue |
| | raise NetworkError(f"network error after {attempt} attempts: {e}") |
| | |
| | def models(self) -> dict: |
| | return self._request("GET", "/v1/models") |
| | |
| | def ask(self, state, questions: dict, model: str | None = None) -> dict: |
| | """POST /v1/systemone. state is str|dict|list. Returns the raw response.""" |
| | if state is None or state == "" or state == {} or state == []: |
| | raise UsageError("state is empty") |
| | validate_questions(questions) |
| | body = {"state": state, "model": model or self.model, "questions": questions} |
| | try: | | | resp = self._request("POST", "/v1/systemone", body) | | | except JevError as e: | | | if self.record: | | | record_usage(None, self, len(questions), err=e) | | | raise | | | if self.record: | | | record_usage(resp, self, len(questions)) | | | return resp | | | | | | | | | def load_config() -> dict: | | | try: |
| | return json.loads(CONFIG_FILE.read_text()) if CONFIG_FILE.exists() else {} |
| | except (OSError, json.JSONDecodeError): |
| | return {} |
| | | | | |
| | def save_config(cfg: dict) -> None: |
| | CONFIG_DIR.mkdir(parents=True, exist_ok=True) |
| | CONFIG_FILE.write_text(json.dumps(cfg, indent=2) + "\n") |
| | | | | |
| | def price_per_mtok() -> float: |
| | v = load_config().get("price_per_mtok_in") |
| | return float(v) if v else PRICE_PER_MTOK_IN | | | | | | | | | def record_usage(resp: dict | None, client: "JevClient", n_questions: int, err: Exception | None = None) -> None: | | | """Append one row per request (success or failure) to the local ledger.""" | | | try: |
| | CONFIG_DIR.mkdir(parents=True, exist_ok=True) |
| | row = { |
| | "ts": datetime.now().isoformat(timespec="seconds"), |
| | "cmd": client.label, | | | "agent": os.environ.get("JEV_AGENT") or os.environ.get("AGENT_ID") or "", | | | "q": n_questions, | | | "ms": round(client.last_ms), | | | } | | | if resp is not None: |
| | u = resp.get("usage") or {} |
| | row.update({"model": resp.get("model"), "in": u.get("input_tokens", 0), "out": u.get("output_tokens", 0)}) |
| | if client.last_request_id: |
| | row["rid"] = client.last_request_id |
| | if client.last_attempts > 1: |
| | row["attempts"] = client.last_attempts |
| | else: |
| | msg = str(err) |
| | code = msg.split(":", 1)[0].replace("HTTP ", "").strip() if msg.startswith("HTTP") else type(err).__name__ |
| | row.update({"in": 0, "out": 0, "err": code}) |
| | with USAGE_FILE.open("a") as f: |
| | f.write(json.dumps(row) + "\n") |
| | except OSError: |
| | pass |
| | |
| | |
| | def cost_usd(input_tokens: int) -> float: |
| | return input_tokens / 1_000_000 * price_per_mtok() |
| | |
| | |
| | # --------------------------------------------------------------------------- input parsing |
| | |
| | |
| | def parse_value(v: str): |
| | """@path reads a file (.json parsed). JSON-looking text is parsed. Else string.""" |
| | if v.startswith("@"): |
| | p = Path(v[1:]).expanduser() |
| | if not p.exists(): |
| | raise UsageError(f"file not found: {p}") |
| | txt = p.read_text() |
| | if p.suffix.lower() in (".json", ".jsonl"): |
| | try: | | | return json.loads(txt) if p.suffix.lower() == ".json" else [json.loads(line) for line in txt.splitlines() if line.strip()] | | | except json.JSONDecodeError as e: | | | raise UsageError(f"{p}: invalid JSON: {e}") | | | return txt |
| | s = v.strip() |
| | if s and s[0] in "{[" or s in ("true", "false", "null"): |
| | try: | | | return json.loads(s) | | | except json.JSONDecodeError: | | | pass | | | return v | | | | | | |
| | def build_state(args) -> object: |
| | srcs = [n for n in ("state", "state_file", "state_json") if getattr(args, n, None)] |
| | if len(srcs) > 1: |
| | raise UsageError("give the state one way: --state, --state-file, or --state-json (--field may be added to any of them, and becomes `text`)") |
| | state = None |
| | if getattr(args, "state", None): |
| | state = sys.stdin.read() if args.state == "-" else (parse_value(args.state) if args.state.startswith("@") else args.state) |
| | elif getattr(args, "state_file", None): |
| | state = parse_value("@" + args.state_file) |
| | elif getattr(args, "state_json", None): |
| | try: | | | state = json.loads(args.state_json) | | | except json.JSONDecodeError as e: |
| | raise UsageError(f"--state-json: invalid JSON: {e}") |
| | if getattr(args, "field", None): |
| | obj = {} |
| | if state is not None: | | | obj["text"] = state | | | for kv in args.field: |
| | if "=" not in kv: |
| | raise UsageError(f"--field expects key=value, got {kv!r}") |
| | k, v = kv.split("=", 1) |
| | obj[k.strip()] = parse_value(v) |
| | state = obj | | | if state is None: | | | state = implicit_stdin() | | | if not state: |
| | raise UsageError("no state. Use --state TEXT, --state-file PATH, --state-json JSON, --field k=v, or pipe text on stdin") |
| | n = len(state) if isinstance(state, str) else len(json.dumps(state)) |
| | if n > STATE_CHAR_BUDGET: |
| | eprint(f"jev: warning: state is {n:,} chars (~{n // 4:,} tokens); the per-request budget is ~32k tokens for state + longest question. Filter first, or use jev batch/ jev rank which chunk.") |
| | return state |
| | |
| | |
| | def implicit_stdin(timeout: float = 1.5) -> str | None: |
| | """Read stdin only when it is not a terminal AND has data ready. A harness that leaves |
| | stdin open with no writer would otherwise block forever; --state - forces a blocking read.""" |
| | if sys.stdin is None or sys.stdin.isatty(): |
| | return None |
| | try: |
| | import select |
| | ready, _, _ = select.select([sys.stdin], [], [], timeout) |
| | if not ready: |
| | return None |
| | except (OSError, ValueError): |
| | pass |
| | return sys.stdin.read() |
| | |
| | |
| | def parse_kv_options(items: list[str]) -> dict: |
| | """['billing=Payments', 'sales'] -> {'billing': 'Payments', 'sales': None}""" |
| | out = {} |
| | for it in items: |
| | if "=" in it: |
| | k, v = it.split("=", 1) |
| | out[k.strip()] = parse_value(v) if v != "" else None |
| | else: | | | out[it.strip()] = None | | | return out | | | | | | |
| | class QAction(argparse.Action): |
| | """Collects --noul/--choice/--score in declaration order.""" |
| | |
| | def __call__(self, parser, ns, values, option_string=None): |
| | lst = getattr(ns, "qlist", None) or [] |
| | lst.append((self.dest, list(values))) |
| | ns.qlist = lst | | | | | | |
| | def questions_from_args(args) -> dict: |
| | qs: dict = {} |
| | if getattr(args, "questions_file", None): |
| | v = parse_value("@" + args.questions_file) |
| | if not isinstance(v, dict): |
| | raise UsageError("--questions-file must contain a JSON object of id -> question") |
| | qs.update(v) |
| | if getattr(args, "questions_json", None): |
| | try: | | | qs.update(json.loads(args.questions_json)) | | | except json.JSONDecodeError as e: |
| | raise UsageError(f"--questions-json: invalid JSON: {e}") |
| | for kind, vals in getattr(args, "qlist", None) or []: |
| | if len(vals) < 2: |
| | raise UsageError(f"--{kind} needs ID INSTRUCTIONS [...]") |
| | qid, instr, rest = vals[0], parse_value(vals[1]), vals[2:] |
| | if kind == "noul": |
| | kv = parse_kv_options(rest) |
| | extra = set(kv) - {"true", "false"} |
| | if extra: |
| | raise UsageError(f"--noul {qid}: after the instructions only true=... and false=... are allowed, got {sorted(extra)}") |
| | qs[qid] = noul(instr, kv.get("true"), kv.get("false")) |
| | elif kind == "choice": |
| | if len(rest) < 2: |
| | raise UsageError(f"--choice {qid}: give at least 2 options as NAME or NAME=description") |
| | qs[qid] = choice(instr, parse_kv_options(rest)) |
| | elif kind == "score": |
| | if len(rest) < 2: |
| | raise UsageError(f"--score {qid}: give 2..{MAX_SCORE_LEVELS} level descriptions, low to high") |
| | qs[qid] = score(instr, [parse_value(r) for r in rest]) |
| | if not qs: |
| | raise UsageError("no questions. Use --noul/--choice/--score, --questions-file, or --questions-json (see jev ask --help)") |
| | return qs |
| | |
| | |
| | # --------------------------------------------------------------------------- rendering |
| | |
| | |
| | def eprint(*a, **k): |
| | print(*a, file=sys.stderr, **k) |
| | | | | |
| | def describe_level(desc) -> str: |
| | if isinstance(desc, dict): |
| | for k in ("what", "level", "description", "name"): |
| | if k in desc: |
| | return str(desc[k]) |
| | return json.dumps(desc)[:80] |
| | return str(desc) |
| | | | | |
| | def render_answer(qid: str, a: dict) -> str: |
| | t = a.get("type") |
| | if t == "noul": |
| | p = a["noul"] |
| | return f"{qid:<24} noul {p:.2f} {'yes' if p >= 0.5 else 'no'}" |
| | if t == "choice": |
| | probs = a.get("probabilities", {}) |
| | best = a["choice"] |
| | others = ", ".join(f"{k} {v:.2f}" for k, v in sorted(probs.items(), key=lambda kv: -kv[1]) if k != best) |
| | return f"{qid:<24} choice {best} p={probs.get(best, 0):.2f} conf={a.get('confidence', 0):.2f} [{others}]" |
| | if t == "score": |
| | legend = a.get("legend", {}) |
| | top = max(len(legend) - 1, 1) |
| | s = a["score"] |
| | nearest = describe_level(legend.get(str(int(round(s))), "")) |
| | return f"{qid:<24} score {s:.2f}/{top} conf={a.get('confidence', 0):.2f} ≈ {nearest}" |
| | return f"{qid:<24} {t} {json.dumps(a)}" |
| | | | | |
| | def render_response(resp: dict, ms: float, verbose: bool) -> str: |
| | lines = [render_answer(qid, a) for qid, a in resp.get("answers", {}).items()] |
| | u = resp.get("usage", {}) |
| | lines.append(f"· {resp.get('model')} · {u.get('input_tokens', 0)} in / {u.get('output_tokens', 0)} out tokens · {ms:.0f} ms · ${cost_usd(u.get('input_tokens', 0)):.6f}") |
| | return "\n".join(lines) |
| | | | | | | | def emit(args, resp: dict, ms: float, extra: dict | None = None) -> None: | | | if args.json: | | | out = dict(resp) | | | if extra: |
| | out.update(extra) |
| | print(json.dumps(out, indent=None if args.compact else 2)) |
| | else: | | | print(render_response(resp, ms, args.verbose)) | | | | | | | | | # --------------------------------------------------------------------------- commands | | | | | | |
| | def cmd_ask(args) -> int: |
| | state = build_state(args) |
| | qs = questions_from_args(args) |
| | c = JevClient(args.api_key, args.model, timeout=args.timeout, label="ask") |
| | resp = c.ask(state, qs) |
| | emit(args, resp, c.last_ms) |
| | return 0 | | | | | | |
| | def cmd_yes(args) -> int: |
| | state = build_state(args) |
| | q = noul(parse_value(args.instructions), args.true, args.false) |
| | c = JevClient(args.api_key, args.model, timeout=args.timeout, label="yes") |
| | resp = c.ask(state, {"q": q}) |
| | p = resp["answers"]["q"]["noul"] |
| | lo, hi = (args.band if args.band else (args.threshold, args.threshold)) |
| | if p >= hi: |
| | verdict, code = "yes", 0 | | | elif p < lo: | | | verdict, code = "no", 1 | | | else: | | | verdict, code = "uncertain", 1 | | | if args.json: | | | print(json.dumps({"noul": p, "verdict": verdict, "threshold": args.threshold, "band": args.band, "model": resp["model"], "usage": resp["usage"]})) | | | else: | | | print(f"{p:.3f} {verdict}") | | | return code | | | | | | |
| | def cmd_pick(args) -> int: |
| | state = build_state(args) |
| | opts = parse_kv_options(args.options) |
| | if len(opts) < 2: |
| | raise UsageError("pick needs at least 2 options (NAME or NAME=description)") |
| | c = JevClient(args.api_key, args.model, timeout=args.timeout, label="pick") |
| | resp = c.ask(state, {"q": choice(parse_value(args.instructions), opts)}) |
| | a = resp["answers"]["q"] |
| | conf = a.get("confidence", 0.0) |
| | ok = conf >= args.min_confidence |
| | if args.json: | | | print(json.dumps({"choice": a["choice"], "confidence": conf, "probabilities": a["probabilities"], "verdict": "ok" if ok else "uncertain", "min_confidence": args.min_confidence, "model": resp["model"], "usage": resp["usage"]})) | | | else: | | | ranked = ", ".join(f"{k} {v:.2f}" for k, v in sorted(a["probabilities"].items(), key=lambda kv: -kv[1])) | | | if ok: | | | print(f"{a['choice']} conf={conf:.2f} [{ranked}]") | | | else: | | | print(f"uncertain best={a['choice']} conf={conf:.2f} < {args.min_confidence} [{ranked}]") | | | return 0 if ok else 1 | | | | | | |
| | def cmd_rate(args) -> int: |
| | state = build_state(args) |
| | levels = [parse_value(lv) for lv in args.levels] |
| | if not 2 <= len(levels) <= MAX_SCORE_LEVELS: |
| | raise UsageError(f"rate needs 2..{MAX_SCORE_LEVELS} level descriptions, low to high") |
| | c = JevClient(args.api_key, args.model, timeout=args.timeout, label="rate") |
| | resp = c.ask(state, {"q": score(parse_value(args.instructions), levels)}) |
| | a = resp["answers"]["q"] |
| | conf = a.get("confidence", 0.0) |
| | top = len(levels) - 1 |
| | ok = conf >= args.min_confidence |
| | nearest = int(round(a["score"])) |
| | if args.json: | | | print(json.dumps({"score": a["score"], "top": top, "normalized": a["score"] / top, "nearest_level": nearest, "nearest": describe_level(levels[nearest]), "confidence": conf, "probabilities": a["probabilities"], "verdict": "ok" if ok else "uncertain", "model": resp["model"], "usage": resp["usage"]})) | | | else: |
| | flag = "" if ok else f" uncertain (conf < {args.min_confidence})" |
| | print(f"{a['score']:.2f}/{top} conf={conf:.2f} ≈ {describe_level(levels[nearest])}{flag}") |
| | return 0 if ok else 1 | | | | | | | | | def read_candidates(args) -> list: | | | if args.candidates_file: |
| | v = parse_value("@" + args.candidates_file) |
| | if isinstance(v, list): |
| | return v | | | return [line for line in str(v).splitlines() if line.strip()] | | | if args.candidates: |
| | return list(args.candidates) |
| | txt = implicit_stdin() |
| | if txt: |
| | return [line.rstrip("\n") for line in txt.splitlines() if line.strip()] |
| | raise UsageError("no candidates: give them as arguments, --candidates-file (lines, or a JSON array), or on stdin") |
| | | | | |
| | def chunk_candidates(cands: list, max_chars: int, max_n: int) -> list[list[tuple[int, object]]]: |
| | chunks, cur, size = [], [], 0 |
| | for i, c in enumerate(cands): |
| | n = len(c) if isinstance(c, str) else len(json.dumps(c)) |
| | if cur and (size + n > max_chars or len(cur) >= max_n): |
| | chunks.append(cur) |
| | cur, size = [], 0 |
| | cur.append((i, c)) |
| | size += n | | | if cur: | | | chunks.append(cur) | | | return chunks | | | | | | |
| | def cmd_rank(args) -> int: |
| | cands = read_candidates(args) |
| | if not cands: | | | raise UsageError("no candidates") |
| | c = JevClient(args.api_key, args.model, timeout=args.timeout, label="rank") |
| | levels = [parse_value(lv) for lv in args.levels] if args.levels else None |
| | question_text = args.instructions or ("How well does candidate match query?" if levels else "Is candidate relevant to query?") |
| | if "{i}" in question_text or "candidates[" in question_text: |
| | eprint("jev: warning: refer to the item as `candidate`; each question carries its own item. Positional `candidates[i]` references mis-locate past ~20 items (measured 27% wrong at 150).") |
| | results: list[dict] = [] |
| | usage_in = usage_out = 0 |
| | t0 = time.time() |
| | chunks = chunk_candidates(cands, args.chunk_chars, args.chunk_size) |
| | |
| | def run(chunk): |
| | # The item lives INSIDE its question, not at an index in the state: Jev mis-locates |
| | # candidates[i] in long arrays (measured 2026-09-18: 86/320 wrong at 150 per request, |
| | # 29/320 at 25) while an embedded or keyed item scored 0/320 wrong at 320 per request. |
| | state = {"query": parse_value(args.query)} |
| | if args.context: |
| | state["context"] = parse_value(args.context) |
| | qs = {} |
| | for j, (i, cand) in enumerate(chunk): |
| | instr = {"question": question_text, "candidate": cand} |
| | qs[f"c{j}"] = score(instr, levels) if levels else noul(instr) |
| | resp = c.ask(state, qs) |
| | out = [] |
| | for j, (i, cand) in enumerate(chunk): |
| | a = resp["answers"][f"c{j}"] |
| | if levels: |
| | top = len(levels) - 1 |
| | out.append({"i": i, "p": a["score"] / top, "score": a["score"], "confidence": a.get("confidence"), "candidate": cand}) |
| | else: |
| | out.append({"i": i, "p": a["noul"], "candidate": cand}) |
| | return out, resp.get("usage", {}) |
| | |
| | with cf.ThreadPoolExecutor(max_workers=args.concurrency) as ex: |
| | for out, u in ex.map(run, chunks): |
| | results.extend(out) |
| | usage_in += u.get("input_tokens", 0) |
| | usage_out += u.get("output_tokens", 0) |
| | results.sort(key=lambda r: -r["p"]) |
| | if args.min is not None: | | | results = [r for r in results if r["p"] >= args.min] | | | if args.top: |
| | results = results[: args.top] |
| | ms = (time.time() - t0) * 1000 |
| | if args.json: | | | print(json.dumps({"results": results, "n": len(cands), "requests": len(chunks), "usage": {"input_tokens": usage_in, "output_tokens": usage_out}, "ms": round(ms)}, indent=None if args.compact else 2)) | | | else: | | | for r in results: |
| | cand = r["candidate"] if isinstance(r["candidate"], str) else json.dumps(r["candidate"]) |
| | cand = cand.replace("\n", " ") |
| | if len(cand) > args.width: |
| | cand = cand[: args.width - 1] + "…" |
| | print(f"{r['p']:.3f} #{r['i']:<4} {cand}") |
| | eprint(f"· {len(cands)} candidates · {len(chunks)} request(s) · {usage_in} in tokens · {ms:.0f} ms · ${cost_usd(usage_in):.5f}") |
| | return 0 | | | | | | |
| | def cmd_batch(args) -> int: |
| | qs = questions_from_args(args) |
| | if args.input == "-": |
| | raw = sys.stdin.read() |
| | else: |
| | p = Path(args.input).expanduser() |
| | if not p.exists(): |
| | raise UsageError(f"input not found: {p}") |
| | raw = p.read_text() |
| | items = [] |
| | for n, line in enumerate(raw.splitlines()): |
| | if not line.strip(): |
| | continue | | | if args.text_lines: | | | items.append((n, None, line)) | | | continue | | | try: | | | obj = json.loads(line) | | | except json.JSONDecodeError: | | | items.append((n, None, line)) | | | continue | | | if isinstance(obj, dict) and args.state_key: | | | if args.state_key not in obj: |
| | raise UsageError(f"line {n}: no key {args.state_key!r}") |
| | items.append((n, obj.get(args.id_key), obj[args.state_key])) |
| | else: | | | items.append((n, obj.get(args.id_key) if isinstance(obj, dict) else None, obj)) | | | if not items: | | | raise UsageError("no input rows") | | | c = JevClient(args.api_key, args.model, timeout=args.timeout, label="batch") | | | out_f = open(args.out, "w") if args.out else sys.stdout | | | usage_in = 0 | | | t0 = time.time() | | | errors = 0 | | | | | | def run(item): | | | n, ident, state = item | | | try: |
| | resp = c.ask(state, qs) |
| | row = {"line": n, "answers": resp["answers"], "usage": resp["usage"]} |
| | except JevError as e: | | | row = {"line": n, "error": str(e)} | | | if ident is not None: | | | row["id"] = ident | | | if args.echo: | | | row["state"] = state | | | return row | | | | | | try: |
| | with cf.ThreadPoolExecutor(max_workers=args.concurrency) as ex: |
| | for row in ex.map(run, items): |
| | if "error" in row: | | | errors += 1 | | | else: |
| | usage_in += row["usage"].get("input_tokens", 0) |
| | out_f.write(json.dumps(row) + "\n") |
| | out_f.flush() |
| | finally: | | | if args.out: |
| | out_f.close() |
| | ms = (time.time() - t0) * 1000 |
| | eprint(f"· {len(items)} rows · {errors} errors · {usage_in} in tokens · {ms:.0f} ms · ${cost_usd(usage_in):.5f}" + (f" · wrote {args.out}" if args.out else "")) |
| | return 0 if errors == 0 else 4 |
| | | | | |
| | def cmd_models(args) -> int: |
| | c = JevClient(args.api_key, args.model, timeout=args.timeout, record=False) |
| | resp = c.models() |
| | if args.json: | | | print(json.dumps(resp, indent=2)) | | | else: |
| | for m in resp.get("models", []): |
| | print(f"{m.get('name'):<14} {str(m.get('release_date', ''))[:10]} {m.get('description', '')}") |
| | return 0 | | | | | | |
| | def cmd_auth(args) -> int: |
| | if args.action == "set": |
| | key = args.key or (sys.stdin.readline().strip() if not sys.stdin.isatty() else "") |
| | if not key: |
| | raise UsageError("jev auth set <key> (or pipe the key on stdin)") |
| | CONFIG_DIR.mkdir(parents=True, exist_ok=True) |
| | old = os.umask(0o077) |
| | try: | | | KEY_FILE.write_text(key.strip() + "\n") | | | finally: |
| | os.umask(old) |
| | os.chmod(KEY_FILE, 0o600) |
| | print(f"stored {mask(key)} in {KEY_FILE} (0600)") |
| | return 0 |
| | if args.action == "clear": |
| | if KEY_FILE.exists(): |
| | KEY_FILE.unlink() |
| | print(f"removed {KEY_FILE}") |
| | else: | | | print("no key file") | | | return 0 | | | try: | | | key, src = resolve_key(args.api_key) | | | except AuthError as e: | | | print(f"no key: {e}") | | | return 3 |
| | print(f"key {mask(key)} from {src}") |
| | if src == str(KEY_FILE): |
| | mode = oct(KEY_FILE.stat().st_mode & 0o777) |
| | print(f"file mode {mode}" + ("" if mode == "0o600" else " <- should be 0600")) |
| | return 0 | | | | | | | | | def cmd_doctor(args) -> int: | | | ok = True | | | | | | def step(name, fn): | | | nonlocal ok | | | try: |
| | msg = fn() |
| | print(f"ok {name}: {msg}") |
| | except Exception as e: # noqa: BLE001 | | | ok = False | | | print(f"FAIL {name}: {e}") | | | |
| | step("python", lambda: sys.version.split()[0]) |
| | def key(): |
| | k, src = resolve_key(args.api_key) |
| | return f"{mask(k)} from {src}" |
| | | | | step("key", key) | | | c = None | | | | | | def models(): | | | nonlocal c |
| | c = JevClient(args.api_key, args.model, timeout=args.timeout, record=False) |
| | r = c.models() |
| | return f"{[m['name'] for m in r.get('models', [])]} in {c.last_ms:.0f} ms" |
| | | | | step("GET /v1/models", models) | | | | | | def roundtrip(): | | | r = c.ask("The export button crashes the settings page in Safari but works in Chrome.", |
| | {"is_bug": noul("Does this describe a software bug?"), |
| | "severity": score("How severe?", ["Cosmetic", "Degraded, workaround exists", "Blocking"])}) |
| | a = r["answers"] |
| | return f"model={r['model']} is_bug={a['is_bug']['noul']:.2f} severity={a['severity']['score']:.2f} in {c.last_ms:.0f} ms, {r['usage']['input_tokens']} tokens" |
| | |
| | if c is not None: |
| | step("POST /v1/systemone", roundtrip) |
| | step("usage ledger", lambda: f"{USAGE_FILE} ({USAGE_FILE.stat().st_size if USAGE_FILE.exists() else 0} bytes)") |
| | step("skill", lambda: ", ".join(str(p) for p in (Path.home() / ".claude/skills/jev", Path.home() / ".codex/skills/jev", Path.home() / ".config/opencode/skills/jev") if p.exists()) or "not linked (see jev guide install)") |
| | print("healthy" if ok else "problems found") |
| | return 0 if ok else 4 |
| | |
| | |
| | def load_ledger() -> list[dict]: |
| | if not USAGE_FILE.exists(): |
| | return [] |
| | rows = [] |
| | for line in USAGE_FILE.read_text().splitlines(): |
| | try: |
| | r = json.loads(line) |
| | r["_t"] = datetime.fromisoformat(r["ts"]) |
| | rows.append(r) |
| | except (json.JSONDecodeError, KeyError, ValueError): | | | continue | | | return rows | | | | | | | | | def agent_names(ids: set[str]) -> dict[str, str]: | | | """Agents are attributed by $JEV_AGENT (or $AGENT_ID). Names are whatever you export.""" | | | return {i: (i if i else "(no agent)") for i in ids} | | | | | | |
| | def _agg(sel: list[dict]) -> dict: |
| | ok = [r for r in sel if "err" not in r] |
| | tin = sum(r.get("in", 0) for r in ok) |
| | ms = sorted(r["ms"] for r in ok if r.get("ms")) |
| | pct = lambda p: ms[min(len(ms) - 1, int(len(ms) * p))] if ms else 0 # noqa: E731 |
| | return { |
| | "requests": len(sel), "errors": len(sel) - len(ok), "questions": sum(r.get("q", 0) for r in ok), |
| | "input_tokens": tin, "output_tokens": sum(r.get("out", 0) for r in ok), "usd": round(cost_usd(tin), 6), |
| | "avg_ms": round(sum(ms) / len(ms)) if ms else 0, "p50_ms": pct(0.5), "p95_ms": pct(0.95), "max_ms": ms[-1] if ms else 0, |
| | } | | | | | | |
| | def _peaks(sel: list[dict]) -> dict: |
| | per_min: dict[str, int] = {} |
| | per_sec: dict[str, int] = {} |
| | for r in sel: |
| | per_min[r["ts"][:16]] = per_min.get(r["ts"][:16], 0) + 1 |
| | per_sec[r["ts"]] = per_sec.get(r["ts"], 0) + r.get("in", 0) |
| | rpm = max(per_min.values()) if per_min else 0 |
| | tps = max(per_sec.values()) if per_sec else 0 |
| | return {"peak_rpm": rpm, "peak_rpm_at": max(per_min, key=per_min.get) if per_min else None, "rpm_limit": RATE_LIMIT_RPM, |
| | "peak_tps": tps, "peak_tps_at": max(per_sec, key=per_sec.get) if per_sec else None, "tps_limit": RATE_LIMIT_TPS} |
| | | | | |
| | def cmd_usage(args) -> int: |
| | rows = load_ledger() |
| | if args.label: | | | rows = [r for r in rows if r.get("cmd") == args.label] | | | if args.agent: | | | rows = [r for r in rows if args.agent in (r.get("agent") or "")] | | | if args.since: | | | try: | | | since = datetime.fromisoformat(args.since) | | | except ValueError: |
| | raise UsageError("--since expects YYYY-MM-DD or an ISO timestamp") |
| | rows = [r for r in rows if r["_t"] >= since] |
| | if not rows: | | | print("no usage recorded" + (" for that filter" if (args.label or args.agent or args.since) else " yet")) | | | return 0 |
| | now = datetime.now() |
| | day0 = now.replace(hour=0, minute=0, second=0, microsecond=0) |
| | cfg = load_config() |
| | | | | if args.tail: | | | sel = rows[-args.tail:] | | | if args.json: | | | print(json.dumps([{k: v for k, v in r.items() if k != "_t"} for r in sel], indent=2)) | | | return 0 |
| | names = agent_names({r.get("agent", "") for r in sel}) |
| | print(f"{'ts':<19} {'cmd':<12} {'agent':<22} {'q':>4} {'in':>7} {'ms':>5} rid/err") |
| | for r in sel: |
| | tail = r.get("err") and f"ERR {r['err']}" or r.get("rid", "") |
| | print(f"{r['ts']:<19} {r.get('cmd', '?')[:12]:<12} {names[r.get('agent', '')][:22]:<22} {r.get('q', 0):>4} {r.get('in', 0):>7} {r.get('ms', 0):>5} {tail}") |
| | return 0 | | | | | | if args.by: | | | key = args.by |
| | if key == "day": |
| | groups = {} |
| | for r in rows: |
| | if r["_t"] >= day0 - timedelta(days=args.days - 1): |
| | groups.setdefault(r["ts"][:10], []).append(r) |
| | elif key == "hour": |
| | groups = {} |
| | for r in rows: |
| | if r["_t"] >= now - timedelta(hours=24): |
| | groups.setdefault(r["ts"][:13] + ":00", []).append(r) |
| | elif key in ("cmd", "model"): |
| | groups = {} |
| | for r in rows: |
| | groups.setdefault(str(r.get(key) or "?"), []).append(r) |
| | elif key == "agent": |
| | names = agent_names({r.get("agent", "") for r in rows}) |
| | groups = {} |
| | for r in rows: | | | groups.setdefault(names[r.get("agent", "")], []).append(r) | | | else: |
| | raise UsageError("--by expects day, hour, cmd, agent or model") |
| | table = {k: _agg(v) for k, v in groups.items()} |
| | order = sorted(table) if key in ("day", "hour") else sorted(table, key=lambda k: -table[k]["input_tokens"]) |
| | if args.json: | | | print(json.dumps({k: table[k] for k in order}, indent=2)) | | | return 0 | | | print(f"{key:<24} {'requests':>8} {'errors':>6} {'questions':>9} {'in tokens':>10} {'usd':>9} {'avg ms':>6}") | | | for k in order: |
| | a = table[k] |
| | print(f"{k[:24]:<24} {a['requests']:>8} {a['errors']:>6} {a['questions']:>9} {a['input_tokens']:>10,} {a['usd']:>9.4f} {a['avg_ms']:>6}") |
| | return 0 | | | |
| | windows = { |
| | "today": day0, "yesterday": (day0 - timedelta(days=1), day0), "7d": now - timedelta(days=7), |
| | "30d": now - timedelta(days=30), "month": day0.replace(day=1), "all": datetime.min, |
| | } |
| | summary = {} |
| | for name, w in windows.items(): |
| | lo, hi = (w if isinstance(w, tuple) else (w, None)) |
| | sel = [r for r in rows if r["_t"] >= lo and (hi is None or r["_t"] < hi)] |
| | summary[name] = _agg(sel) |
| | last7 = [r for r in rows if r["_t"] >= now - timedelta(days=7)] |
| | last30 = [r for r in rows if r["_t"] >= now - timedelta(days=30)] |
| | first_ts = rows[0]["_t"] |
| | days_observed = max(1e-9, (now - first_ts).total_seconds() / 86400) |
| | rate7 = summary["7d"]["usd"] / min(7, days_observed) |
| | rate30 = summary["30d"]["usd"] / min(30, days_observed) |
| | forecast = {"usd_per_day_7d": round(rate7, 6), "usd_per_day_30d": round(rate30, 6), |
| | "usd_per_month_at_7d_rate": round(rate7 * 30, 4), "usd_per_month_at_30d_rate": round(rate30 * 30, 4), |
| | "days_observed": round(days_observed, 2)} |
| | credits = None |
| | if cfg.get("credits_usd") is not None: |
| | as_of = datetime.fromisoformat(cfg.get("credits_as_of") or first_ts.isoformat()) |
| | spent = cost_usd(sum(r.get("in", 0) for r in rows if r["_t"] >= as_of)) |
| | remaining = float(cfg["credits_usd"]) - spent |
| | credits = {"credits_usd": float(cfg["credits_usd"]), "as_of": as_of.isoformat(timespec="seconds"), "spent_since": round(spent, 6), |
| | "remaining_usd": round(remaining, 6), "remaining_mtok": round(remaining / price_per_mtok(), 3) if remaining > 0 else 0.0, |
| | "days_left_at_7d_rate": round(remaining / rate7, 1) if rate7 > 0 and remaining > 0 else None} |
| | budget = None |
| | if cfg.get("budget_usd_month"): |
| | b = float(cfg["budget_usd_month"]) |
| | mtd = summary["month"]["usd"] |
| | dim = ((day0.replace(day=28) + timedelta(days=4)).replace(day=1) - timedelta(days=1)).day |
| | budget = {"budget_usd_month": b, "mtd_usd": mtd, "pct_used": round(100 * mtd / b, 1) if b else None, |
| | "projected_month_end_usd": round(mtd + rate7 * (dim - now.day), 4), "days_left_in_month": dim - now.day} |
| | errors = {} |
| | for r in last7: | | | if "err" in r: |
| | errors[r["err"]] = errors.get(r["err"], 0) + 1 |
| | by_cmd = {} |
| | for r in rows: |
| | by_cmd.setdefault(r.get("cmd") or "?", []).append(r) |
| | names = agent_names({r.get("agent", "") for r in rows}) |
| | by_agent = {} |
| | for r in rows: |
| | by_agent.setdefault(names[r.get("agent", "")], []).append(r) |
| | by_model = {} |
| | for r in rows: |
| | if r.get("model"): |
| | by_model.setdefault(r["model"], []).append(r) |
| | peaks = _peaks(last30) |
| | out = { |
| | "price_per_mtok_in": price_per_mtok(), "ledger": str(USAGE_FILE), "rows": len(rows), "first": rows[0]["ts"], "last": rows[-1]["ts"], |
| | "windows": summary, "by_cmd": {k: _agg(v) for k, v in by_cmd.items()}, "by_agent": {k: _agg(v) for k, v in by_agent.items()}, |
| | "by_model": {k: _agg(v) for k, v in by_model.items()}, "errors_7d": errors, "throughput_30d": peaks, |
| | "forecast": forecast, "credits": credits, "budget": budget, | | | } | | | if args.json: | | | print(json.dumps(out, indent=2)) | | | return 0 | | | |
| | print(f"jev usage · ${price_per_mtok()}/Mtok in, output free · {len(rows)} requests since {rows[0]['ts'][:16]} · {USAGE_FILE}") |
| | print(f"\n{'window':<10} {'requests':>8} {'errors':>6} {'questions':>9} {'in tokens':>10} {'out':>7} {'usd':>9} {'avg ms':>6}") |
| | for name, a in summary.items(): |
| | print(f"{name:<10} {a['requests']:>8} {a['errors']:>6} {a['questions']:>9} {a['input_tokens']:>10,} {a['output_tokens']:>7,} {a['usd']:>9.4f} {a['avg_ms']:>6}") |
| | a7 = summary["7d"] |
| | print(f"\nlatency 7d p50 {a7['p50_ms']} ms · p95 {a7['p95_ms']} ms · max {a7['max_ms']} ms · avg questions/request {a7['questions'] / max(1, a7['requests'] - a7['errors']):.1f} · avg tokens/request {a7['input_tokens'] / max(1, a7['requests'] - a7['errors']):,.0f}") | | | print(f"throughput peak {peaks['peak_rpm']} req/min ({100 * peaks['peak_rpm'] / RATE_LIMIT_RPM:.1f}% of {RATE_LIMIT_RPM}) at {peaks['peak_rpm_at']} · peak {peaks['peak_tps']:,} tok/s ({100 * peaks['peak_tps'] / RATE_LIMIT_TPS:.2f}% of {RATE_LIMIT_TPS:,}) · 30d window") | | | print("errors 7d " + (", ".join(f"{k} x{v}" for k, v in sorted(errors.items(), key=lambda kv: -kv[1])) if errors else "none")) | | | print(f"forecast ${forecast['usd_per_day_7d']:.4f}/day at the 7d rate -> ${forecast['usd_per_month_at_7d_rate']:.2f}/month · ${forecast['usd_per_month_at_30d_rate']:.2f}/month at the 30d rate") | | | if credits: |
| | dl = f" · ~{credits['days_left_at_7d_rate']} days left at the 7d rate" if credits["days_left_at_7d_rate"] else "" |
| | print(f"credits ${credits['remaining_usd']:.4f} remaining of ${credits['credits_usd']:.2f} set {credits['as_of'][:16]} ({credits['remaining_mtok']:.1f} Mtok){dl}") |
| | else: |
| | print("credits not configured. The API exposes no balance; read it at https://console.typesafe.ai and run jev config set credits <usd>") |
| | if budget: |
| | print(f"budget ${budget['mtd_usd']:.4f} of ${budget['budget_usd_month']:.2f} this month ({budget['pct_used']}%) · projected month end ${budget['projected_month_end_usd']:.2f}") |
| | print(f"\n{'by command':<24} {'requests':>8} {'questions':>9} {'in tokens':>10} {'usd':>9}") |
| | for k, v in sorted(by_cmd.items(), key=lambda kv: -sum(r.get('in', 0) for r in kv[1]))[:12]: |
| | a = _agg(v) |
| | print(f"{k[:24]:<24} {a['requests']:>8} {a['questions']:>9} {a['input_tokens']:>10,} {a['usd']:>9.4f}") |
| | print(f"\n{'by agent':<24} {'requests':>8} {'questions':>9} {'in tokens':>10} {'usd':>9}") |
| | for k, v in sorted(by_agent.items(), key=lambda kv: -sum(r.get('in', 0) for r in kv[1]))[:12]: |
| | a = _agg(v) |
| | print(f"{k[:24]:<24} {a['requests']:>8} {a['questions']:>9} {a['input_tokens']:>10,} {a['usd']:>9.4f}") |
| | if len(by_model) > 1: |
| | print("\nby model: " + ", ".join(f"{k} {len(v)}" for k, v in by_model.items())) |
| | print("\nmore: --by day\|hour\|cmd\|agent\|model · --tail N · --since DATE · --label X · --agent X · --json · jev config · jev cost") |
| | return 0 |
| | |
| | |
| | CONFIG_KEYS = { |
| | "credits": ("credits_usd", float, "prepaid balance in USD, read off https://console.typesafe.ai; jev usage counts down from it"), |
| | "credits_as_of": ("credits_as_of", str, "ISO timestamp the balance was read (default: when you set it)"), |
| | "budget": ("budget_usd_month", float, "monthly spend budget in USD; jev usage shows % used and a month-end projection"), |
| | "price": ("price_per_mtok_in", float, f"USD per million input tokens if the vendor changes it (default {PRICE_PER_MTOK_IN})"), |
| | } |
| | |
| | |
| | def cmd_config(args) -> int: |
| | cfg = load_config() |
| | if args.action == "show" or not args.action: |
| | if args.json: | | | print(json.dumps(cfg, indent=2)) | | | return 0 |
| | print(f"{CONFIG_FILE}" + ("" if CONFIG_FILE.exists() else " (not created yet)")) |
| | for k, (real, _, desc) in CONFIG_KEYS.items(): |
| | print(f" {k:<14} {str(cfg.get(real, '-')):<24} {desc}") |
| | return 0 | | | if args.action == "unset": | | | if not args.key or args.key not in CONFIG_KEYS: |
| | raise UsageError(f"jev config unset <{'\|'.join(CONFIG_KEYS)}>") |
| | cfg.pop(CONFIG_KEYS[args.key][0], None) |
| | if args.key == "credits": |
| | cfg.pop("credits_as_of", None) |
| | save_config(cfg) |
| | print(f"unset {args.key}") |
| | return 0 | | | if args.action == "set": | | | if not args.key or args.key not in CONFIG_KEYS or args.value is None: |
| | raise UsageError(f"jev config set <{'\|'.join(CONFIG_KEYS)}> <value>") |
| | real, typ, _ = CONFIG_KEYS[args.key] |
| | try: | | | val = typ(args.value) | | | except ValueError: |
| | raise UsageError(f"{args.key} expects a {typ.__name__}") |
| | if args.key == "credits_as_of": |
| | try: | | | datetime.fromisoformat(val) | | | except ValueError: |
| | raise UsageError("credits_as_of expects an ISO timestamp, e.g. 2026-09-19T14:00") |
| | cfg[real] = val |
| | if args.key == "credits": |
| | cfg["credits_as_of"] = args.as_of or datetime.now().isoformat(timespec="seconds") |
| | save_config(cfg) |
| | print(f"set {args.key} = {val}" + (f" (as of {cfg['credits_as_of']})" if args.key == "credits" else "")) |
| | return 0 | | | raise UsageError("jev config [show|set|unset]") | | | | | | | | | CHARS_PER_TOKEN = 4.0 # rough English heuristic; --exact measures the real count with one tiny call | | | | | | | | | def cmd_cost(args) -> int: | | | """Estimate tokens and cost for a prospective job before running it.""" |
| | state = build_state(args) |
| | text = state if isinstance(state, str) else json.dumps(state) |
| | est_state = int(len(text) / CHARS_PER_TOKEN) |
| | q_tokens = int(args.question_chars / CHARS_PER_TOKEN) | | | exact = None | | | if args.exact: |
| | c = JevClient(args.api_key, args.model, timeout=args.timeout, label="cost") |
| | r = c.ask(state, {"probe": noul("Is this text written in English?")}) |
| | exact = r["usage"]["input_tokens"] |
| | est_state = max(0, exact - q_tokens - 40) # subtract the probe question and request overhead | | | per_request = est_state + args.questions * q_tokens | | | fits = per_request <= 64_000 and (est_state + q_tokens) <= 32_000 | | | n_items = args.items or 1 | | | if args.items: | | | # rank/batch shape: each item is its own request if --per-item, else items are packed with the state repeated per request | | | if args.per_item: | | | total = per_request * n_items | | | requests = n_items | | | else: |
| | per_req_items = max(1, min(250, (64_000 - est_state) // max(1, q_tokens))) |
| | requests = -(-n_items // per_req_items) |
| | total = requests * est_state + n_items * q_tokens | | | else: | | | total, requests = per_request, 1 | | | out = {"state_tokens": est_state, "exact": exact is not None, "question_tokens_each": q_tokens, "questions": args.questions, | | | "tokens_per_request": per_request, "fits_request_budget": fits, "items": n_items, "requests": requests, | | | "total_input_tokens": total, "usd": round(cost_usd(total), 6), "price_per_mtok_in": price_per_mtok()} | | | if args.json: | | | print(json.dumps(out, indent=2)) | | | return 0 |
| | how = "measured" if exact is not None else f"estimated at {CHARS_PER_TOKEN:g} chars/token (add --exact to measure)" |
| | print(f"state {est_state:,} tokens ({how})") |
| | print(f"per request {per_request:,} tokens = state + {args.questions} question(s) x ~{q_tokens} · {'fits' if fits else 'EXCEEDS'} the 64k/32k request budget") |
| | if args.items: |
| | print(f"job {n_items:,} items -> {requests:,} request(s), {total:,} input tokens") |
| | print(f"cost ${cost_usd(total):.6f} at ${price_per_mtok()}/Mtok" + (f" (${cost_usd(total) / n_items:.7f} per item)" if args.items else "")) |
| | return 0 | | | | | | |
| | def cmd_schema(args) -> int: |
| | print(json.dumps({ |
| | "state": "string | object | array (the content to judge; use an object with named fields; reference them in instructions with backticks like ticket.message)", |
| | "model": "jev-latest", |
| | "questions": { |
| | "is_urgent": {"type": "noul", "instructions": "Does `ticket.message` convey urgency?", "criteria": {"true": "optional: what yes means", "false": "optional: what no means"}}, |
| | "department": {"type": "choice", "instructions": "Which team should handle ticket.message?", "criteria": {"billing": "Charges, invoices, refunds", "technical": "Bugs, outages, integrations", "other": None}}, |
| | "frustration": {"type": "score", "instructions": "How frustrated does the customer appear in ticket.message?", "criteria": ["Calm and matter-of-fact", "Frustrated but civil", "Very angry or threatening to leave"]}, |
| | }, |
| | }, indent=2)) |
| | print("\nAnswers come back under the same ids: noul -> {noul}; choice -> {choice, probabilities, confidence}; score -> {score, legend, probabilities, confidence}.\ninstructions and every criteria entry may be a string OR a JSON object/array (see jev guide structure).") |
| | return 0 |
| | |
| | |
| | def cmd_docs(args) -> int: |
| | url = f"{DOCS}/llms.txt" |
| | try: |
| | with urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": f"jev-cli/{VERSION}"}), timeout=20) as r: |
| | txt = r.read().decode() |
| | except Exception as e: # noqa: BLE001 | | | raise NetworkError(f"could not fetch {url}: {e}") | | | if args.grep: |
| | needle = args.grep.lower() |
| | txt = "\n".join(line for line in txt.splitlines() if needle in line.lower()) |
| | print(txt) |
| | return 0 | | | | | | | | | def cmd_guide(args) -> int: | | | if args.list: |
| | for k, (title, _, live) in GUIDE.items(): |
| | print(f"{k:<12} {title}" + (f" (live: {live if live.startswith('http') else DOCS + live + '.md'})" if live else "")) |
| | return 0 | | | topic = args.topic or "when" | | | if topic not in GUIDE: |
| | raise UsageError(f"unknown topic {topic!r}. Topics: {', '.join(GUIDE)}") |
| | title, body, live = GUIDE[topic] |
| | if args.live or topic == "vendor": |
| | if not live: |
| | raise UsageError(f"topic {topic!r} has no live page") |
| | url = live if live.startswith("http") else f"{DOCS}{live}.md" |
| | try: |
| | with urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": f"jev-cli/{VERSION}"}), timeout=20) as r: |
| | print(r.read().decode()) |
| | except Exception as e: # noqa: BLE001 |
| | raise NetworkError(f"could not fetch {url}: {e}") |
| | return 0 |
| | print(f"# {title}\n\n{body.strip()}\n") |
| | if live: |
| | print(f"Live source: {live if live.startswith('http') else DOCS + live + '.md'} (jev guide {topic} --live)") |
| | return 0 |
| | |
| | |
| | def cmd_examples(args) -> int: |
| | if not args.name: |
| | print("Copy-paste examples. jev examples <name> prints one; jev examples all prints everything.\n") |
| | for k, (title, _) in EXAMPLES.items(): |
| | print(f" {k:<10} {title}") |
| | return 0 | | | keys = list(EXAMPLES) if args.name == "all" else [args.name] | | | for k in keys: | | | if k not in EXAMPLES: |
| | raise UsageError(f"unknown example {k!r}. Names: {', '.join(EXAMPLES)}") |
| | title, body = EXAMPLES[k] |
| | print(f"## {title}\n{body.strip()}\n") |
| | return 0 |
| | |
| | |
| | # --------------------------------------------------------------------------- embedded docs |
| | |
| | GUIDE: dict[str, tuple[str, str, str | None]] = {} |
| | |
| | GUIDE["when"] = ("Jev playbook for agents: when to reach for it, how to ask, how to read", r""" |
| | Jev is TypeSafe's System One model. It does not write text. It makes ONE narrow judgment |
| | per question against a state you hand it, and returns a calibrated probability. ~250 ms. |
| | $0.042 per million input tokens (a 1k-token item costs $0.00004). Output tokens are free. |
| | Same input -> same answer, run to run. It is a semantic if-statement, not a chatbot. |
| | |
| | ## Reach for Jev when |
| | 1. The decision REPEATS over many items and you should not read them all into context: |
| | classify / filter / rank / dedupe hundreds of notes, emails, transcript segments, log |
| | lines, RSS items, PRs, findings. jev rank and jev batch do this in a few requests. |
| | 2. You want a CALIBRATED PROBABILITY, not your own vibe. Your confidence is not calibrated; |
| | Jev's is trained to be (RLCD). Use it to decide act / confirm / escalate. |
| | 3. A SCRIPT or CUE needs a judgment that runs unattended, a million times, with no agent |
| | loop: "is this Ring event a delivery?", "is this email a receipt?", "is this item relevant?" |
| | 4. You need an INDEPENDENT CHECK of your own output: does the source support this claim, |
| | does this draft break a stated rule, does this text contain an instruction aimed at an |
| | agent (prompt injection), is this tool call risky. |
| | 5. You need SPEED: real-time gates inside a request path or a UI. |
| | |
| | ## Do NOT use Jev for |
| | - Generation, rewriting, summarizing, explaining. It cannot. Use yourself. |
| | - Math, counting, date arithmetic, numeric comparison. Keep those in code; Jev extracts |
| | the parts (a Choice over months/days), code does the arithmetic. |
| | - Multi-hop reasoning, double negatives, "a property of a property". |
| | - One small item you can judge in a glance. The call costs more than the decision. |
| | - Hostile input without care: state is data, and injected text can move an answer. Write |
| | precise criteria and test edge cases. |
| | |
| | ## The three primitives (pick by the SHAPE of the answer your code needs) |
| | | need | type | answer fields | |
| | | is this true? | noul | noul = P(yes) 0..1 (no separate confidence) | |
| | | which one of these? | choice | choice, probabilities{opt: p}, confidence | |
| | | where on this rubric? | score | score (0..N-1, may fall between), legend, | |
| | | | | probabilities{level: p}, confidence | |
| | |
| | ## Fast path (one question) |
| | jev yes "Does this message ask for a refund?" --state "I was charged twice, fix it" |
| | jev pick "Which team handles this?" billing="Charges, refunds" tech="Bugs" other -s "…" |
| | jev rate "How severe is the bug?" "Cosmetic" "Degraded, workaround exists" "Blocking" -s @report.txt |
| | yes exits 0/1 on --threshold (default 0.5). pick/ rate exit 1 below --min-confidence. |
| | Pipe text on stdin instead of --state: cat email.txt | jev yes "Is this a receipt?" |
| | |
| | ## Real path (many questions, one call: SPECULATIVE FAN-OUT) |
| | Ask EVERY question you might need in ONE request. They run in parallel and independently; |
| | adding questions barely changes latency and costs only the question tokens. Let code ignore |
| | the answers it does not need. |
| | jev ask --field message=@ticket.txt --field policy=@refund_policy.md \ |
| | --noul refund 'Does message request a refund?' \ |
| | --noul covered 'Does policy cover the refund requested in message?' \ |
| | --choice team 'Which team should handle message?' billing=Charges tech=Bugs other \ |
| | --score anger 'How angry is message?' "Calm" "Frustrated but civil" "Hostile" --json |
| | Shell rule: SINGLE-QUOTE any instruction containing backticks, or zsh runs them as commands. |
| | |
| | ## Writing a question that works (Jev reads LITERALLY) |
| | - One judgment per question. "Analyze this and decide what to do" is not a question; split |
| | it. "Does message ask for money back?" is. |
| | - Say the exact condition. If you find yourself explaining what you "really meant", that |
| | explanation belongs in the instructions or criteria. |
| | - Point at the field with a backticked path: ticket.messages[0].text. Question IDs are |
| | NOT sent to the model; the instructions must carry the whole meaning. |
| | - NEVER index into a long array (items[137]). Measured 2026-09-18: 27% wrong at 150 |
| | items per request, still 9% at 25. Key the object instead (items.k137) or put the |
| | item inside the question itself; both scored 0 wrong out of 320. jev rank does the |
| | latter for you. |
| | - Choice: describe each option so the boundary with its neighbour is clear; add other / |
| | none when the list may not cover the input. Up to 255 options. |
| | - Score: levels describe SITUATIONS, low to high, 2..10 of them. "Broken but a workaround |
| | exists" works; "moderately bad" and bare numbers do not. One dimension per score. |
| | - Noul: phrase it so high = yes. Optional true=/false= criteria pin a subtle boundary. |
| | - Send only the state the questions need. Irrelevant context lowers accuracy (context rot). |
| | Filter in code first. Budget: ~32k tokens for state + longest question. |
| | |
| | ## Reading the answer |
| | - noul: p >= 0.5 is yes, but pick the threshold by stakes. Do not read 0.5 as "medium". |
| | - choice: choice is the argmax; confidence says how peaked the distribution is. A second |
| | option with real mass is signal, not noise. |
| | - score: a position between levels (0.71 + 0.32 = 1.3). Round for one outcome; compare or |
| | sort for ranking; divide by N-1 to normalize before weighting several scores together. |
| | - THREE BANDS: high confidence -> act; medium -> confirm or gather more; low -> do not act, |
| | escalate or ask. Thresholds scale with risk: read-only at 0.6, destructive at 0.9. |
| | - Do not carry a threshold tuned on a noul over to a choice, and do not expect P(x) and |
| | 1 - P(not x) from two questions to agree. Ask the question you want, one way. |
| | - Combine independent answers in CODE with weights you own (composite scoring). A weight |
| | change is a code change, not a prompt rewrite, and needs no re-inference. |
| | |
| | ## Build it into a tool, not just a chat turn |
| | sys.path.insert(0, "/path/to/dir/containing/jev.py") |
| | from jev import JevClient, noul, choice, score |
| | r = JevClient(label="my_tool").ask(state, {"ok": noul("Is `text` a receipt?")}) |
| | p = r["answers"]["ok"]["noul"] |
| | Keep the QUESTIONS and THRESHOLDS in one constant block at the top of the tool so a human |
| | can review them. That is the part that needs eyes; the rest is plumbing. |
| | |
| | ## Go deeper |
| | jev guide --list topics: questions choice score noul state confidence patterns |
| | design pitfalls structure cookbooks api library usage |
| | exit-codes install vendor |
| | jev guide design BUILDING a tool or cron job on Jev, not just calling it once |
| | jev guide vendor TypeSafe's own agent skill, fetched live |
| | jev guide <topic> --live the vendor's page for that topic, current |
| | jev examples copy-paste recipes: triage rank filter guardrail verify batch python |
| | jev docs live index of every vendor page and cookbook |
| | """, "/concepts/how-to-build-with-system-one") |
| | |
| | GUIDE["questions"] = ("Writing instructions and criteria", r""" |
| | A question is: an ID you choose (not sent to the model), a type, instructions, and |
| | criteria (options for choice, ordered levels for score, optional true/false for noul). |
| | |
| | ## Rules that come from measured failure modes (jev-1.13) |
| | 1. LITERAL READING. Jev answers the words, not the intent. Scoping words, negations and |
| | implied conditions are taken at face value. Write the exact condition. Put boundary |
| | cases in the criteria. Split any interpretation into two literal questions. |
| | 2. ATOMIC. Broad questions hide several judgments behind one number. "Is this spam?" |
| | becomes: requests a credential? sender identity conflicts with domain? announces an |
| | unexpected reward? Then weight them in code. |
| | 3. NAME THE FIELD. With an object state, reference path.to[0].field in backticks. |
| | Structured state + named references removes ambiguity about what to judge. But do |
| | not rely on POSITIONAL indexes in long arrays: `items[137]` mis-locates (27% wrong |
| | at 150 items, 9% at 25, measured 2026-09-18). Use keyed objects (`items.k137`) or |
| | embed the item in the question; both 0% wrong at 320. |
| | 4. ALIGN INSTRUCTIONS AND CRITERIA. A noul where true means "no" confuses it. The |
| | criteria are an extension of the instruction, in the same direction. |
| | 5. CONTRASTIVE OPTIONS. For a choice, say what each option covers, what belongs to the |
| | neighbour instead, and 1-3 examples. Same field names on every option. |
| | 6. SITUATIONS, NOT DEGREES. Score levels are matched one by one against the state; the |
| | model does not see the level numbers or neighbours. "Cosmetic; no impact" beats "1". |
| | 7. COVER THE SPACE. Add other / none_of_the_above to a choice when inputs may not fit. |
| | For selection-from-candidates, the model cannot pick a value you did not offer. |
| | 8. KEEP MATH OUT. Counting, dates, magnitudes, hex colours, assembly: convert or compute in |
| | code, hand Jev the semantic part ("does this colour read as a warning?"). |
| | 9. LESS STATE. Send only what the question needs. Retrieve and filter first. |
| | |
| | ## Shapes |
| | noul {"type":"noul","instructions":"Does msg request a refund?", |
| | "criteria":{"true":"asks for money back or a credit","false":"complains without asking for a remedy"}} |
| | choice {"type":"choice","instructions":"Which team should handle msg?", |
| | "criteria":{"billing":"charges, invoices, refunds","orders":"status, delivery, returns","other":null}} |
| | score {"type":"score","instructions":"How severe is the bug in report?", |
| | "criteria":["Cosmetic; no functional impact","Broken, workaround exists","Blocking; no workaround"]} |
| | |
| | instructions and every criteria entry may be a JSON object or array instead of a string |
| | (jev guide structure). Start with strings; add structure when two options keep getting |
| | confused or a level needs examples. |
| | """, "/primitives") |
| | |
| | GUIDE["choice"] = ("Choice: pick one option from a defined set", r""" |
| | Use when the answer is one of a known set with NO order between them: routing, category, |
| | language, entity type, which candidate span is the value. Up to 255 options; adding an |
| | option costs a few tokens, so give the FULL list rather than a shortlist. |
| | |
| | Answer: choice (argmax), probabilities (every option, sums to 1), confidence |
| | (how peaked). Read the runner-up: 0.60 vs 0.38 means two teams want this ticket. |
| | |
| | jev pick "Which team should handle this?" \ |
| | returns="Exchanges, refunds, wrong or damaged items" \ |
| | shipping="Delivery status, delays, lost packages" \ |
| | billing="Charges, invoices, payment problems" other \ |
| | -s "My shoes came in the wrong size and I was charged twice" --min-confidence 0.4 |
| | |
| | - Confusable neighbours: give each option an object {what, not_for, examples}. |
| | - Deep taxonomy: one choice per level, options = children, value = the subtree; walk in |
| | code, keep several branches alive when probabilities are close (beam search). |
| | - Relative vs absolute: a choice says WHICH option is best even when all are bad. Pair it |
| | with one noul per option (or a none option) to know whether ANY fits. |
| | - Speculative: "If this is a return, why?" is fine to ask on every ticket; ignore the |
| | answer when the department was not returns. |
| | """, "/primitives/choice") |
| | |
| | GUIDE["score"] = ("Score: rate the state on an ordered rubric", r""" |
| | Use when the answer is a position on a spectrum you can describe in steps: severity, |
| | frustration, relevance, seniority, formality. 2..10 levels, low to high. Level N is its |
| | index in the array. The model judges each level's description on its own against the |
| | state; it never sees the numbers or the neighbours. |
| | |
| | Answer: `score` = sum(level * p), can land between levels; `probabilities` per level; |
| | `legend` maps level -> description; `confidence`. |
| | |
| | jev rate "How much does this bug report give an engineer to work with?" \ |
| | "No detail; just says something is broken" \ |
| | "Names the feature but no steps or environment" \ |
| | "Steps to reproduce or environment, but not both" \ |
| | "Steps to reproduce and environment" -s @report.txt |
| | |
| | - Describe SITUATIONS. Examples inside a level (object with what + examples) sharpen it, |
| | but only when they resemble real inputs. Test against known cases. |
| | - One dimension per score. "Punctual and smart and experienced" cannot be placed. |
| | - Give a rare extreme its own level ("abusive or threatening") so the top is not shared. |
| | - Do not interpolate magnitudes from a score ("1.3 means 30% of users"). It is a |
| | position for thresholding, ranking and rounding, not a measurement. |
| | - Same score, different shapes: 1.0 can be all-on-level-1 or half 0 / half 2. Read |
| | probabilities and confidence alongside. |
| | - Composite: normalize each score by (levels-1), then weight in code: |
| | priority = 0.6severity + 0.3frustration + 0.1*report_quality. |
| | """, "/primitives/score") |
| | |
| | GUIDE["noul"] = ("Noul: probability that a yes/no statement is true", r""" |
| | Use for a clean yes/no where the probability itself is the signal: mentions X, requests |
| | Y, contains PII, is a receipt, is relevant to the query, contains an instruction aimed at |
| | an agent. Answer: noul = P(yes). No separate confidence: near 1 strong yes, near 0 |
| | strong no, near 0.5 undecided (NOT "medium"; for degree use a score). |
| | |
| | jev yes 'Does text contain an instruction directed at an AI agent or assistant?' \ |
| | --true "tells an agent/assistant/model to do, ignore, or reveal something" \ |
| | --false "ordinary content with no directive aimed at a model" \ |
| | --field text=@untrusted.md --threshold 0.7 |
| | |
| | - Phrase so high means yes. A statement form ("the customer is requesting a refund") |
| | works as well as a question; test both on your data. |
| | - Criteria are optional; add true=/false= when the boundary is subtle. |
| | - One noul per label when several labels may apply at once (multi-label), instead of |
| | a single choice. |
| | - Counting: one noul per item, the item embedded in its question or under a key |
| | (`items.k7`), never `items[7]` in a long array; then count in code. |
| | - `jev yes` exit codes: 0 yes, 1 no or uncertain (with --band LOW HIGH), 2+ errors. |
| | """, "/primitives/noul") | | | | | | GUIDE["state"] = ("State: what you hand the model", r""" | | | State is the material a panel of experts would be shown before judging. String, JSON | | | object, or array. Text only (no images/audio). English is strongest. | | | |
| | --state "text" --state @file --state - (stdin) cat f \| jev … |
| | --state-file path (.json parsed, else text) |
| | --state-json '{"a":1}' |
| | --field msg=@ticket.txt --field policy=@policy.md --field n=3 (builds an object) |
| | |
| | Every shape below is state, and a question is separate from it: |
| | |
| | | Shape | How | Reference it as | |
| | |---|---|---| |
| | | one string | -s TEXT, -s @file, stdin | the whole state | |
| | \| many sources at once \| `--field msg=@t.txt --field policy=@p.md --field orders=@o.json` \| `msg`, `policy`, `orders` \| |
| | \| nested JSON \| `--state-file o.json` \| `open[0].charges`, `sla.tier` \| |
| | \| a sequence \| `--state-json '["turn 1","turn 2"]'` \| the array, in order \| |
| | | a whole document | --state-file bigdoc.md | measured OK to 107k chars | |
| | | full control | jev ask -f request.json (state AND questions) | anything you wrote | |
| | |
| | A --field value that is @file.json or inline JSON is PARSED, so an object stays an |
| | object and the model sees real structure. Mixing types in one request is fine: a text file, |
| | a JSON file, a JSONL file and a bare scalar side by side. |
| | |
| | Prefer an OBJECT with named fields, then reference fields in instructions with backticked |
| | paths: ticket.messages[0].text, policy. Small arrays index fine; long ones do not |
| | (items[137] is wrong 27% of the time at 150 items). Key them: {"k137": ...}. |
| | |
| | - One request = one state, any number of questions, all independent, all parallel. |
| | - Budget, MEASURED 2026-09-20 by binary search rather than taken from the docs: the largest |
| | state accepted was 107,500 chars = 32,388 input tokens (765 ms); ~110k chars returns |
| | HTTP 400 max_tokens_exceeded. English measured at 3.32 chars/token, not 4, so the |
| | real ceiling is nearer 107k chars than the 150k a 4-char rule of thumb implies. jev |
| | warns above 120k chars; rank and batch chunk under it. |
| | - Irrelevant material costs accuracy. Filter in code. If you cannot, ask a noul for |
| | relevance first and drop what fails. |
| | - State is data, not instructions, but adversarial text inside it can still steer an |
| | answer. Be explicit in criteria; test edge cases before trusting a guardrail. |
| | - When a later question depends on an earlier ANSWER (you need it to fetch data or pick |
| | the next options), make a second request. Otherwise ask everything at once. |
| | """, "/concepts/state") |
| | |
| | GUIDE["confidence"] = ("Probabilities vs confidence, and how to act on them", r""" |
| | Every choice/score answer carries probabilities (the distribution) and confidence |
| | (one number, 0..1, derived from how peaked that distribution is). Nouls carry only P(yes). |
| | |
| | Calibration: across many predictions, outcomes given 0.8 happen ~80% of the time. It |
| | describes groups, not any single answer; confidence 1.0 is not a guarantee. |
| | |
| | ## Three bands (draw the lines by stakes, then test on your data) |
| | high act automatically |
| | medium act with a confirmation, or gather more, or flag |
| | low do not act: escalate to a person or a reasoning model, or ask |
| | |
| | ## Thresholds scale with risk |
| | if a.confidence < 0.5: escalate() # genuinely unsure |
| | elif a.choice == "read_only": do_it() # cheap to be wrong |
| | elif a.choice == "destroy": |
| | if a.confidence > 0.9: confirm_then_do() |
| | else: ask_user() |
| | | | | - Low choice confidence: no clear winner, OR several acceptable answers (harmless). | | | - Low score confidence: levels overlap, question measures >1 thing, or the state does | | | not say enough. | | | - If all you need is the best option, take the argmax; thresholds are for deciding | | | whether to ACT. If you have a statistical procedure in mind, use probabilities, not | | | the confidence summary. |
| | - Aliases move (`jev-latest` -> new version). Thresholds tuned on one version: pin the |
| | versioned id (`jev-1.13.0`) with --model and migrate on your schedule. |
| | """, "/confidence") |
| | |
| | GUIDE["patterns"] = ("Patterns that compose Jev into systems", r""" |
| | 1. SPECULATIVE FAN-OUT. Put every question the workflow might need in one request, |
| | including ones that matter only on some branches. Parallel, independent, near-zero |
| | latency cost. 13 questions in one call measured 12x cheaper and 10x faster than 13 |
| | calls, with identical answers. Code decides which answers count. |
| | 2. CONFIDENCE-GATED ROUTING. The answer says WHAT; confidence says WHETHER to act. Gate |
| | each action at its own threshold by consequence. (jev guide confidence) |
| | 3. COMPOSITE SCORING. Break a judgment into atomic scores, normalize by (levels-1), |
| | weight in code. Weights are reviewable and tunable without re-inference. |
| | 4. INTENT ROUTING. Cheap classifier in front of expensive handlers: deterministic code |
| | for one intent, a specialist LLM for another, a human for the low-confidence rest. |
| | 5. SELECT, DO NOT GENERATE. Find candidate spans/values with regex or a generative model, |
| | then let Jev CHOOSE the right one. Extraction becomes a choice over enumerated options |
| | with an explicit "not stated". |
| | 6. RERANK / FILTER. Cheap retrieval (BM25, embeddings, grep) makes a shortlist; one Jev |
| | question per query-candidate pair grades it. jev rank does this in one request per |
| | chunk. Measured on a legal corpus: top-1 5% -> 18%, top-10 38% -> 62%. |
| | 7. VERIFY AND ESCALATE. Check a claim against its source (supports / contradicts / says |
| | nothing), an extraction against the document, a tool-call trace against its intent. |
| | Low confidence goes to a human or a reasoning model. Cascade: small model extracts, |
| | Jev verifies, big model only on the uncertain slice. |
| | 8. GUARDRAIL. One request per message, in and out of an LLM: a battery of nouls (jailbreak? |
| | harm? self-harm? instruction aimed at the model?) plus a severity score. Threshold in |
| | code: pass / review / block / route. |
| | 9. MAP-REDUCE. Thousands of items x same questions -> jev batch, then aggregate in code. |
| | Probabilities become features for a classical model when you have labels. |
| | 10. TWO-STAGE SELECTION. Rank everything cheaply, then re-judge the top K with fuller |
| | evidence and let the second stage reject all of them. (skill suggestion cookbook: |
| | wrong loads down by more than half.) |
| | """, "/patterns") |
| | |
| | GUIDE["design"] = ("Designing a NEW integration (tool, cron job, pipeline)", r""" |
| | Runtime use is jev yes\|pick\|rate\|ask\|rank\|batch. This topic is for the other job: you |
| | are about to BUILD something that calls Jev on every run. Much of it is distilled from |
| | TypeSafe's own agent skill (github.com/typesafe-ai/skills, MIT), which is a docs-oriented |
| | skill for exactly this job; jev guide vendor fetches it live. |
| | |
| | ## Start from the behaviour, work back to the judgments |
| | What will the tool show, select, change, or hand off? List the judgments that decision |
| | needs. Keep known rules, calculations, exact lookups and execution in code. Preserve the |
| | stack and scope already in place; add Jev only where semantic understanding helps. |
| | |
| | ## Shapes beyond "classify" |
| | - Route AND fill arguments: one request selects the handler and its closed-set params. |
| | - Select instead of generate: code finds candidate spans/values, Jev picks the intended |
| | one. Check candidate COVERAGE: the model cannot choose a value you did not offer. |
| | - Find and judge evidence: cheap retrieval, then per-candidate relevance, then select. |
| | - Judgments as reusable data: score dimensions once; weights, thresholds, rankings and |
| | views change in code with no re-inference while the evidence and questions stand. |
| | - Verify and escalate: check a claim/field/tool-call against its evidence; send the |
| | uncertain slice to a person or a reasoning model. |
| | - Respond to changing state: keep INFERRED state distinct from OBSERVED facts, and check |
| | freshness before applying an old answer to a changed situation. |
| | |
| | ## Atomic is not "one fact per question" |
| | A bounded action selection or a contextual interpretation is a valid single judgment. |
| | Split independently useful dimensions; do not split a relationship you need judged whole. |
| | |
| | ## Policy stays explicit in code |
| | Weighted sums suit compensating preferences. An "any serious violation blocks" rule needs |
| | separate conditions, not a weight. Ignore uncertainty on branches you do not take. Several |
| | acceptable alternatives can spread probability; low confidence on a harmless preference |
| | choice is not a reason to stop. Typed output guarantees the INTERFACE, not truth: |
| | validate on your own data before trusting it unattended. |
| | |
| | ## Ship checklist |
| | 1. Questions + thresholds in ONE constant block at the top of the file. Agents write |
| | mediocre questions; expect a human to edit them, so make them easy to find. |
| | 2. --model jev-1.13.0 pinned once thresholds are tuned. jev-latest moves. |
| | 3. A handful of known cases run before the first unattended run; when one fails, separate |
| | missing evidence / model error / code error / service failure before changing anything. |
| | 4. label= set on the client so jev usage attributes the spend. |
| | 5. Long lists: never items[i]; key the object or embed the item (see pitfalls row 10). |
| | 6. Never embed the key in a web page or a repo; the CLI reads ~/.config/typesafe/api_key. |
| | """, "/concepts/how-to-build-with-system-one") |
| | |
| | GUIDE["vendor"] = ("TypeSafe's own agent skill (fetched live from GitHub)", r""" |
| | TypeSafe publishes a docs-oriented skill for agents BUILDING integrations: |
| | https://github.com/typesafe-ai/skills (skills/typesafe-ai/SKILL.md, MIT, ~2.5k tokens) |
| | It has no executable and no runtime commands; jev is the runtime. Its value is the |
| | design guidance and the map of which vendor page to read for which task, and it is |
| | updated by the vendor when the API changes. jev guide vendor --live prints the current |
| | copy. jev guide design is our distillation, kept short. |
| | """, "https://raw.githubusercontent.com/typesafe-ai/skills/main/skills/typesafe-ai/SKILL.md") |
| | |
| | GUIDE["pitfalls"] = ("Jagged edges of jev-1.13 (vendor-acknowledged)", r""" |
| | | # | failure | do this instead | |
| | | 1 | literal reading | write the exact condition; boundary cases in criteria | |
| | | 2 | math, counting, magnitudes | compute in code; one noul per item then sum | |
| | | 3 | date/time comparison | extract parts as choices; compare in code | |
| | | 4 | indirection, double negatives | direct wording; name the field | |
| | | 5 | large state with irrelevant detail | filter first; relevance noul if you cannot | |
| | | 6 | adversarial content in state | precise criteria; test edge cases | |
| | | 7 | contradictory instructions/criteria | align them; true must mean yes | |
| | | 8 | expected structural invariants | P(x) and 1-P(not x) need not agree; noul vs choice thresholds differ; ask one way | |
| | | 9 | generation | it cannot; select from candidates instead | |
| | | 10| positional index into a long array | (ours, measured 2026-09-18) items[137] 27% wrong at 150, 9% at 25; keyed object or item-in-question 0% at 320 | |
| | |
| | Numeric representations (hex colours, RGB, assembly) underperform names and high-level |
| | code. Non-English works but is weaker; watch confidence. Reviewed by the vendor |
| | 2026-09-17. Live: jev guide pitfalls --live |
| | """, "/model-jaggedness/jev-1.13") |
| | |
| | GUIDE["structure"] = ("Structured instructions and criteria (JSON instead of strings)", r""" |
| | instructions, every choice option description, every score level, and noul true/false |
| | may be a string, an object, an array, or null. The model is trained on structure; keys |
| | are labels it reads, so use short descriptive names (none are reserved). |
| | |
| | "instructions": {"question": "Does the claimed sender conflict with the domain?", |
| | "compare": ["ticket.sender.display_name", " ticket.sender.email"], |
| | "focus": "Compare the named organization with the email domain."} |
| | |
| | "criteria": {"return_policy": {"what": "rules for returns in general", |
| | "not_for": "the status of a specific return", |
| | "examples": ["Can I return sale items?"]}, |
| | "return_status": {"what": "where a specific return is", |
| | "not_for": "general policy questions", |
| | "examples": ["Where is my refund for order A-104?"]}} |
| | |
| | score level: {"what": "Broken or degraded, workaround exists", |
| | "examples": ["export fails in one browser but works in another"]} |
| | |
| | When: a question has several parts; two options keep getting confused; a level needs |
| | examples; the supporting data is already JSON (schema, taxonomy, record). Use the same |
| | field names across options so the model compares like with like. Examples only help when |
| | they resemble real inputs; the wrong example changes little, the right one concentrates |
| | probability (measured 0.54 -> 0.90 confidence on the same ticket). |
| | |
| | Taxonomy walk: option value = the child subtree, so the model sees what lives under a |
| | branch before committing. Trim large subtrees to direct children plus sample leaves. |
| | """, "/primitives/advanced") |
| | |
| | GUIDE["cookbooks"] = ("Vendor cookbooks (worked, measured examples)", r""" |
| | Each is a page under https://docs.typesafe.ai/cookbooks/<slug>.md |
| | parallel_questions 13 questions, one call: 12.2x cheaper, 10x faster, same answers |
| | skill_suggestion rank 182 agent skills, re-judge top 3, may reject all; wrong loads halved |
| | rerank_typesafe BM25 shortlist -> one question per pair; top-1 5% -> 18% |
| | semantic_find score 218 line ids vs a query in ONE choice; noul: is there an answer at all |
| | classifying_rag_passages per passage: relevant? usable? contradicts premise? instructs the model? |
| | citation_check supports / contradicts / says nothing; confidence flags for review |
| | llm_guardrails noul battery + severity score on every LLM input and output |
| | function_calling NL request -> function name + closed-set args, confidence-aware |
| | pre_parsed_value_extraction regex finds candidates, Jev selects the intended span |
| | date_extraction_cookbook month/day/year as choices with "not stated"; code assembles and compares |
| | autoformat recover Markdown structure: line-join nouls, then block-type choices |
| | hierarchical_classification beam search over choice probabilities through deep taxonomies |
| | entity_alignment one 3-level score decides merge / leave / send to curator |
| | sde_cascade mini extracts -> Jev verifies -> reasoning model only where needed |
| | consistency_noul_cookbook 14 nouls x 15 runs: std dev 0.0102, below every LLM; route 0.3-0.7 to review |
| | consistency_choice_cookbook add an explicit uncertain outcome to moderation decisions |
| | classification_using_confidence 75 industry groups; low confidence -> report the parent division |
| | autoresearch_feature_discovery propose questions, turn text into features, train CatBoost on them |
| | Read one with: jev docs --grep <slug> then fetch the .md URL. |
| | """, "/cookbooks/parallel_questions") |
| | |
| | GUIDE["api"] = ("HTTP API, limits, pricing", r""" |
| | POST https://api.typesafe.ai/v1/systemone Authorization: Bearer <key> |
| | body {"state": str\|obj\|arr, "model": "jev-latest", "questions": {id: Question}} |
| | answer {"model": "jev-1.13.0", "answers": {id: Answer}, "usage": {"input_tokens", "output_tokens"}} |
| | GET /v1/models lists aliases (jev-latest, jev-preview); versioned ids like jev-1.13.0 also accepted |
| | |
| | Errors: 401 bad key · 422 malformed request (body names the field) · 429 rate limit · |
| | 529 overloaded. jev retries 429/529/5xx with backoff and honours retry-after. |
| | Limits (jev-1.13, adjust without notice): 250k tokens/s, 1200 req/min; 64k tokens per |
| | request, 32k for state + longest question. MEASURED 2026-09-20: 107,500 chars of English |
| | state = 32,388 tokens went through; ~110k chars returned 400 max_tokens_exceeded, an error |
| | code the docs do not list. English runs 3.32 chars/token, so budget on tokens, not on the |
| | 4-chars-per-token rule of thumb. Text only. |
| | Price: $42 per billion input tokens ($0.042/Mtok). Output free. jev usage shows spend, |
| | jev cost estimates a job, jev config set credits tracks a balance (no balance endpoint |
| | exists; see jev guide usage). |
| | Model is not fine-tuned per customer; you shape it through state, instructions, criteria. |
| | Not trained on customer traffic. jev schema prints a request skeleton. |
| | Full JSON path: jev ask -f request.json (or jev ask with the JSON body on stdin) |
| | """, "/api") |
| | |
| | GUIDE["library"] = ("Using jev from Python (tools, Cues, pipelines)", r""" |
| | import sys; sys.path.insert(0, "/path/to/dir/containing/jev.py") |
| | from jev import JevClient, noul, choice, score, JevError |
| | |
| | QUESTIONS = { # keep questions + thresholds together, reviewable |
| | "is_receipt": noul("Is email.body a purchase receipt or order confirmation?"), |
| | "vendor": choice("Which vendor sent `email`?", {"amazon": None, "apple": None, "other": None}), |
| | "urgency": score("How urgent is `email.body`?", ["None", "This week", "Today"]), |
| | } |
| | THRESHOLDS = {"is_receipt": 0.7, "vendor_conf": 0.6} |
| | |
| | client = JevClient(label="mail_triage") # label shows up in jev usage |
| | r = client.ask({"email": {"subject": s, "body": b}}, QUESTIONS) |
| | a = r["answers"] |
| | if a["is_receipt"]["noul"] >= THRESHOLDS["is_receipt"]: ... |
| | if a["vendor"]["confidence"] < THRESHOLDS["vendor_conf"]: escalate() |
| | |
| | - ask() raises JevError (exit_code attr: 2 usage, 3 auth, 4 api, 5 network). |
| | - Many items: a ThreadPoolExecutor over client.ask (8 workers is safe), or shell out |
| | to jev batch. Every call appends one line to ~/.config/typesafe/usage.jsonl. |
| | - Official SDKs exist (pip install typesafe-sdk, npm i @typesafe-ai/sdk) with typed |
| | answers and retries; this module is the zero-dependency path that works from any agent. |
| | - Pin --model jev-1.13.0 in a pipeline whose thresholds you tuned. |
| | """, "/sdk/python") |
| | |
| | GUIDE["usage"] = ("Usage, cost, credits and rate limits", r""" |
| | Every request appends one row to ~/.config/typesafe/usage.jsonl: ts, cmd (the label), agent |
| | ($JEV_AGENT), q (questions), in/out tokens, ms, model, rid (x-typesafe-request-id), |
| | attempts when retried, or err on failure. Nothing secret is in it. record=False on |
| | JevClient skips it. |
| | |
| | jev usage windows (today/yesterday/7d/30d/month/all), latency p50/p95, |
| | peak req/min and tok/s vs the published limits, errors, forecast, |
| | credits countdown, budget, by command, by agent (names resolved) |
| | jev usage --by day also hour, cmd, agent, model --days N |
| | jev usage --tail 20 recent requests with request ids (quote a rid to the vendor) |
| | jev usage --since 2026-09-01 --label rank --agent 8ba5 --json |
| | jev cost --state-file f --questions 12 [--exact] tokens + usd before you run it |
| | jev cost -s "…" --items 5000 a rank-shaped job over 5,000 items |
| | jev config set credits 50 balance read off the console; usage counts down from it |
| | jev config set budget 5 monthly USD budget; usage shows % used and a projection |
| | jev config set price 0.042 only if the vendor changes list price |
| | |
| | ## What the API does NOT give you (measured 2026-09-19) |
| | No balance, usage, or quota endpoint exists (/v1/usage, /v1/balance, /v1/account ... all |
| | 404) and responses carry no rate-limit headers, only x-typesafe-request-id. "Tokens |
| | remaining" therefore means one of three things here: |
| | 1. credits remaining: a balance YOU snapshot from https://console.typesafe.ai via |
| | jev config set credits, minus spend recorded since. Drift = calls made from |
| | another machine or key. Re-snapshot when you look at the console. |
| | 2. request budget: 64k tokens per request, 32k for state + longest question. jev cost |
| | says whether a state fits and how many items pack per request. |
| | 3. rate headroom: 1,200 req/min and 250k tok/s (adjust without notice). jev usage |
| | shows your measured peaks as a percentage of both. |
| | Price: $42 per billion input tokens ($0.042/Mtok). Output is free. A 1k-token item costs |
| | $0.00004; a thousand of them cost four cents. |
| | """, "/models") |
| | |
| | GUIDE["exit-codes"] = ("Exit codes", r""" |
| | 0 success / yes / confident |
| | 1 yes: no or uncertain · pick/ rate: confidence below --min-confidence |
| | 2 usage error (bad flags, empty state, malformed question) |
| | 3 auth error (no key, key rejected) |
| | 4 API error after retries (429/529/5xx exhausted, 422) · batch: some rows failed |
| | 5 network error after retries |
| | Errors print one line to stderr prefixed jev:; with --json the line is JSON. |
| | """, None) |
| | |
| | GUIDE["install"] = ("Where jev lives and how it is maintained", r""" |
| | Single file jev.py, anywhere on disk; symlink it onto PATH: ln -s $PWD/jev.py ~/.local/bin/jev |
| | Requires Python 3.10+, standard library only |
| | Key jev auth set <key> -> ~/.config/typesafe/api_key (0600), or export TYPESAFE_API_KEY |
| | Keys: https://console.typesafe.ai/keys |
| | Usage ledger ~/.config/typesafe/usage.jsonl (jev usage) |
| | Config ~/.config/typesafe/config.json (jev config: credits, budget, price; no secrets) |
| | Skill copy the jev/ skill folder (SKILL.md) into your agent's skills dir: |
| | Claude Code ~/.claude/skills/jev/ |
| | Codex ~/.agents/skills/jev/ (documented user path; ~/.codex/skills/ also read) |
| | OpenCode ~/.config/opencode/skills/jev/ |
| | One canonical copy, symlinked into each, is easiest to keep current. |
| | Attribution export JEV_AGENT=<name> so jev usage --by agent can tell your agents apart |
| | Health jev doctor |
| | Vendor docs https://docs.typesafe.ai (Markdown at any page path + .md; index at /llms.txt) |
| | Vendor skill github.com/typesafe-ai/skills is a docs-orientation skill for BUILDING integrations, |
| | with no executable. jev guide vendor fetches it live. Installing both puts two skills |
| | on the same triggers; pick one. |
| | """, None) |
| | |
| | EXAMPLES: dict[str, tuple[str, str]] = {} |
| | |
| | EXAMPLES["gutcheck"] = ("One calibrated yes/no, usable in an if", r""" |
| | if jev yes 'Does text describe a package delivery?' --field text="$DESC" --threshold 0.7; then |
| | echo delivery |
| | fi |
| | cat draft.md | jev yes "Does the text contain an em dash character or the phrase 'you're absolutely right'?" |
| | # (that one is a lint, and grep does it better; use jev for judgments grep cannot make) |
| | cat draft.md | jev yes "Does the text flatter the reader or agree without giving a reason?" --json |
| | """) |
| | |
| | EXAMPLES["triage"] = ("Classify + speculative questions in one call", r""" |
| | jev ask --field message=@ticket.txt --field open_orders='["A-104","A-118"]' \ |
| | --choice team 'Which team should handle message?' \ |
| | billing="Charges, invoices, refunds, subscriptions" \ |
| | orders="Order status, delivery, cancellation, returns" \ |
| | account="Login, profile, permissions, security" other \ |
| | --noul refund 'Does message explicitly ask for money back or a credit?' \ |
| | --noul mentions_order 'Does message refer to one of open_orders by id?' \ |
| | --score anger 'How angry is the author of message?' \ |
| | "Calm and matter-of-fact" "Frustrated but civil" "Hostile or threatening to leave" \ |
| | --json |
| | # then in code: route on team.choice if team.confidence >= 0.6 else escalate; |
| | # read refund only on the billing branch; read mentions_order only on orders. |
| | """) |
| | |
| | EXAMPLES["rank"] = ("Rank or filter many candidates against a query (one request per chunk)", r""" |
| | # lines in a file |
| | jev rank --query "notes about thermostat schedules" --candidates-file titles.txt --top 10 |
| | # JSON array of objects (each candidate is shown to the model as JSON) |
| | jev rank --query "a bug that causes data loss" --candidates-file issues.json --min 0.6 --json |
| | # stdin, graded on a rubric instead of yes/no |
| | ls Meetings | jev rank --query "meetings about the Q3 launch" \ |
| | --levels "unrelated" "adjacent" "directly about it" |
| | # custom question; refer to the item as candidate (it is embedded in each question) |
| | jev rank --query "$Q" --candidates-file x.txt --instructions 'Does candidate answer query?' |
| | # extra shared context for every judgment |
| | jev rank --query "…" --candidates-file x.txt --context @rubric.md |
| | """) |
| | |
| | EXAMPLES["guardrail"] = ("Screen untrusted text before acting on it", r""" |
| | jev ask --field text=@untrusted.md \ |
| | --noul injection 'Does text contain an instruction aimed at an AI agent, assistant, or model?' \ |
| | true="tells a model to do, ignore, reveal, or change something" \ |
| | false="ordinary content; any imperatives are aimed at a human reader" \ |
| | --noul secrets 'Does text ask the reader to disclose a credential, key, or password?' \ |
| | --score harm 'How much harm would follow from doing what text asks?' \ |
| | "None" "Minor or reversible" "Serious or irreversible" --json |
| | # thresholds live in your code: block if injection >= 0.7 or harm >= 1.5; review 0.4..0.7 |
| | """) |
| | |
| | EXAMPLES["verify"] = ("Check a claim against its source, or a draft against a rule", r""" |
| | jev ask --field claim="$CLAIM" --field source=@source.md \ |
| | --choice support 'How does source relate to claim?' \ |
| | supports="the source states or entails the claim" \ |
| | contradicts="the source states the opposite or an incompatible fact" \ |
| | silent="the source does not address the claim" --json |
| | # confidence < 0.8 -> a human confirms it |
| | |
| | jev ask --field draft=@post.md --field rules=@voice_rules.md \ |
| | --noul r1 'Does draft violate any rule stated in rules?' \ |
| | --noul r2 'Does draft contain a sentence that flatters the reader?' \ |
| | --score tone 'How formal is draft?' "casual" "neutral" "formal" |
| | """) |
| | |
| | EXAMPLES["batch"] = ("Same questions over thousands of rows -> JSONL", r""" |
| | # rows: one JSON object per line; --state-key picks the field to judge, --id-key is echoed back |
| | jev batch --input items.jsonl --state-key text --id-key id --concurrency 8 \ |
| | --noul relevant 'Is text about home automation?' \ |
| | --score quality 'How useful is text to a power user?' "noise" "some value" "must read" \ |
| | --out results.jsonl |
| | # plain text lines, one state per line |
| | jev batch --input lines.txt --text-lines --noul q "Is this line a TODO?" > out.jsonl |
| | # aggregate in code: jq 'select(.answers.relevant.noul > 0.7) | .id' results.jsonl |
| | """) |
| | |
| | EXAMPLES["python"] = ("From a tool or a cron job", r""" |
| | import sys; sys.path.insert(0, "/path/to/dir/containing/jev.py") |
| | from jev import JevClient, noul, choice, score |
| | Q = {"delivery": noul("Does event.description describe a package being delivered?"), |
| | "who": choice("Who is at the door in event.description?", |
| | {"courier": None, "family": None, "stranger": None, "nobody": None})} |
| | r = JevClient(label="ring_watch").ask({"event": ev}, Q) |
| | if r["answers"]["delivery"]["noul"] > 0.8: notify() |
| | """) | | | | | | # --------------------------------------------------------------------------- argparse | | | | | | SHORT_HELP = """jev: calibrated, typed decisions from TypeSafe's Jev model (~250 ms, $0.042/Mtok) | | | | | | jev yes "question?" -s TEXT P(yes); exit 0 yes / 1 no | | | jev pick "which?" a=desc b=desc other -s TEXT one option + probabilities + confidence | | | jev rate "how much?" "low" "mid" "high" -s TEXT position on your rubric + confidence |
| | jev ask --noul/--choice/--score … -s TEXT many questions, one call (fan-out), --json |
| | jev rank --query Q --candidates-file F rank/filter many items, chunked |
| | jev batch --input rows.jsonl --noul … same questions over many rows -> JSONL |
| | |
| | jev guide the playbook (read once per session) jev guide --list |
| | jev examples copy-paste recipes jev docs live vendor index |
| | jev usage spend, tokens, latency, throughput, credits, budget (--by, --tail, --json) |
| | jev cost estimate a job before running it jev config credits / budget / price |
| | jev doctor · models · schema · auth set|status|clear · version |
| | |
| | State: -s/--state TEXT | --state-file F | --state-json J | --field k=v (v=@file) | stdin. |
| | Questions reference fields with backticks; SINGLE-QUOTE them in a shell: 'Does message ask for a refund?' |
| | """ |
| | |
| | |
| | def add_state_args(p): |
| | g = p.add_argument_group("state") |
| | g.add_argument("-s", "--state", help="text to judge; @path reads a file; '-' reads stdin") |
| | g.add_argument("--state-file", help="file to judge (.json is parsed, else text)") |
| | g.add_argument("--state-json", help="inline JSON state") |
| | g.add_argument("--field", action="append", metavar="KEY=VALUE", help="build an object state; VALUE may be @file or JSON; repeatable; combines with --state as `text`") |
| | | | | |
| | def add_question_args(p): |
| | g = p.add_argument_group("questions (repeatable, any mix)") |
| | g.add_argument("--noul", dest="noul", action=QAction, nargs="+", metavar="ARG", help="ID INSTRUCTIONS [true=…] [false=…]") |
| | g.add_argument("--choice", dest="choice", action=QAction, nargs="+", metavar="ARG", help="ID INSTRUCTIONS OPTION[=desc] OPTION[=desc] …") |
| | g.add_argument("--score", dest="score", action=QAction, nargs="+", metavar="ARG", help="ID INSTRUCTIONS LEVEL LEVEL … (low to high, 2..10)") |
| | g.add_argument("--questions-file", "-q", help="JSON file: {id: question}") |
| | g.add_argument("--questions-json", help="inline JSON: {id: question}") |
| | | | | |
| | def add_common(p): |
| | p.add_argument("--model", default=None, help=f"model id or alias (default {DEFAULT_MODEL}; pin e.g. jev-1.13.0)") |
| | p.add_argument("--api-key", default=None, help="override key resolution") |
| | p.add_argument("--timeout", type=float, default=30.0) |
| | p.add_argument("--json", action="store_true", help="machine output (raw API response where applicable)") |
| | p.add_argument("--compact", action="store_true", help="single-line JSON") |
| | p.add_argument("-v", "--verbose", action="store_true") |
| | | | | | | | def build_parser() -> argparse.ArgumentParser: | | | p = argparse.ArgumentParser(prog="jev", description=SHORT_HELP, formatter_class=argparse.RawDescriptionHelpFormatter, add_help=True) |
| | p.add_argument("--version", action="version", version=f"jev {VERSION}") |
| | sub = p.add_subparsers(dest="cmd") |
| | |
| | a = sub.add_parser("ask", help="many questions, one call", description="Ask any mix of noul/choice/score questions about one state in ONE request (speculative fan-out). Also: jev ask -f request.json or a full JSON body on stdin.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=EXAMPLES["triage"][1]) |
| | add_state_args(a) |
| | add_question_args(a) |
| | a.add_argument("-f", "--request", help="full request JSON file (state+questions); '-' for stdin") |
| | add_common(a) |
| | a.set_defaults(fn=cmd_ask_entry) |
| | |
| | y = sub.add_parser("yes", help="one yes/no -> P(yes); exit 0/1", description="One noul question. Prints `<p> yes\|no\|uncertain`. Exit 0 when p >= --threshold (or >= HIGH of --band), else 1.") |
| | y.add_argument("instructions", help="the yes/no question; phrase so that high = yes") |
| | y.add_argument("--true", help="what a yes means (optional criteria)") |
| | y.add_argument("--false", help="what a no means (optional criteria)") |
| | y.add_argument("--threshold", type=float, default=0.5) |
| | y.add_argument("--band", type=float, nargs=2, metavar=("LOW", "HIGH"), help="p < LOW no · LOW..HIGH uncertain · >= HIGH yes") |
| | add_state_args(y) |
| | add_common(y) |
| | y.set_defaults(fn=cmd_yes) |
| | | | | k = sub.add_parser("pick", help="one choice -> option + confidence", description="One choice question. Options as NAME or NAME=description (null description is fine when the name is clear). Exit 1 when confidence < --min-confidence.") |
| | k.add_argument("instructions") |
| | k.add_argument("options", nargs="+", metavar="OPTION[=desc]") |
| | k.add_argument("--min-confidence", type=float, default=0.0) |
| | add_state_args(k) |
| | add_common(k) |
| | k.set_defaults(fn=cmd_pick) |
| | | | | r = sub.add_parser("rate", help="one score -> position on rubric", description="One score question. Levels low to high, 2..10, each describing a situation. Exit 1 when confidence < --min-confidence.") |
| | r.add_argument("instructions") |
| | r.add_argument("levels", nargs="+", metavar="LEVEL") |
| | r.add_argument("--min-confidence", type=float, default=0.0) |
| | add_state_args(r) |
| | add_common(r) |
| | r.set_defaults(fn=cmd_rate) |
| | | | | rk = sub.add_parser("rank", help="rank/filter many candidates vs a query", description="Grades every candidate against --query with one question per candidate (the item embedded in its question), packed into as few requests as the token budget allows. Default: noul relevance. With --levels: a score rubric (normalized to 0..1 as p).", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=EXAMPLES["rank"][1]) |
| | rk.add_argument("--query", required=True, help="what you are looking for (text, JSON, or @file)") |
| | rk.add_argument("--candidates-file", help="one candidate per line, or a .json array") |
| | rk.add_argument("candidates", nargs="*", help="candidates as arguments (or stdin)") |
| | rk.add_argument("--instructions", help="the question, referring to candidate and query, e.g. 'Does candidate answer query?' (the item is embedded in each question)") |
| | rk.add_argument("--levels", nargs="+", metavar="LEVEL", help="use a score rubric instead of yes/no") |
| | rk.add_argument("--context", help="extra shared state (text, JSON, or @file) available as `context`") |
| | rk.add_argument("--top", type=int, help="keep the best N") |
| | rk.add_argument("--min", type=float, help="keep p >= MIN") |
| | rk.add_argument("--chunk-size", type=int, default=250, help="max candidates per request (default 250; 320 measured at 0.4 s)") |
| | rk.add_argument("--chunk-chars", type=int, default=90_000, help="max candidate chars per request (default 90k)") |
| | rk.add_argument("--concurrency", type=int, default=6) |
| | rk.add_argument("--width", type=int, default=110, help="truncate candidate display") |
| | add_common(rk) |
| | rk.set_defaults(fn=cmd_rank) |
| | | | | b = sub.add_parser("batch", help="same questions over many rows -> JSONL", description="One request per input row, in parallel. Rows: JSONL (object = state, or --state-key picks a field), or plain text lines (--text-lines). Output: JSONL with answers per row.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=EXAMPLES["batch"][1]) |
| | b.add_argument("--input", "-i", required=True, help="JSONL or text file; '-' for stdin") |
| | b.add_argument("--state-key", help="field of each JSON row to use as the state") |
| | b.add_argument("--id-key", default="id", help="field echoed back as `id` (default id)") |
| | b.add_argument("--text-lines", action="store_true", help="treat every line as a plain-text state") |
| | b.add_argument("--echo", action="store_true", help="include the state in each output row") |
| | b.add_argument("--out", "-o", help="write JSONL here instead of stdout") |
| | b.add_argument("--concurrency", type=int, default=8) |
| | add_question_args(b) |
| | add_common(b) |
| | b.set_defaults(fn=cmd_batch) |
| | |
| | m = sub.add_parser("models", help="list models/aliases") |
| | add_common(m) |
| | m.set_defaults(fn=cmd_models) |
| | |
| | au = sub.add_parser("auth", help="set \| status \| clear the API key") |
| | au.add_argument("action", choices=["set", "status", "clear"], nargs="?", default="status") |
| | au.add_argument("key", nargs="?") |
| | add_common(au) |
| | au.set_defaults(fn=cmd_auth) |
| | | | | d = sub.add_parser("doctor", help="check key, connectivity, one round trip") |
| | add_common(d) |
| | d.set_defaults(fn=cmd_doctor) |
| | |
| | u = sub.add_parser("usage", help="spend, tokens, latency, throughput, credits and budget from the local ledger", description="Every request appends a row to ~/.config/typesafe/usage.jsonl. Default prints the summary; --by breaks it down; --tail lists recent requests. Credits and budget come from jev config because the API exposes no balance.") |
| | u.add_argument("--by", choices=["day", "hour", "cmd", "agent", "model"], help="breakdown table") |
| | u.add_argument("--days", type=int, default=14, help="days for --by day (default 14)") |
| | u.add_argument("--tail", type=int, metavar="N", help="last N requests with request ids") |
| | u.add_argument("--since", help="only rows at or after YYYY-MM-DD / ISO timestamp") |
| | u.add_argument("--label", help="only rows with this cmd/label") |
| | u.add_argument("--agent", help="only rows whose agent id contains this") |
| | add_common(u) |
| | u.set_defaults(fn=cmd_usage) |
| | |
| | cf = sub.add_parser("config", help="show | set | unset credits, budget, price", description="Values live in ~/.config/typesafe/config.json (no secrets). jev config set credits 50 after reading the balance off the console; jev usage then counts down.") |
| | cf.add_argument("action", nargs="?", choices=["show", "set", "unset"], default="show") |
| | cf.add_argument("key", nargs="?", help="credits | credits_as_of | budget | price") |
| | cf.add_argument("value", nargs="?") |
| | cf.add_argument("--as-of", help="ISO timestamp the credits balance was read (with `set credits`)") |
| | add_common(cf) |
| | cf.set_defaults(fn=cmd_config) |
| | | | | co = sub.add_parser("cost", help="estimate tokens and cost for a job before running it", description="Estimates from characters (4 per token) or measures with one tiny real call (--exact). --items N and --per-item model a rank/batch job.") |
| | co.add_argument("--questions", type=int, default=1, help="questions per request (default 1)") |
| | co.add_argument("--question-chars", type=int, default=120, help="average characters per question incl. criteria (default 120)") |
| | co.add_argument("--items", type=int, help="number of items for a rank/batch-shaped job") |
| | co.add_argument("--per-item", action="store_true", help="each item is its own request with the full state (batch shape) rather than packed (rank shape)") |
| | co.add_argument("--exact", action="store_true", help="measure the state's real token count with one tiny call (costs a fraction of a cent)") |
| | add_state_args(co) |
| | add_common(co) |
| | co.set_defaults(fn=cmd_cost) |
| | |
| | s = sub.add_parser("schema", help="print a request skeleton") |
| | s.set_defaults(fn=cmd_schema) |
| | |
| | g = sub.add_parser("guide", help="the agent playbook; `guide <topic>`; `guide --list`") |
| | g.add_argument("topic", nargs="?") |
| | g.add_argument("--list", action="store_true") |
| | g.add_argument("--live", action="store_true", help="fetch the vendor's current page for this topic") |
| | g.set_defaults(fn=cmd_guide) |
| | |
| | e = sub.add_parser("examples", help="copy-paste recipes; `examples <name>` or `examples all`") |
| | e.add_argument("name", nargs="?") |
| | e.set_defaults(fn=cmd_examples) |
| | |
| | dc = sub.add_parser("docs", help="live vendor doc index (llms.txt); --grep to filter") |
| | dc.add_argument("--grep") |
| | dc.set_defaults(fn=cmd_docs) |
| | |
| | ver = sub.add_parser("version") |
| | ver.set_defaults(fn=lambda a: print(f"jev {VERSION}") or 0) |
| | return p |
| | |
| | |
| | def cmd_ask_entry(args) -> int: |
| | if args.request: |
| | raw = sys.stdin.read() if args.request == "-" else Path(args.request).expanduser().read_text() |
| | try: |
| | body = json.loads(raw) |
| | except json.JSONDecodeError as e: |
| | raise UsageError(f"request JSON invalid: {e}") |
| | if not isinstance(body, dict) or "state" not in body or "questions" not in body: |
| | raise UsageError("request JSON must have state and questions") |
| | c = JevClient(args.api_key, args.model or body.get("model"), timeout=args.timeout, label="ask") |
| | resp = c.ask(body["state"], body["questions"]) |
| | emit(args, resp, c.last_ms) |
| | return 0 | | | # a full JSON body on stdin (no flags) is also accepted | | | if not (args.state or args.state_file or args.state_json or args.field) and not getattr(args, "qlist", None) and not args.questions_file and not args.questions_json and not sys.stdin.isatty(): | | | raw = implicit_stdin() or "" | | | try: | | | body = json.loads(raw) | | | if isinstance(body, dict) and "state" in body and "questions" in body: |
| | c = JevClient(args.api_key, args.model or body.get("model"), timeout=args.timeout, label="ask") |
| | resp = c.ask(body["state"], body["questions"]) |
| | emit(args, resp, c.last_ms) |
| | return 0 | | | except json.JSONDecodeError: | | | pass |
| | raise UsageError("stdin was not a request JSON; pass questions with --noul/--choice/--score and the state with --state or stdin") |
| | return cmd_ask(args) |
| | | | | |
| | def main(argv=None) -> int: |
| | parser = build_parser() |
| | args = parser.parse_args(argv) |
| | if not args.cmd: | | | print(SHORT_HELP.rstrip()) | | | return 0 | | | if not hasattr(args, "json"): | | | args.json = False | | | if not hasattr(args, "compact"): | | | args.compact = False | | | if not hasattr(args, "verbose"): | | | args.verbose = False | | | try: | | | return int(args.fn(args) or 0) | | | except JevError as e: |
| | if getattr(args, "json", False): |
| | eprint(json.dumps({"error": str(e), "exit_code": e.exit_code})) |
| | else: | | | eprint(f"jev: {e}") | | | return e.exit_code | | | except KeyboardInterrupt: | | | return 130 | | | | | | |
| | if __name__ == "__main__": |
| | sys.exit(main()) |