Repository navigation
fix(reasoning): use fresh parser on non-streaming path to avoid shared mutex - #1642
Conversation
…d mutex Non-streaming reasoning extraction took a pooled parser (Arc<Mutex<Box<dyn ReasoningParser>>>) shared per model, so concurrent requests for the same model serialized on one mutex even though each request reset() the parser and discards its state. Switch the non-streaming paths (chat/messages processors and the /separate_reasoning handler) to a fresh per-request parser via create_parser, the same way the streaming path already isolates state. Drops the now-unused pooled helper. Signed-off-by: Simo Lin <25425177+slin1237@users.noreply.github.com>
📝 WalkthroughWalkthroughThe PR refactors reasoning parser management from a pooled, shared, mutex-locked pattern to per-request instantiation. The pooled ChangesPer-request reasoning parser refactor
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Code Review
This pull request replaces the use of shared pooled reasoning parsers with fresh, independent parser instances per request for non-streaming extraction. This change avoids serialization bottlenecks on the shared pooled mutex and ensures proper state isolation. The unused get_reasoning_parser helper has been removed, and unit tests have been added to verify that independent parser instances are correctly created and configured. There are no review comments to address.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Clean, correct refactoring. The switch from pooled parser + mutex to fresh per-request parser eliminates unnecessary serialization and the subtle state-leak risk (especially in handlers.rs where reset() was missing). All call sites are properly guarded by reasoning_parser_available, so the Option return from create_reasoning_parser/create_for_model won't cause silent skipping. Tests cover the key invariant (instance independence). No issues found.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
model_gateway/src/routers/grpc/utils/parsers.rs (1)
76-100:⚠️ Potential issue | 🟠 Major | ⚡ Quick winMake the availability check match this fallback behavior.
create_reasoning_parser()now warns and falls back tocreate_for_model(model), but the gRPC non-streaming callers only reach this code aftercheck_reasoning_parser_availability(...)succeeds. That helper still returnsfalseas soon asconfigured_parseris unknown, so a typo inconfigured_reasoning_parserskips reasoning extraction entirely and this new fallback never runs.Suggested fix
pub(crate) fn check_reasoning_parser_availability( reasoning_parser_factory: &ReasoningParserFactory, configured_parser: Option<&str>, model: &str, ) -> bool { if let Some(parser_name) = configured_parser { - reasoning_parser_factory.registry().has_parser(parser_name) + reasoning_parser_factory.registry().has_parser(parser_name) + || reasoning_parser_factory.registry().has_parser_for_model(model) } else { reasoning_parser_factory .registry() .has_parser_for_model(model) } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@model_gateway/src/routers/grpc/utils/parsers.rs` around lines 76 - 100, The availability check in check_reasoning_parser_availability must match create_reasoning_parser's fallback logic: when a configured_parser is provided but registry().create_parser(parser_name) returns None, the helper should not immediately return false — instead attempt registry().create_for_model(model) and return true if that yields a parser. Update check_reasoning_parser_availability to call ReasoningParserFactory.registry().create_parser(parser_name) and, on None, call registry().create_for_model(model) to determine availability (mirroring create_reasoning_parser), so a typo in configured_reasoning_parser will fall back to model-based selection.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@model_gateway/src/routers/grpc/utils/parsers.rs`:
- Around line 76-100: The availability check in
check_reasoning_parser_availability must match create_reasoning_parser's
fallback logic: when a configured_parser is provided but
registry().create_parser(parser_name) returns None, the helper should not
immediately return false — instead attempt registry().create_for_model(model)
and return true if that yields a parser. Update
check_reasoning_parser_availability to call
ReasoningParserFactory.registry().create_parser(parser_name) and, on None, call
registry().create_for_model(model) to determine availability (mirroring
create_reasoning_parser), so a typo in configured_reasoning_parser will fall
back to model-based selection.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 37f92b1d-a4e0-478a-a4bf-60eab3d0b067
📒 Files selected for processing (4)
model_gateway/src/routers/grpc/regular/processor.rsmodel_gateway/src/routers/grpc/utils/mod.rsmodel_gateway/src/routers/grpc/utils/parsers.rsmodel_gateway/src/routers/parse/handlers.rs
|
Maybe we can remove the shared mutex after this PR? Right now I don't see the usage of get_parser. |
Description
Problem
Non-streaming reasoning extraction served all requests for a given model from one pooled parser instance (
Arc<Mutex<Box<dyn ReasoningParser>>>). Every non-streaming request wouldlock().awaitthat single mutex,reset()the parser, rundetect_and_parse_reasoning, then drop the state — so concurrent requests for the same model serialized on one mutex despite needing no shared state. The streaming path already avoids this by creating a fresh parser per request/index for state isolation.From the codebase audit: crates/reasoning_parser/src/factory.rs:64 — Non-streaming requests serialize on one shared parser mutex per model.
Solution
Switch the non-streaming paths to a fresh, owned per-request parser via
create_parser/create_reasoning_parser(the same approach the streaming path already uses). A fresh parser is inherently clean, so the explicitreset()and thelock().awaitare no longer needed, removing the per-model contention entirely. The now-unused pooled helperget_reasoning_parseris dropped.Changes
model_gateway/src/routers/grpc/regular/processor.rs: both non-streaming sites (chat completions + Anthropic messages) now take a fresh parser viautils::create_reasoning_parser(...)instead of the pooled parser +lock().await+reset().model_gateway/src/routers/parse/handlers.rs: the/separate_reasoningendpoint usesregistry().create_parser(...)instead ofget_pooled_parser(...).lock().await.model_gateway/src/routers/grpc/utils/parsers.rs: removed the now-unusedget_reasoning_parserpooled helper (and its unusedPooledParserimport); refreshed thecreate_reasoning_parserdoc to note it serves both streaming and non-streaming. Added unit tests.model_gateway/src/routers/grpc/utils/mod.rs: dropped theget_reasoning_parserre-export.The tool-parser pooled path is intentionally left unchanged (separate concern/finding).
Test Plan
Added focused unit tests in
parsers.rs:create_reasoning_parser_returns_independent_instances— two parsers created for the same model are independent:mark_reasoning_started()on one does not flip the other'sis_in_reasoning(), and the untouched instance still parses a full<think>…</think>document correctly. This is exactly the isolation the shared pooled parser violated.create_reasoning_parser_honors_configured_parser— the configured-parser path resolves to the requested parser.Gate (sccache disabled, scoped to
smg):Checklist
cargo +nightly fmtpassescargo clippy --all-targets --all-features -- -D warningspassesSummary by CodeRabbit
Refactor
Tests