Skip to content

fix(chat): switch completion TPS calculation to OpenCode-style generation samples + live still - #26

Merged
Blankeos merged 2 commits into
mainfrom
feat/better-tps-tally
Aug 13, 2026
Merged

fix(chat): switch completion TPS calculation to OpenCode-style generation samples + live still#26
Blankeos merged 2 commits into
mainfrom
feat/better-tps-tally

Conversation

@Blankeos

Copy link
Copy Markdown
Owner

Track assistant text generation as discrete samples from first text token to completion/tool-call boundary, then compute aggregate throughput via (tokens - 1) / duration with proper exclusion of tool-call segments and pause intervals; fall back when no valid rate exists. Persist tokens_per_sec support in session message conversion and message type defaults.

…tion samples + live still
Track assistant text generation as discrete samples from first text token to completion/tool-call boundary, then compute aggregate throughput via `(tokens - 1) / duration` with proper exclusion of tool-call segments and pause intervals; fall back when no valid rate exists. Persist `tokens_per_sec` support in session message conversion and message type defaults.
@Blankeos

Blankeos commented Aug 13, 2026

Copy link
Copy Markdown
OwnerAuthor

PR Review: feat/better-tps-tallymain

Merge confidence: 4.5/5 (was 3/5)

No open GitHub PR for this branch at last check. HEAD feature commit: b1de123 + uncommitted checklist fixes below.


Body (copy for GitHub)

Switches assistant throughput (t/s) from wall-clock streaming timers to OpenCode-style generation samples: discrete text-generation windows that pause across tool-call gaps, then aggregate via (tokens - 1) / duration. Live “still” t/s appears in the input footer while streaming; completed metadata uses the same sample math (with fallback when no valid sample rate exists). Adds Message.tokens_per_sec (in-memory) and wires session conversion defaults.

Also includes small streaming correctness fixes found while greening tests: reuse turn-level token counter for generation samples (avoids re-loading tiktoken mid-stream), and defer full layout invalidation until the adaptive tool-heavy interval elapses.


Summary of changes

AreaWhat changed
src/ui/components/chat.rsGenerationSample + active generation tracking; pause/resume around tool calls; still t/s API; metadata uses (n-1)/s; reuse turn counter for samples; gate layout invalidation on adaptive interval
src/app.rsCall sites for pause/resume/end-for-tool-calls; still rate into input; idempotent retry-status setter; test warm-up for tiktoken
src/ui/components/input.rsLive still NNt/s in footer during stream
src/session/types.rstokens_per_sec: Option<f64> on Message
src/persistence/conversions.rsLoad path sets tokens_per_sec: None

Regressions / risks

Previously failing (fixed on this tree)

  1. completed_assistant_metadata_includes_latency_metrics — expected 50t/s; formula is (40-1)/0.8s49t/s. Assertion updated.
  2. assistant_reasoning_can_be_collapsed — asserted non-existent "Thinking collapsed" label. Now accepts real Thought for / Thinking labels.
  3. tool_heavy_streaming_message_defers_full_layout_refresh — markdown refresh was bumping render_revision even when layout interval still open. Production now only invalidates layout when interval elapses; test freezes the clock to avoid wall-clock flakes.
  4. streaming_text_persistence_is_throttled_* / streaming_text_chunks_do_not_dirty_* — first text chunk after stream start paid ~280ms tiktoken load, blowing the 250ms snapshot/probe windows. Generation samples now clone/reset the turn counter; tests warm the counter before measuring.

Remaining (low)

  • Completed metadata t/s is systematically lower than old wall-clock math (n-1 + tool-gap exclusion). Intentional OpenCode parity — note in release notes.
  • tokens_per_sec is not persisted — reload falls back to duration/output_tokens math.
  • Single-token / zero-duration samples → no t/s (None).

Migrations?

No DB / schema migrations required.

Runtime-only field; SQLite message columns unchanged. No backfill.


Pre-merge checklist

  • Fix completed_assistant_metadata_includes_latency_metrics for (n-1) formula
  • Fix collapsed-thinking label assertion
  • Fix tool-heavy deferred layout (production + test)
  • Fix streaming text dirty/persist tests (tiktoken warm path)
  • cargo test --bin crabcode chat::tests119 passed
  • cargo test --bin crabcode app::tests115 passed
  • cargo fmt
  • Manual smoke (still recommended):
    • Plain stream → still t/s + final metadata
    • Tool multi-round → pause during tools; final t/s excludes gap
    • Abort / error mid-stream
    • Reload session → fallback metadata OK
    • Single-token reply
  • Open PR if not opened; document OpenCode-style formula so lower t/s isn’t filed as a bug
  • Commit the uncommitted checklist fixes (src/app.rs, src/ui/components/chat.rs) before merge

Checks run (this pass)

CheckResult
Latency metrics unit testPASS (49t/s)
Thinking collapse unit testPASS
Tool-heavy defer layout unit testPASS
Streaming dirty / persist unit testsPASS
Full chat::tests119 passed
Full app::tests115 passed
cargo fmtapplied
Schema / migrations scanno new message-column migration

Verdict

Ready to merge after commit + optional manual smoke. Design direction is sound; checklist items that could be fixed without mutating user data are done. Confidence 4.5/5 (half-point reserved for manual stream/tool smoke only).

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:b1de123f06

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2674 to +2678
if let Some(counter) = self.generation_token_counter.as_mut() {
let tokens = counter.add_text(chunk);
if let Some(sample) = self.active_generation.as_mut() {
sample.tokens = tokens;
}

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 Badge Reset the active TPS sample on stream rollback

When a provider retry emits StreamRollback after partial text, rollback_streamed_output resets only the turn-level streaming_token_counter; this newly added generation_token_counter and the active sample's start time remain unchanged. Retried text is therefore counted on top of discarded text, while retry/backoff time remains in the sample duration, corrupting both live and finalized TPS. Reset or discard the active generation sample when a rollback succeeds.

Useful? React with 👍 / 👎.

output_tokens: msg
.output_tokens
.and_then(|v| if v > 0 { Some(v as usize) } else { None }),
tokens_per_sec: None,

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 Badge Persist the precomputed TPS value

Every saved and reloaded assistant message receives None here because the persistence message/schema does not carry tokens_per_sec. For multi-step turns, the fallback cannot reconstruct the same value: output_tokens includes reasoning and tool-call-ending steps, while the aggregate TPS excludes those contributions. Consequently reopening a session changes the displayed throughput, so this field needs to be round-tripped through persistence.

Useful? React with 👍 / 👎.

Comment on lines 2567 to +2570
msg.output_tokens = Some(token_count);
msg.token_count = Some(token_count);
msg.duration_ms = Some(decode_duration_ms);
msg.tokens_per_sec = final_tps;

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 Badge Expose the aggregate TPS to remote clients

In the remote-host context, this computed value stops at the session message: RemoteMessage in src/remote/mod.rs has no corresponding field, and assistantMetrics in remote-client/src/pages/index/thread-model.ts still calculates tokens / (tn - t1). Browser clients therefore continue to include tool execution and reasoning in the old formula instead of showing the new sample aggregate; serialize this value and prefer it in the remote client.

Useful? React with 👍 / 👎.

- reuse the previous turn’s streaming token counter when available to avoid repeated tiktoken warm-up after tool calls - gate markdown stream cache invalidation behind layout interval cadence and keep pending dirty state for later flush - update streaming and token-rate tests for revised collapsed-reasoning label and layout-timing behavior
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 13, 2026

Copy link
Copy Markdown

Deploying crabcode with Cloudflare Pages Cloudflare Pages

Latest commit:0813fa6
Status: ✅ Deploy successful!
Preview URL:https://b27c08f4.crabcode.pages.dev
Branch Preview URL:https://feat-better-tps-tally.crabcode.pages.dev

View logs

@Blankeos
Blankeos merged commit 27a6a29 into mainAug 13, 2026
7 checks passed
Sign up for freeto 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

@Blankeos