Skip to content

decide: ✨ Ask typed questions of TypeSafe Jev and OpenAI Decisions - #13

Merged
yasyf merged 2 commits into
mainfrom
decide
Oct 7, 2026
Merged

yasyf merged 2 commits into
mainfrom
decide

Conversation

@yasyf

@yasyf yasyf commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Several hook and CI classifiers need a yes/no, a label, or a score in well under a second, and today each one prompts a chat model and parses JSON out of its reply. TypeSafe's Jev and OpenAI's Decisions API answer those questions directly with probabilities in about 120 ms. This adds one client for both, so capt-hook hooks and the per-site evals can ask a question set once and run it on either provider.

decide and decide_sync take a state (text or JSON) and an ordered map of Binary, Label, and Score questions, and return a Decision with one typed answer per id in declared order. A Label or Score answer carries its probabilities keyed by option, and Refused stands in for a question OpenAI declines while it answers the rest. JEV pins jev-1.13.0 and OPENAI uses gpt-6-luna.

Following the core-first rule, both wire mappings and the retry policy live in the Rust core as two new ops, decide_plan and decide_resolve, so the Go and Rust hosts replay the same 21 golden vectors. Two of those vectors are recorded responses, one from each API.

The Jev mapping reorders Jev's probability maps, which come back in arbitrary order, and OpenAI's refusals map to Refused. The core retries 408, 429, 5xx, 529, and a lost connection, backing off from 0.5s to a 5s cap and honoring retry-after and retry-after-ms. The Python host retries only while the next attempt still fits inside the caller's timeout, then raises TimeoutError. A 401 or 422 raises DecideError on the first try. The deadline starts before the Keychain read, and an answer that arrives after it raises TimeoutError instead of returning late. Each process keeps one keep-alive httpx client, and decide runs decide_sync on a worker thread so it shares that pool without tying a client to an event loop.

Keys come from TYPESAFE_API_KEY or OPENAI_API_KEY. On macOS, an unset variable falls through to a Keychain item read at call time, and spawnllm key set jev|openai writes that item from stdin through security -i, so the key never appears in a process listing. I evaluated the vendor SDKs, typesafe_sdk and openai. They add two dependencies with different retry policies and answer shapes, and Jev ships no Go SDK, so the client speaks HTTP directly.

On this Mac I stored both keys in the Keychain from AWS Systems Manager and ran three-question calls live. Six warm Jev calls took 121 to 133 ms end to end, with about 105 ms of that on the wire; OpenAI took 118 to 160 ms. The async path returned the same answers.

A sol finder pass flagged four issues. Three are fixed in the second commit: the deadline overrun from per-phase httpx timeouts, the Keychain read outside the deadline, and async clients that pinned closed event loops. The fourth asked to honor an HTTP-date Retry-After. I left it out because both providers send delta seconds, and a date header falls back to the capped backoff, which still stops at the deadline.

yasyf added 2 commits October 7, 2026 05:28
Claude-Session-Id: 899e5f7a-9099-4d8f-b96d-79291e63b5b4
Claude-Session-Id: 899e5f7a-9099-4d8f-b96d-79291e63b5b4
@yasyf
yasyf merged commit a5dcd20 into main Oct 7, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant