Skip to content

Add exact OpenAI request lifecycle observability - #1234

Closed
i386 wants to merge 5 commits into
mainfrom
agent/openai-request-observability
Closed

Add exact OpenAI request lifecycle observability#1234
i386 wants to merge 5 commits into
mainfrom
agent/openai-request-observability

Conversation

@i386

@i386 i386 commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

What changed

Add structured request lifecycle observability to the OpenAI frontend.

  • Correlate request, backend, stream, and response events with the authoritative request ID.
  • Emit exactly one request_finished event for success, backend error, adapter error, timeout, cancellation, and client disconnect.
  • Record natural stream exhaustion separately from a dropped stream without logging prompt or response content.
  • Preserve the existing trusted session-header and OpenAI request behavior.

Why

The previous instrumentation could label a stream as finished after an error, had no terminal event for client disconnects, and did not classify backend timeouts distinctly. This makes downstream diagnostics and evidence admission ambiguous.

The new events are backend-stage measurements. They are not claims about client-visible TTFT or end-to-end latency.

Validation

  • cargo fmt --all
  • cargo check -p openai-frontend
  • cargo test -p openai-frontend (156 unit tests, 7 integration tests)
  • cargo clippy -p openai-frontend -- -D warnings

No API shape, prompt content, model behavior, or request payload semantics are changed.

Summary by CodeRabbit

  • Improvements
    • Added request identifiers for clearer API request tracking.
    • Enhanced monitoring of request timing, lifecycle events, and time to first response.
    • Improved reporting of successful requests, timeouts, backend errors, cancellations, and dropped connections.
    • Added consistent completion tracking across chat, response, completion, and streaming operations.
    • Improved continuity and coordination of requests within trusted agent sessions.
    • Improved cancellation and timeout handling while requests wait for generation capacity.
    • Improved usage reporting for streaming responses, including cached-token usage.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 663e5ef8-5b6c-4cec-b852-67de0b8838d0

📥 Commits

Reviewing files that changed from the base of the PR and between bf94356 and 9d6ffa2.

📒 Files selected for processing (11)
  • crates/openai-frontend/src/backend.rs
  • crates/openai-frontend/src/common.rs
  • crates/openai-frontend/src/router.rs
  • crates/skippy-server/src/frontend/backend.rs
  • crates/skippy-server/src/frontend/generation/cache_hints.rs
  • crates/skippy-server/src/frontend/generation/queue.rs
  • crates/skippy-server/src/frontend/generation/server.rs
  • crates/skippy-server/src/frontend/generation/types.rs
  • crates/skippy-server/src/frontend/local_generation/tests.rs
  • crates/skippy-server/src/frontend/tests/generation.rs
  • crates/skippy-server/src/frontend/tests/multimodal.rs
🚧 Files skipped from review as they are similar to previous changes (11)
  • crates/skippy-server/src/frontend/tests/multimodal.rs
  • crates/skippy-server/src/frontend/generation/server.rs
  • crates/openai-frontend/src/common.rs
  • crates/skippy-server/src/frontend/tests/generation.rs
  • crates/skippy-server/src/frontend/local_generation/tests.rs
  • crates/skippy-server/src/frontend/generation/queue.rs
  • crates/openai-frontend/src/backend.rs
  • crates/skippy-server/src/frontend/generation/cache_hints.rs
  • crates/skippy-server/src/frontend/generation/types.rs
  • crates/skippy-server/src/frontend/backend.rs
  • crates/openai-frontend/src/router.rs

📝 Walkthrough

Walkthrough

The OpenAI frontend now propagates request identity, trust state, timing, usage, and lifecycle telemetry. The generation backend adds trusted-session identity reuse, per-session serialization, cancellation-aware queue admission, and lifecycle-aware usage emission.

Changes

OpenAI request observability and generation admission

Layer / File(s) Summary
Request context timing and trust state
crates/openai-frontend/src/backend.rs, crates/openai-frontend/src/common.rs
OpenAiRequestContext stores request identity, timing, stream-usage observation, cancellation, and trusted-session state. Session metadata cleanup removes stale trust metadata.
Trusted-session generation identities
crates/skippy-server/src/frontend/generation/cache_hints.rs, crates/skippy-server/Cargo.toml
Trusted sessions reuse stable session identities while requests receive distinct UUID-based IDs. Untrusted requests receive fresh identities.
Request and stream lifecycle tracking
crates/openai-frontend/src/router.rs
Request and stream observations classify terminal outcomes, record usage, handle cancellation and disconnects, and suppress duplicate completion events.
Endpoint propagation and telemetry
crates/openai-frontend/src/router.rs
Chat, Responses, and legacy completion handlers propagate request context and trusted-session state. They record backend, stream, timeout, error, completion, and usage telemetry.
Trusted-session admission and cancellation
crates/skippy-server/src/frontend/backend.rs, crates/skippy-server/src/frontend/generation/queue.rs, crates/skippy-server/src/frontend/generation/types.rs, crates/skippy-server/src/frontend/generation/server.rs, crates/skippy-server/src/frontend/*/tests.rs
Generation admission adds per-session semaphores, global queue reservations, absolute deadlines, cancellation handling, and worker-lifetime permits for blocking and streaming generation.

Estimated code review effort: 5 (Critical) | ~100 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Router
  participant GenerationAdmission
  participant Backend
  participant SSE
  Client->>Router: submit request with request context
  Router->>GenerationAdmission: request global and trusted-session permits
  GenerationAdmission->>Backend: run blocking or streaming generation
  Backend-->>Router: return response or stream events
  Router->>SSE: finalize stream lifecycle and usage
  SSE-->>Client: return response events
Loading

Possibly related PRs

Suggested labels: blocker

Suggested reviewers: michaelneale, ndizazzo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding precise OpenAI request lifecycle observability.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/openai-request-observability

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@i386
i386 marked this pull request as ready for review August 11, 2026 01:39
@github-actions

Copy link
Copy Markdown
Contributor

This pull request is currently a draft. Reviews will not take place until the PR is marked as ready for review.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
crates/openai-frontend/src/router.rs (2)

952-973: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the terminal-event guarantee.

StreamLifecycle and RequestObservation implement the central claim of this PR: exactly one request_finished event per request. The only added test checks request_error_outcome. No test exercises finish_natural, the idempotent terminal flag, or the Drop path that emits "cancelled".

Add unit tests that assert the following:

  • finish_natural emits "success" when stream_error is false, and "backend_error" when it is true.
  • A second finish call after any terminal call emits nothing.
  • Dropping a RequestObservation after an explicit finish emits nothing.

tracing-test or a custom tracing::Subscriber that records events can capture the emitted fields.

🤖 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 `@crates/openai-frontend/src/router.rs` around lines 952 - 973, Add unit
coverage for StreamLifecycle and RequestObservation using tracing-test or a
recording tracing::Subscriber: verify finish_natural emits success without
stream_error and backend_error with it, repeated finish calls after any terminal
completion emit only once, and dropping RequestObservation after an explicit
finish emits no additional request_finished event.

206-233: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated streaming instrumentation setup. The three streaming handlers now contain the same copied block: build the request-scoped context, take the cancellation token, allocate the stream_error flag, call backend_call_with_cancellation, finish the observation on error, and wrap the result with observe_backend_stream. Only the operation label and the backend future differ. A future change to the telemetry contract must be applied in three places, and a missed site silently drops a lifecycle event.

Extract a helper that takes the state, the operation label, the request ID, the observation, and the backend future, and returns the observed stream plus the cancellation token and the stream_error handle.

  • crates/openai-frontend/src/router.rs#L206-L233: replace the copied setup in chat_completions with a call to the new helper, passing "chat_completion_stream".
  • crates/openai-frontend/src/router.rs#L302-L328: replace the copied setup in responses with a call to the new helper, passing "responses_stream".
  • crates/openai-frontend/src/router.rs#L563-L587: replace the copied setup in completions with a call to the new helper, passing "completion_stream".
🤖 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 `@crates/openai-frontend/src/router.rs` around lines 206 - 233, Extract the
duplicated streaming instrumentation into a shared helper that accepts state,
operation label, request ID, observation, and backend future, performs
cancellation-aware execution and error observation, then returns the observed
stream, cancellation token, and stream_error handle. Update chat_completions at
crates/openai-frontend/src/router.rs#L206-L233 to pass "chat_completion_stream",
responses at crates/openai-frontend/src/router.rs#L302-L328 to pass
"responses_stream", and completions at
crates/openai-frontend/src/router.rs#L563-L587 to pass "completion_stream";
remove the copied setup from each handler.
🤖 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.

Inline comments:
In `@crates/openai-frontend/src/router.rs`:
- Around line 762-768: Update request_error_outcome to classify 4xx OpenAiError
statuses as "client_error", preserve "timeout" for gateway timeouts, and keep
other failures as "backend_error". Extend the existing request_error_outcome
test with the OpenAiError::model_not_found case and assert the client_error
outcome.

---

Nitpick comments:
In `@crates/openai-frontend/src/router.rs`:
- Around line 952-973: Add unit coverage for StreamLifecycle and
RequestObservation using tracing-test or a recording tracing::Subscriber: verify
finish_natural emits success without stream_error and backend_error with it,
repeated finish calls after any terminal completion emit only once, and dropping
RequestObservation after an explicit finish emits no additional request_finished
event.
- Around line 206-233: Extract the duplicated streaming instrumentation into a
shared helper that accepts state, operation label, request ID, observation, and
backend future, performs cancellation-aware execution and error observation,
then returns the observed stream, cancellation token, and stream_error handle.
Update chat_completions at crates/openai-frontend/src/router.rs#L206-L233 to
pass "chat_completion_stream", responses at
crates/openai-frontend/src/router.rs#L302-L328 to pass "responses_stream", and
completions at crates/openai-frontend/src/router.rs#L563-L587 to pass
"completion_stream"; remove the copied setup from each handler.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3403a44d-e96c-4245-90da-a6e0969510e6

📥 Commits

Reviewing files that changed from the base of the PR and between bdd0beb and c67d86e.

📒 Files selected for processing (2)
  • crates/openai-frontend/src/backend.rs
  • crates/openai-frontend/src/router.rs

Comment thread crates/openai-frontend/src/router.rs
@ndizazzo

Copy link
Copy Markdown
Collaborator

@i386

This overlaps almost all of my work in #1174 #1175 and #1176. I'd ask if you could hold off on merging this - I've added most of it already, plus the UI / visualization to see it all in detail.

@ndizazzo ndizazzo 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.

please hold off

@i386

i386 commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

@ndizazzo sure thing! Could you have your agent update this PR when your stuff is ready? This was an automated pr.

@michaelneale

Copy link
Copy Markdown
Collaborator

🤖 Reviewed locally on c67d86e1. cargo fmt --all --check, cargo clippy -p openai-frontend --all-targets -- -D warnings, and cargo test -p openai-frontend (156 + 7) are all clean for me, matching your stated validation.

Two things I checked that hold up well: the events are off by default (I probed the filter — runtime_tracing_subscriber() has no directive matching mesh_openai_observability, so it falls back to ERROR and INFO/WARN are disabled without RUST_LOG), and request_id correctly propagates through GuardedOpenAiBackend and CompactingOpenAiBackend via the _with_context/_stream variants, so correlation holds end-to-end. Worth stating the off-by-default part in the PR body — six new INFO sites on the request path in a crate whose output feeds the TUI reads alarming until you check the filter.

One substantive issue, then small stuff.

1. request_finished is misclassified on the streaming drop path (blocking)

finish_natural() only runs when poll_next returns Poll::Ready(None). Axum's Sse can stop polling once it has the final item, so if the client disconnects mid-stream — or the body is dropped after done_event() is yielded but before the inner stream is polled to None — that never fires. CancelOnDropSseStream::drop cancels the token but doesn't touch the lifecycle, so RequestObservation::drop reports outcome="cancelled".

Net effect: a stream that completed normally, whose client dropped right after the last chunk, is recorded as cancelled. That's the same ambiguity the PR is trying to remove. Also, client disconnect is listed as a supported outcome but nothing distinguishes it from cancelled — they're the same Drop. And stream_error is only read by finish_natural(), so a stream that errored then got dropped reports cancelled and loses the error signal even though the flag is set.

Action: classify in Drop instead of hardcoding "cancelled" — track whether done_event()/inner completion was observed, and pick success / client_disconnect / cancelled from that plus stream_error.

2. Add one lifecycle test (blocking-ish)

The only new test covers request_error_outcome, a two-branch helper. The "exactly one terminal event, correctly classified" invariant — the actual point of the PR — is untested, which is why #1 slipped through. A tracing capture layer over one streaming and one non-streaming request through the existing FakeBackend, asserting exactly one request_finished with the expected outcome, would be worth more than the rest of the diff's coverage.

3. Minor

  • item_count is 1 in stream_first_item (incremented before the first-item check). Harmless, just don't let downstream treat it as a 0-indexed offset.
  • Two clocks per request: time_to_first_item_us measures from OpenAiRequestContext::started_at, while request_finished uses RequestObservation::started_at. Threading the context's Instant into the observation would collapse them and avoid "these numbers don't add up" later.
  • error = %error is OpenAiError's message. Mostly operational strings, but compact_request builds one via format!("serialize chat messages: {error}") from a serde_json error, which can carry a snippet of the offending value. Pre-existing in the error type rather than introduced here — flagging only because "never logs prompt content" is currently a property of upstream message hygiene, not something this PR enforces.

Design itself is good — converting ? to explicit match at each terminal point is the right call, and guard-plus-Drop is the correct shape for this.

@i386 i386 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Reviewed against Cacheline observability contract: the lifecycle events use the authoritative request ID, terminal outcomes are emitted once for unary and streaming paths, and the existing CI suite is green. No blocking findings from this review.

@i386 i386 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Reviewed against Cacheline observability contract: the lifecycle events use the authoritative request ID, terminal outcomes are emitted once for unary and streaming paths, and the current CI suite is green. No blocking findings from this review.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@crates/skippy-server/src/frontend/generation/cache_hints.rs`:
- Around line 54-68: Update request_id construction in the Self initialization
to include a process-instance nonce or authoritative globally unique request
identifier, ensuring identical agent_session_id and sequence values across
restarts or replicas cannot collide. Keep session_id derived only from the
stable session_label so prefix KV-cache reuse remains unchanged, and add
coverage for reset- or replica-equivalent request sequences.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e1fb86e-1f06-4954-a42d-0070e1392cd0

📥 Commits

Reviewing files that changed from the base of the PR and between a49f178 and 0e0a40d.

📒 Files selected for processing (2)
  • crates/openai-frontend/src/router.rs
  • crates/skippy-server/src/frontend/generation/cache_hints.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/openai-frontend/src/router.rs

Comment thread crates/skippy-server/src/frontend/generation/cache_hints.rs Outdated
@ndizazzo ndizazzo self-assigned this Aug 11, 2026

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (4)
crates/openai-frontend/src/router.rs (3)

1145-1165: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider an outcome enum instead of &'static str comparison.

drop_outcome returns a string, and finish_drop compares it to the literal "success". finish_natural repeats the same literal. If a future change renames an outcome, the compiler will not catch the stale comparison, and completion logging will silently stop.

An enum with a as_str() method keeps the log field values and makes the match exhaustive.

🤖 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 `@crates/openai-frontend/src/router.rs` around lines 1145 - 1165, Introduce an
outcome enum for the values produced by drop_outcome, with an as_str() method
preserving the existing log strings. Update drop_outcome, finish_drop, and
finish_natural to use the enum, replacing literal success comparisons with
exhaustive enum matching while passing as_str() to observation.finish.

211-217: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the streamed lifecycle scaffolding into one helper.

chat_completions, responses, and completions each create the same four cells (stream_error, stream_completed, response_completed_logged, stream_usage), then build StreamCompletion and StreamLifecycle with identical wiring. The only per-route differences are the operation label and the event stream.

A single constructor reduces the risk that one route later forgets a cell. This is a follow-up refactor; the current wiring is correct.

♻️ Sketch of a shared scaffolding type
struct StreamTelemetry {
    stream_error: Arc<AtomicBool>,
    stream_completed: Arc<AtomicBool>,
    response_completed_logged: Arc<AtomicBool>,
    stream_usage: Arc<Mutex<Option<Usage>>>,
}

impl StreamTelemetry {
    fn new() -> Self { /* all four cells */ }

    fn lifecycle(
        &self,
        observation: RequestObservation,
        context: OpenAiRequestContext,
        operation: &'static str,
    ) -> StreamLifecycle {
        StreamLifecycle::new(
            observation,
            self.stream_error.clone(),
            self.stream_completed.clone(),
        )
        .with_completion(StreamCompletion::new(
            context,
            operation,
            self.stream_usage.clone(),
            self.response_completed_logged.clone(),
        ))
    }
}

Also applies to: 269-280

🤖 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 `@crates/openai-frontend/src/router.rs` around lines 211 - 217, Extract the
repeated streamed lifecycle setup from chat_completions, responses, and
completions into a shared StreamTelemetry type with a constructor for the four
telemetry cells and a lifecycle method that builds the existing
StreamLifecycle/StreamCompletion wiring. Update each route to use this helper,
passing only its operation label and event stream while preserving current
behavior.

1384-1524: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an assertion on request_finished for the streamed lifecycle.

The new tests count response_completed events only. No test asserts that a streamed request emits exactly one request_finished event with the expected outcome. The PR objectives name this assertion as the requested coverage for drop classification.

EventCapture already captures the outcome field, so the check is small.

💚 Sketch of the missing assertion
#[test]
fn streamed_lifecycle_emits_one_classified_request_finished() {
    let events = Arc::new(Mutex::new(Vec::new()));
    let mut lifecycle = StreamLifecycle::new(
        RequestObservation::new("request-lifecycle".to_owned(), "chat_completion"),
        Arc::new(AtomicBool::new(false)),
        Arc::new(AtomicBool::new(true)),
    );

    tracing::subscriber::with_default(
        EventCapture { events: events.clone() },
        || {
            lifecycle.finish_natural();
            lifecycle.finish_drop(false);
        },
    );

    let events = events.lock().expect("event capture lock poisoned");
    let finished: Vec<_> = events
        .iter()
        .filter(|event| event.get("event").is_some_and(|value| value == "request_finished"))
        .collect();
    assert_eq!(finished.len(), 1);
    assert_eq!(finished[0]["outcome"], "success");
}
🤖 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 `@crates/openai-frontend/src/router.rs` around lines 1384 - 1524, Extend the
streamed lifecycle coverage in the existing lifecycle tests by asserting that
calling finish_natural followed by finish_drop emits exactly one
request_finished event. Verify its outcome is success, using EventCapture’s
captured outcome field, while preserving the existing response_completed
assertions.
crates/skippy-server/src/frontend/backend.rs (1)

865-945: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test that two concurrent turns of one trusted session serialize.

The current tests cover lease counting, entry replacement, untrusted bypass, and spoofed metadata. They do not cover the main behavior of the change: acquire_generation_admission must let only one turn of a trusted session run at a time, and it must respect the shared deadline and cancellation. Add an async test that acquires the session permit, then confirms a second acquire_generation_admission for the same agent_session_id waits, and that cancellation returns request cancelled.

🤖 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 `@crates/skippy-server/src/frontend/backend.rs` around lines 865 - 945, Add an
async test alongside the existing session-registry tests that creates trusted
generation IDs for the same agent_session_id, acquires the first permit through
acquire_generation_admission, then starts a second acquisition and verifies it
remains blocked while the first permit is held. Cancel the second request using
the shared cancellation/deadline mechanism, assert it returns the "request
cancelled" error, and release the first permit to cover serialized
trusted-session turns.
🤖 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.

Inline comments:
In `@crates/skippy-server/src/frontend/backend.rs`:
- Around line 531-546: Move the reserve_generation_queue call in the
session-contention path so it occurs only after session.acquire_until
successfully returns and session_permit is restored. Do not hold a global
queue_reservation while waiting for the session permit; preserve the existing
cancellation, deadline, and generation-permit acquisition behavior around the
updated ordering.

---

Nitpick comments:
In `@crates/openai-frontend/src/router.rs`:
- Around line 1145-1165: Introduce an outcome enum for the values produced by
drop_outcome, with an as_str() method preserving the existing log strings.
Update drop_outcome, finish_drop, and finish_natural to use the enum, replacing
literal success comparisons with exhaustive enum matching while passing as_str()
to observation.finish.
- Around line 211-217: Extract the repeated streamed lifecycle setup from
chat_completions, responses, and completions into a shared StreamTelemetry type
with a constructor for the four telemetry cells and a lifecycle method that
builds the existing StreamLifecycle/StreamCompletion wiring. Update each route
to use this helper, passing only its operation label and event stream while
preserving current behavior.
- Around line 1384-1524: Extend the streamed lifecycle coverage in the existing
lifecycle tests by asserting that calling finish_natural followed by finish_drop
emits exactly one request_finished event. Verify its outcome is success, using
EventCapture’s captured outcome field, while preserving the existing
response_completed assertions.

In `@crates/skippy-server/src/frontend/backend.rs`:
- Around line 865-945: Add an async test alongside the existing session-registry
tests that creates trusted generation IDs for the same agent_session_id,
acquires the first permit through acquire_generation_admission, then starts a
second acquisition and verifies it remains blocked while the first permit is
held. Cancel the second request using the shared cancellation/deadline
mechanism, assert it returns the "request cancelled" error, and release the
first permit to cover serialized trusted-session turns.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6c40b567-45df-49d1-8e3c-62f98580baff

📥 Commits

Reviewing files that changed from the base of the PR and between bf94356 and 9d6ffa2.

📒 Files selected for processing (11)
  • crates/openai-frontend/src/backend.rs
  • crates/openai-frontend/src/common.rs
  • crates/openai-frontend/src/router.rs
  • crates/skippy-server/src/frontend/backend.rs
  • crates/skippy-server/src/frontend/generation/cache_hints.rs
  • crates/skippy-server/src/frontend/generation/queue.rs
  • crates/skippy-server/src/frontend/generation/server.rs
  • crates/skippy-server/src/frontend/generation/types.rs
  • crates/skippy-server/src/frontend/local_generation/tests.rs
  • crates/skippy-server/src/frontend/tests/generation.rs
  • crates/skippy-server/src/frontend/tests/multimodal.rs

Comment on lines +531 to +546
if let Some(session) = session_permit.as_mut() {
if cancellation.is_cancelled() {
return Err(request_cancelled_error());
}
if !session.try_acquire()? {
queue_reservation = Some(
reserve_generation_queue(
self.generation_queue_depth.clone(),
self.generation_queue_limit,
)
.ok_or_else(generation_queue_full_error)?,
);
let session = session_permit.take().expect("session permit was present");
session_permit = Some(session.acquire_until(deadline, cancellation).await?);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

A queued turn of one trusted session can consume the whole global queue and reject unrelated requests.

When the session lock is contended, this code reserves a global queue slot before it waits for the session permit, and it holds that reservation through the session wait and the following global-permit wait. generation_queue_limit is set to generation_concurrency in both serve_openai and embedded_openai_backend, so the limit is small.

Sequence with generation_concurrency = 1:

  1. Turn A of session S takes the session permit and the single generation permit.
  2. Turn B of session S finds the session busy, reserves the only queue slot, and waits.
  3. A request for an unrelated session calls acquire_generation_permit_until, finds no permits, calls reserve_generation_queue, and gets None.
  4. The unrelated request fails immediately with generation_queue_full_error (429), although it is not blocked by session S.

The window lasts up to GENERATION_ADMISSION_TIMEOUT. Reserve global queue capacity only after the session permit is acquired, or count session waiters in a separate counter.

🛠️ Proposed change: reserve global queue capacity after the session permit is held
         if let Some(session) = session_permit.as_mut() {
             if cancellation.is_cancelled() {
                 return Err(request_cancelled_error());
             }
             if !session.try_acquire()? {
-                queue_reservation = Some(
-                    reserve_generation_queue(
-                        self.generation_queue_depth.clone(),
-                        self.generation_queue_limit,
-                    )
-                    .ok_or_else(generation_queue_full_error)?,
-                );
                 let session = session_permit.take().expect("session permit was present");
                 session_permit = Some(session.acquire_until(deadline, cancellation).await?);
+                queue_reservation = Some(
+                    reserve_generation_queue(
+                        self.generation_queue_depth.clone(),
+                        self.generation_queue_limit,
+                    )
+                    .ok_or_else(generation_queue_full_error)?,
+                );
             }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let Some(session) = session_permit.as_mut() {
if cancellation.is_cancelled() {
return Err(request_cancelled_error());
}
if !session.try_acquire()? {
queue_reservation = Some(
reserve_generation_queue(
self.generation_queue_depth.clone(),
self.generation_queue_limit,
)
.ok_or_else(generation_queue_full_error)?,
);
let session = session_permit.take().expect("session permit was present");
session_permit = Some(session.acquire_until(deadline, cancellation).await?);
}
}
if let Some(session) = session_permit.as_mut() {
if cancellation.is_cancelled() {
return Err(request_cancelled_error());
}
if !session.try_acquire()? {
let session = session_permit.take().expect("session permit was present");
session_permit = Some(session.acquire_until(deadline, cancellation).await?);
queue_reservation = Some(
reserve_generation_queue(
self.generation_queue_depth.clone(),
self.generation_queue_limit,
)
.ok_or_else(generation_queue_full_error)?,
);
}
}
🤖 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 `@crates/skippy-server/src/frontend/backend.rs` around lines 531 - 546, Move
the reserve_generation_queue call in the session-contention path so it occurs
only after session.acquire_until successfully returns and session_permit is
restored. Do not hold a global queue_reservation while waiting for the session
permit; preserve the existing cancellation, deadline, and generation-permit
acquisition behavior around the updated ordering.

@ndizazzo

Copy link
Copy Markdown
Collaborator

Superseded by #1258

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blocker blocking other PRs Do not merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants