Skip to content

feat(codex): source session savings from the router - #1122

Open
munir-weave wants to merge 5 commits into
mainfrom
munir/codex-session-savings
Open

feat(codex): source session savings from the router#1122
munir-weave wants to merge 5 commits into
mainfrom
munir/codex-session-savings

Conversation

@munir-weave

Copy link
Copy Markdown
Contributor

Re-lands the work from #1109, which merged into its stack parent (munir/codex-persistent-status) instead of main after #1098 landed first — so none of its commits were ever reachable from main. Same four commits, cherry-picked onto main, plus an npm version bump (see below).

Why the endpoint exists

Codex writes its requested model into every transcript turn and never the model that served. Verified on a real routed session with a /fm sonnet turn: turn_context.model read gpt-5.6-terra on all 27 turns and claude-sonnet appeared zero times. So the Claude Code statusline's per-turn client-side pricing cannot be ported — it would price both sides of the comparison at the same model and report saved $0.00 forever. Scraping the badge out of prose does not work either, since routingMarkerFor deliberately emits nothing on sticky turns.

GET /v1/sessions/:session_id/cost — authed by the rk_ key the Codex install already holds, scoped to the caller's installation. Returns the session's committed actual/requested cost in USD micros plus their difference, summing router.upstream and router.auxiliary_inference so handover/compaction summaries the customer paid for are counted. Mounted on the passthrough group (product surface) so hosted/managed installs get it.

Authorization is the installation_id predicate in the SQL, not a post-fetch check. Unknown, foreign, and not-yet-committed sessions all return the same 404 — distinguishing them would confirm a foreign session's existence.

The hook renders what the router returns, fetching in a detached subshell whose result the next turn reads. No turn blocks on the network; every failure path degrades to model-only.

Why the version bump is in this PR

main currently sits at 0.2.11, which is already published. I pulled the published tarball to check: it contains no codex-status.sh, and its install.sh has zero hooks.Stop occurrences. #1098 shipped the packaging but never bumped, so the entire Codex status feature — badge included — is unreachable by every user. codex-status.sh has no self-refresh (unlike cc-statusline.sh), so publishing is the only delivery path. Bumping to 0.2.12 here lets a router-v0.2.12 tag publish once this merges; the release workflow requires the tag be reachable from main.

Verified npm pack --dry-run on the bumped package emits workweave-router-0.2.12.tgz containing codex-status.sh (10.9kB) and an install.sh that writes the Stop hook.

Honest edges

  • Lags by a turn. Router telemetry is asynchronous, so the just-finished turn is absent even from a blocking read. Cache-then-refresh costs a turn of freshness and buys never stalling Codex.
  • Negative totals render nothing. When the router picked a pricier model for quality, saved -$0.02 is worse than silence.
  • Sub-cent reads <$0.01, never $0.00 — the latter is indistinguishable from "did not beat your selection".
  • Self-hosted routers predating the endpoint 404 and degrade to model-only. WEAVE_CODEX_STATUS_SAVINGS=0 opts out.
  • This renders to the terminal title bar, not Codex's bottom status line, which Codex owns.

Verification

  • go build ./..., go vet, gofmt, make generate (no drift) — clean. Full make test green on the original PR; build + touched-package tests re-run after the cherry-pick.
  • Handler tests: savings math, installation scoping, negative totals, 404, 500, and that an unauthenticated caller never reaches the repository.
  • Shell tests: first turn renders model-only while the fetch is in flight, next turn renders saved $0.32, plus negative / sub-cent / malformed-cache / unreachable-router / opt-out paths.
  • Real SQL + adapter exercised against live Postgres with seeded telemetry: 3 rows summed across both span types, another session excluded, and a second installation correctly got ErrSessionCostNotFound for the same id. Seed data removed after.
  • Includes the install.sh heredoc sync fix Bugbot caught on feat(codex): source session savings from the router #1109 (the curl installer embeds its own copy and had gone stale), plus the byte-identical guard that cc-statusline_test.sh already had and codex-status_test.sh lacked — which is how they diverged. Confirmed the guard fails on deliberate drift.

🤖 Generated with Weave Router

The Codex status hook could show which model served a turn but not what
it saved. Porting the Claude Code statusline's per-turn arithmetic is not
an option here: Codex persists its own requested model on every turn and
never the one that served, so pricing both sides of the comparison
locally always yields zero.

The router already computes the real number. Add
GET /v1/sessions/:session_id/cost, authed by the rk_ key the Codex
install already holds and scoped to the caller's installation, returning
the session's committed actual/requested cost and their difference. It
sums served turns plus billed auxiliary inference, so the total is what
the session actually cost.

The hook renders that value, fetching it in a detached subshell whose
result the next turn reads, so no turn blocks on the network and every
failure path degrades to model-only. A session where the router spent
more shows no clause rather than a negative saving.

Signed-off-by: munir-weave <munir@workweave.ai>
(cherry picked from commit 2b53c64)
install.sh embeds the Codex status helper as a heredoc for the standalone
curl | sh path, which has no sibling asset to copy. The savings lookup
landed only in install/codex-status.sh, so every curl install kept
shipping the old helper and could never render saved $X.XX.

Regenerate the embed from the canonical helper and add the byte-identical
check that install/tests/cc-statusline_test.sh already has for its own
heredoc — nothing was keeping these two copies in sync, which is how they
diverged in the first place.

Signed-off-by: munir-weave <munir@workweave.ai>
(cherry picked from commit 43348b1)
Apply workweave-bot's comment-length suggestions verbatim across the
session-cost surface. Each keeps the non-obvious WHY (deliberate
not-found indistinguishability, micros-at-encoding rounding, the
installation_id authz boundary, SQLC's nullable-column pointer) while
dropping the restated mechanics.

Signed-off-by: munir-weave <munir@workweave.ai>
(cherry picked from commit d40e3d2)
Apply workweave-bot's remaining comment-length suggestions verbatim.

Signed-off-by: munir-weave <munir@workweave.ai>
(cherry picked from commit 9334914)
0.2.11 predates the Codex status integration, so the published tarball
ships no codex-status.sh and an install.sh that writes no Stop hook.
Every Codex user is therefore still on the pre-hooks installer with no
way to pick the feature up — the helper has no self-refresh, so a
publish is the only delivery path.

Signed-off-by: munir-weave <munir@workweave.ai>
@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown

Claude finished @munir-weave's task —— View job


Posted one comment-length advisory on internal/proxy/telemetry.go lines 28–30 — the SessionCost struct's 3-line godoc opens with a sentence that restates the type name; the two lines carrying the genuine WHY (micros-as-integer invariant, Actual/Requested abbreviation semantics) are kept in the suggestion.

@workweave-bot workweave-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Advisory only — comment-length nits. Won't block merge.

Comment on lines +28 to +30
// SessionCost is the committed router cost of one client session.
// Costs are USD micros ($1.00 = 1,000,000) summed as integers so no float rounding accumulates.
// Actual = router's chosen binding; Requested = client's originally-requested model.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// SessionCost is the committed router cost of one client session.
// Costs are USD micros ($1.00 = 1,000,000) summed as integers so no float rounding accumulates.
// Actual = router's chosen binding; Requested = client's originally-requested model.
// Costs are USD micros ($1.00 = 1,000,000) summed as integers so no float rounding accumulates.
// Actual = router's chosen binding; Requested = client's originally-requested model.

Was 3 lines; line 1 restates the type name. The WHY (micros-as-integer, Actual/Requested semantics) fits in 2.

@greptile-apps

greptile-apps Bot commented Aug 29, 2026

Copy link
Copy Markdown

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex authored the scope reproduction test script codex-status-scope-repro.sh and used it to reproduce the Codex status scope for the P2 finding.
  • T-Rex executed the reproduction script and captured its run output, including the after-run results with a successful exit.
  • T-Rex captured Codex status hook output showing the credentials used when the project config is missing, validating the scope behavior.
  • Additionally, T-Rex posted a P1 finding and summarized the general-contract-validation proof, detailing the test source and the results observed.

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (1)

  1. General comment

    P1 Project-scoped Codex helper uses user-scoped router credentials when its config is absent

    • Bug
      • At install/codex-status.sh:96-100, the helper searches $helper_dir/config.toml and then $HOME/.codex/config.toml. A project-scoped helper with no local config therefore reads the user config. The executed Stop-hook path then passes that user-managed URL and X-Weave-Router-Key to curl at lines 130-162. The isolated runtime capture shows USER_SCOPE_SECRET sent to http://user-scope.invalid/v1/sessions/scope-test/cost.
    • Cause
      • The fallback loop has no distinction between a user-scope helper (where the HOME config is intended) and a project-scope helper (where only the adjacent config should be eligible).
    • Fix
      • Determine whether the helper is user-scoped before selecting candidates. For a project-scoped helper, read only $helper_dir/config.toml and return no endpoint if it is absent or unreadable; retain $HOME/.codex/config.toml only for the known user-scope helper location. Note that the claimed unreadable-file fallback is not present: -f selects the unreadable file and awk failure yields no endpoint.

    T-Rex Ran code and verified through T-Rex

Reviews (1): Last reviewed commit: "chore(install): bump router npm package ..." | Re-trigger Greptile

Comment thread install/codex-status.sh
Comment on lines +96 to +100
for candidate in "$helper_dir/config.toml" "$HOME/.codex/config.toml"; do
if [ -f "$candidate" ]; then
config="$candidate"
break
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Project helper uses user configuration

When a project-scoped helper remains after its adjacent config.toml has been removed, this loop selects ~/.codex/config.toml. The Stop hook then fetches the project session's cost using the user installation's router URL and key, so the terminal can show savings from the wrong installation. Only permit the home configuration for the known user-scoped helper; a project helper with no local managed configuration should skip the refresh.

Artifacts

Isolated Codex status scope reproduction script

  • The authored executable harness creates missing and unreadable project-config cases and records the curl arguments, demonstrating which configuration scope the hook uses.

Codex status hook execution showing user credentials used when project config is missing

  • A successful isolated hook execution records curl receiving the user-scoped URL and USER_SCOPE_SECRET for the missing project-config case, while the unreadable case makes no request; the fallback is real only for absence.

Repeated Codex status hook execution confirming scope behavior

  • A second successful execution produces the same missing-config credential use and unreadable-config no-request result, confirming the observed shell behavior.

View artifacts

T-Rex Ran code and verified through T-Rex

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Reviewed this against root AGENTS.md plus db/CLAUDE.md and internal/postgres/CLAUDE.mdit conforms, so no changes from me. Thanks for the very legible re-land; the "why the endpoint exists" framing (Codex only ever records the requested model, so client-side pricing would compare a model with itself) made the design easy to check.

What I verified rather than assumed:

  • Layering. Raw SQL stays in db/queries/, pgx stays inside internal/postgres, the domain type + sentinel (SessionCost, ErrSessionCostNotFound) live in internal/proxy with the handler mapping to HTTP status codes — exactly the "sentinel errors live in the package that returns them; don't export HTTP semantics from inner-ring packages" rule (AGENTS.md § Conventions → Go style). internal/sqlc is regenerated, not hand-edited.
  • Handler home. A product-surface /v1/* route implemented in internal/api/admin looked odd at first glance, but admin.DisplaySettingsHandler on the same passthrough group is the existing precedent, and mounting outside the selfhosted block is what AGENTS.md § Deployment modes prescribes for product surface. The server_test.go addition pins that.
  • Query conventions. Named params with casts, Get* prefix, explanatory comment for the generated godoc (db/CLAUDE.md § Query conventions). pgx.ErrNoRows via errors.Is matches billing_repo.go, and pgx.ErrNoRows wraps sql.ErrNoRows (there's even pgx_chain_test.go guarding that).
  • Tests are non-tautological (AGENTS.md § Tests): savings arithmetic, installation scoping observed through the fake, negative totals, 404 vs 500, and the unauthenticated case asserting the repository is never reached. In-memory fake over a mock, external admin_test package, no DB-backed test in internal/. The c.Set("router_installation", ...) literal matches the dominant pattern in internal/server/middleware/*_test.go, so it isn't a magic-string violation.
  • Error handling on the 500 path mirrors metrics.go byte for byte (abort with a static message, no log-and-continue).
  • Comment density in install/ is in line with the local norm — cc-statusline.sh carries ~300 comment lines of the same explanatory kind, so the multi-line "why" blocks here fit rather than violate AGENTS.md's terse-comment rule.
  • The version bump is genuinely load-bearing: npm view @workweave/router version returns 0.2.11, so main's version is indeed already published and 0.2.12 is the only way to ship the Codex status feature.

Two non-blocking notes, neither a rule violation:

  1. Greptile's P2 on read_codex_endpoint is real as written — a project-scoped helper whose adjacent config.toml has been removed falls through to $HOME/.codex/config.toml and refreshes with the user-scope key. Practically it degrades to a 404 (the session id won't exist under the other installation), so I'd call it cosmetic, but restricting the home-config candidate to the user-scope helper would make the comment above the loop unconditionally true.
  2. The install.sh heredoc copy of codex-status.sh plus the byte-identical guard is a nice catch; worth remembering that the guard is the only thing keeping the two from silently diverging again.

Nothing to action on my side — this reads like it was written by someone who already had the conventions in hand.

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.

2 participants