PYR0D-363: Jev dispatch resolution through OpenRouter #44
Loading…
Reference in a new issue
No description provided.
Delete branch "fm/fm-jev-openrouter"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Story: PYR0D-363 - Enable Jev typed dispatch resolution through OpenRouter (typesafe/jev-router) in firstmate
What was wrong
bin/fm-dispatch-resolve.shresolves a crewmate dispatch profile from a written brief with TypeSafe's System One model (Jev), but it only reached Jev throughhttps://api.typesafe.aiwithTYPESAFE_API_KEY. This fleet has no TypeSafe key, so typed dispatch resolution was always off. OpenRouter now serves Jev astypesafe/jev-router, and the fleet already holdsOPENROUTER_API_KEY.What changed
OPENROUTER_API_KEYis present andTYPESAFE_API_KEYis not, the script asks the same single Choice question (every rule'swhenplus the neutral option) through OpenRouter with a structured-output schema, and maps the answer onto the existing clear / ambiguous / escalate / error / off contract and confidence floors.TYPESAFE_API_KEYwins when both keys exist, so the direct typesafe.ai path is unchanged; with neither key the tool stays off with no network call.quota-axialone among children, so quota rows stay measured (a validation finding settled during the run).typesafe/jev-router; each call has a token bound and a short timeout.docs/configuration.md"Typed dispatch resolution" anddocs/verification/dispatch-resolve.mddocument the transport;tests/fm-dispatch-resolve.test.shcovers both transports with stubbed endpoints.Evidence
A live call through OpenRouter on
data/flow-e2-02-schema/brief.mdreturnedclearin 4.2 seconds for $0.0048. The home'sconfig/crew-dispatch.jsonhas since been updated by firstmate to the current roster so real dispatches pass the provider check.Validation
The no-mistakes pipeline passed at
6f0d7469.Adversarial review (Codex GPT-6 Sol) at head
6f0d7469PR-comment-ready summary: Request changes. The OpenRouter branch calls
typesafe/jev-routerthrough chat completions, which routes the classification to another model; the recorded liveclearwas returned by Gemini, not Jev. OpenRouter exposes the actual Jev System One endpoint with the same typed Choice request and answer shape as the direct TypeSafe path. The current branch also prints the bearer key if an HTTP error body echoes it, and can returnclearfrom a chat response marked as refused or incomplete. The targeted tests and repository ShellCheck lint pass, but their fixtures do not cover these cases.Verdict: request changes. Findings 1-3 are blocking. No live paid call was made during this review. Review base:
origin/main...origin/fm/fm-jev-openrouter, head6f0d7469.Acceptance criteria
bin/fm-dispatch-resolve.sh:147-160,:418-419;tests/fm-dispatch-resolve.test.sh:1049-1082.bin/fm-dispatch-resolve.sh:338-349builds a typed Choice for direct TypeSafe;:357-393instead builds chat messages and a JSON schema;:109,:419selecttypesafe/jev-routerand chat completions. The live output atdocs/verification/dispatch-resolve.md:101-123namesgoogle/gemini-3.8-flash. OpenRouter's System One documentation explicitly supportsPOST /api/v1/systemonewith the TypeSafe request and response shapes and mapsjev-latestto the Jev model. Its Jev guide distinguishes Jev's typed decision model from the chat model router.clear/ambiguous/escalate/error/offoutcomestests/fm-dispatch-resolve.test.sh:1049-1162;bin/fm-dispatch-resolve.sh:422-445accepts a response envelope that declares refusal or nonterminal completion, leading to wrongclear(finding 3).TYPESAFE_API_KEYis presentbin/fm-dispatch-resolve.sh:147-157preserves key precedence,:343-350preserves the request shape, and:429-430uses the original endpoint and 5-second timeout. The sharedpost_jsonhelper retains the curl invocation shape; tests exercise the direct path, including both-key precedence, attests/fm-dispatch-resolve.test.sh:256-259and:1091-1095. No direct live call was authorized or made, so network-level byte equivalence was not proven.bin/fm-dispatch-resolve.sh:147-160,tests/fm-dispatch-resolve.test.sh:1157-1163.bin/fm-dispatch-resolve.sh:408-414uses fd 3 for the header, but includes 200 bytes of an uncontrolled HTTP error body in both stderr and stdout. A local synthetic-key reproduction printed the key twice (finding 2). Key isolation for child processes otherwise appears deliberate at:84-87,:449-453.bin/fm-dispatch-resolve.sh:365-366,:396-399set output tokens, price ceilings, and request byte cap;:408sets timeout. OpenRouter documentsprovider.max_priceas a provider price filter. The 5-cent claim atbin/fm-dispatch-resolve.sh:113-120is not established by these limits (finding 4). These chat bounds do not establish a bound for the requested System One transport.docs/configuration.md:1102-1114andtests/fm-dispatch-resolve.test.sh:1028-1163document and test the implemented chat transport.docs/verification/dispatch-resolve.md:107-121records a liveclear, but from Gemini rather than Jev. Both changed test scripts passed; repository lint passed on changed shell scripts.Findings
P1, blocking - the OpenRouter call does not use Jev's decision model.
bin/fm-dispatch-resolve.sh:109,:357-393,:418-428;docs/configuration.md:1109-1112;docs/verification/dispatch-resolve.md:107-121.typesafe/jev-routeris a chat router that chooses an underlying model. The live evidence reports Gemini. A generated JSON object with probabilities is not Jev's typed Choice or its calibrated probabilities, even when it passes local shape validation. OpenRouter's System One API accepts the samemodel: jev-latest,state, andquestionsrequest as the direct path and returnsanswers. Fix: send the existing TypeSafe Choice request tohttps://openrouter.ai/api/v1/systemoneusingOPENROUTER_API_KEY; share request construction and response validation with the direct path; update docs, tests, and the live evidence to identify atypesafe/jev-*answering model.P1, blocking - an HTTP error can disclose the bearer key.
bin/fm-dispatch-resolve.sh:408-414,:260-264;docs/configuration.md:1221. On a non-200 response,post_jsonincludes the first 200 bytes of the response body inemit_error, which prints them to stderr and stdout. In a local fake-curl test, a synthetic 401 body{"error":"Authorization: Bearer local-test-secret"}yieldeddispatch-resolve: error (http 401 ... local-test-secret ...)and the same text in the structuredreason. No real credential was used. This is an uncontrolled provider body, and the acceptance criterion says the key must never be printed or logged. Fix: for the OpenRouter transport, report the HTTP status and latency without raw response text, or redact the key and all encoded/fragmented variants before either output. Prefer omitting the raw body. Add an echoing-error-body regression test. The direct TypeSafe path has a pre-existing version of this body behavior; the new OpenRouter path must not copy it into a key-safety guarantee.P1, blocking - refusal or incomplete chat envelopes can produce
clear.bin/fm-dispatch-resolve.sh:422-445. The mapper extracts only.choices[0].message.contentand ignoresfinish_reason,message.refusal, and role. A local stub returned HTTP 200 withfinish_reason: "content_filter",message.refusal: "I cannot answer", and syntactically valid Choice-shaped content. The resolver emittedstatus: clearand aprofile:line. Afinish_reason: "length"envelope with already complete JSON follows the same path; the project's own live record atdocs/verification/dispatch-resolve.md:122says length endings occurred. Fix: if chat transport remains, require an assistant message with successful terminal finish, no refusal, and valid content before mapping; otherwise returnerrororescalate. Add stub cases for refusal, length, absent/invalid envelope, and a valid-looking content string inside each. Switching to the System One endpoint in finding 1 removes this chat-envelope problem.P2 - the documented sub-5-cent ceiling is not proved.
bin/fm-dispatch-resolve.sh:113-120,:365-366,:396-399;docs/configuration.md:1113-1114. The implementation caps the serialized request at 32,768 bytes, generation at 2,000 tokens, and route prices at $2/M input and $10/M output, but it never measures or caps input tokens. The documented price filter is a rate ceiling, not a per-call dollar ceiling. The statement that one call costs “well under 5 cents” does not follow from these settings; one observed half-cent call is only a sample. Fix: remove the numerical guarantee or establish an actual token/charge ceiling for the selected transport, with a test at the boundary. If the implementation switches to Jev System One, document that API's applicable token and price bounds instead.Verification performed
git diff origin/main...origin/fm/fm-jev-openrouterand full changed-file review, including.agents/skills/firstmate-coding-guidelines/SKILL.md.bash bin/fm-test-run.sh tests/fm-dispatch-resolve.test.sh tests/fm-bootstrap.test.sh:FM_TEST_SUMMARY total=2 failed=0 skipped_gate=0 duration_ms=48907. The runner emitted repeated Git-discovery diagnostics from temporary bootstrap fixtures but both scripts exited 0.bash bin/fm-lint.sh bin/fm-dispatch-resolve.sh bin/fm-bootstrap.sh bin/fm-env-lib.sh: exit 0, pinned ShellCheck 0.11.0 with full extended analysis. A bareshellcheckinvocation lacked the repository's source-aware configuration and emitted source/indirection diagnostics, so the repository lint result is the relevant one.bash bin/fm-doc-audience-check.sh:ok surfaces=127 local_links=702.Recommendation
Revise the OpenRouter path to use its actual Jev System One endpoint and share the direct request/answer path. Suppress uncontrolled HTTP error bodies from output, then refresh stub coverage and obtain a new live
clearexplicitly returned by Jev. Recheck the spend-bound statement against the final endpoint. The current branch should not be approved.Follow-up adversarial review (Codex GPT-6 Sol) at head
80991e65PR-comment-ready summary: The requested transport and safety changes are present at head
80991e65.OpenRouter now receives the same typed System One Choice request as the direct TypeSafe path, and the recorded live runs name
typesafe/jev-1.13-20260917as the answering model.HTTP error bodies are omitted from both output streams, and a response with an
errorfield, missing answer, or malformed Choice is rejected before a profile can be selected.The changed tests, repository ShellCheck lint, documentation check, and a separate synthetic-key 401 reproduction pass.
I found one nonblocking response-validation gap: a structurally valid answer still yields
clearif its responsemodelnames a non-Jev model or its answertypeis notchoice.Verdict: approve with follow-up hardening. No new blocking defect found.
This is a read-only review; I made no live paid calls, commits, pushes, or forge comments.
The live-call record is inspected evidence from this branch, not a call independently repeated in this review.
Review ranges:
git diff 6f0d7469..origin/fm/fm-jev-openrouterandgit diff origin/main...origin/fm/fm-jev-openrouter.Both were reviewed, including the changed scripts, tests, configuration documentation, and live verification record.
The worktree was at detached head
80991e65296742d690ba1589035fc43a1fd02977and clean after verification.Verification table
build_requestmakes the typedstateand onequestions.ruleChoice for either transport; OpenRouter posts it to/api/v1/systemone.bin/fm-dispatch-resolve.sh:330-375;tests/fm-dispatch-resolve.test.sh:1041-1070compares both request bodies apart from model and checks the endpoint. OpenRouter's TypeSafe SDK documentation confirms this endpoint and the barejev-latestmodel mapping.config/crew-dispatch.jsonreporttypesafe/jev-1.13-20260917, with threeclearand oneambiguous.docs/verification/dispatch-resolve.md:103-127; the record names the endpoint, home briefs, model, and observed 123-178 ms latency. No paid call was repeated here.post_jsonemits only HTTP status and latency on non-200.bin/fm-dispatch-resolve.sh:354-368;tests/fm-dispatch-resolve.test.sh:1114-1116,1134-1137. Independent fake-curl 401 with body{"error":"Authorization: Bearer synthetic-review-key-123"}printedstatus: error,reason: http 401 after 4 ms, andsynthetic key leaked: noafter searching both stdout and stderr.clearerror, missing answer, malformed probability set, and non-JSON.bin/fm-dispatch-resolve.sh:370-390;tests/fm-dispatch-resolve.test.sh:1117-1133. A different type/model validation gap remains as finding 7.bin/fm-dispatch-resolve.sh:108-114,347-353;docs/configuration.md:1109-1114;tests/fm-dispatch-resolve.test.sh:1140-1148.OPENROUTER_API_KEYonly for itsquota-axi --jsonchild.bin/fm-dispatch-resolve.sh:83-86,354-362,392-399;tests/fm-dispatch-resolve.test.sh:1051-1058.TYPESAFE_API_KEYbut retainsOPENROUTER_API_KEYwhile using a non-exported private copy for validation.bin/fm-bootstrap.sh:159-162,1041-1049;tests/fm-bootstrap.test.sh:1241-1259.bin/fm-dispatch-resolve.sh:141-153,347-375;tests/fm-dispatch-resolve.test.sh:1072-1089,1159-1164.New finding
bin/fm-dispatch-resolve.sh:376-390,514-518,546-566.The parser checks
choice,confidence, andprobabilities, but does not require.answers.rule.type == "choice"or verify that the OpenRouter response.modelnames a Jev model.In an offline fake-curl reproduction with a valid-looking Choice payload and rankable quota,
model: google/gemini-3.8-flashproducedstatus: clearand a profile; changing onlyanswers.rule.typetorefusalwithmodel: typesafe/jev-1.13also producedstatus: clearand a profile.The official OpenRouter System One contract says the response model identifies the serving System One model and typed questions return typed answers, so these are malformed or contradictory responses, not observed live behavior.
Fix: require
answers.rule.type == "choice"for both transports and, on the OpenRouter transport, require a resolvedtypesafe/jev-*model ID before consideringclear; add fixtures with otherwise valid answers and contradictory type/model fields.Commands and results
bash bin/fm-test-run.sh tests/fm-dispatch-resolve.test.sh tests/fm-bootstrap.test.sh->FM_TEST_SUMMARY total=2 failed=0 skipped_gate=0 duration_ms=44030; the two scripts ended withexit=0(9191and34742ms).Bootstrap temporary fixtures emitted repeated Git-discovery diagnostics; they did not fail either test.
bash bin/fm-lint.sh bin/fm-dispatch-resolve.sh bin/fm-bootstrap.sh bin/fm-env-lib.sh bin/fm-test-run.sh-> exit 0, pinned ShellCheck0.11.0, full extended analysis.bash bin/fm-doc-audience-check.sh->ok surfaces=127 local_links=701.git diff --check origin/main...origin/fm/fm-jev-openrouterandgit diff --check 6f0d7469..origin/fm/fm-jev-openrouter-> exit 0, no output.status: errorandreason: http 401 after 4 ms; stderrdispatch-resolve: error (http 401 after 4 ms); key search across both outputs:synthetic key leaked: no.wrong_model: status: clear | model: google/gemini-3.8-flash | profile: --harness codex --model gpt-6-luna;wrong_type: status: clear | model: typesafe/jev-1.13 | profile: --harness codex --model gpt-6-luna.Recommendation: the branch addresses the previous blocking findings and can proceed through its normal merge authority.
Track finding 7 as response-contract hardening; it does not contradict the live record or the documented endpoint's Jev-only behavior.