Skip to content

feat(protocols): implement P2 top-level ResponsesRequest fields - #1278

Merged
slin1237 merged 3 commits into
mainfrom
feat/audit-p2-top-level
Apr 21, 2026
Merged

slin1237 merged 3 commits into
mainfrom
feat/audit-p2-top-level

Conversation

@slin1237

@slin1237 slin1237 commented Apr 21, 2026 •

Copy link
Copy Markdown
Member

Summary

Implements audit task P2: add the 6 missing top-level fields to ResponsesRequest (prompt, prompt_cache_key, prompt_cache_retention, safety_identifier, stream_options, context_management) and the supporting PromptCacheRetention / ContextManagementEntry / ContextManagementType::Compaction types, plus extending shared StreamOptions with include_obfuscation. Unblocks audit tasks P6 (conversation type) and I3 (compaction item).

What changed

Commits on branch (main..HEAD):

  • 98245338 — feat(protocols): implement P2 top-level fields (substance)
  • 1730e5c6 — refactor(protocols): drop speculative ConversationRef from P2 (cycle 2) (cycle-1 REJECT remediation — removed the ConversationRef dead-pub type per §7)

Diff totals (vs main): 6 files, +312 / -3.

  • crates/protocols/src/responses.rs (+180 / -3): 6 new top-level ResponsesRequest fields threaded into the ResponsesRequest struct + updated Default impl + 3 serde round-trip tests covering (a) spec-full-field positive roundtrip with 24h + compaction + include_obfuscation=false, (b) alternate enum variant in-memory, (c) skip_serializing_if absent-field verification.
  • crates/protocols/src/common.rs (+44 / new types): PromptCacheRetention enum (InMemory / Duration24h with hyphen serde renames in-memory / 24h); ContextManagementEntry struct + ContextManagementType::Compaction closed enum (no #[serde(other)], no #[serde(untagged)]; unknown variants serde-fail, aligned with P5's future silent-swallow removal); existing StreamOptions extended with include_obfuscation: Option<bool> + derived Default.
  • model_gateway/src/routers/grpc/regular/responses/common.rs (+10): build_next_request threads the 6 new fields through tool-loop continuations (compile-forced and semantically correct — multi-turn continuations must not silently drop the caller's prompt template / cache key / safety ID).
  • model_gateway/src/routers/grpc/regular/responses/conversions.rs (+1): ..StreamOptions::default() after StreamOptions gained include_obfuscation (compile-forced).
  • model_gateway/tests/api/responses_api_test.rs (+78 compile-forced): 13 struct-literal test fixtures extended with None for the 6 new fields (verified compile-forced by revert-test — removing one prompt: None yields error[E0063]: missing field 'prompt' in initializer of ResponsesRequest).
  • model_gateway/tests/spec/chat_completion.rs (+2): ..StreamOptions::default() sites (compile-forced; shared StreamOptions type).

Why

OpenAI Responses API spec defines 6 additional top-level fields on ResponsesRequest (see .claude/_audit/openai-responses-api-spec.md request-body section). smg's previous schema lacked all of them; spec-valid clients using prompt templates, cache-key hinting, safety identifiers, obfuscation options, or context-management directives hit deserialization failures. Threading through the tool-loop continuation in build_next_request ensures multi-turn request state carries through correctly.

The closed ContextManagementType::Compaction enum (no #[serde(other)], no #[serde(untagged)]) aligns with P5's fail-fast direction — unknown variants surface at deserialize time rather than silent-swallow.

Verification

  • cargo check -p openai-protocol clean (isolated CARGO_TARGET_DIR=/tmp/p2-c2-lead-target)
  • cargo check --workspace --tests clean
  • cargo test -p openai-protocol → 85/0 (58 unit + 8 background_mode_protocol + 18 skills_protocol + 1 doc)
  • cargo clippy -p openai-protocol --tests -- -D warnings clean
  • cargo +nightly fmt --all -- --check silent
  • Tech Lead cycle-1 hand-built 19/19 spec fixtures (each new field + PromptCacheRetention both variants byte-identical with hyphen forms + ContextManagementType::Compaction closed-enum rejects unknown + StreamOptions.include_obfuscation optional absent-vs-present)
  • Tech Lead cycle-2 spot-checked cycle-1 fixtures still pass after ConversationRef deletion
  • 13 struct-literal test-fixture extensions verified compile-forced (revert one → error[E0063])
  • ContextManagementType closed-enum claim verified: no #[serde(other)], no #[serde(untagged)], rename_all = "snake_case" only (value renaming, not a catch-all)
  • StreamOptions reuse safety: Go SDK (bindings/golang/client.go:203) and Python SDK (clients/python/smg_client/types/__init__.py) mirrors only reference the type; include_obfuscation is Option<bool> with skip_serializing_if — absent on wire for non-setters, no chat-path breakage
  • P6 boundary preserved: ResponsesRequest::conversation: Option<String> intact at responses.rs:703 (field type and identity unchanged — P6 owns the migration)
  • Codex review: unavailable (harness skill permission; Lead proceeded solo per playbook §8 fallback after own 19 fixtures passed both cycles)

Blast radius

6 files. Protocol schema (2) + router propagation (2) + test-fixture compile-forced extensions (2). Matches audit Blast Radius; no forbidden files touched.

Out of scope

  • ResponsesRequest::conversation type (currently Option<String>) — P6 owns the migration to a string-or-object ConversationRef union.
  • Runtime wiring of the new fields upstream (prompt-template resolution into SGLang/vLLM gRPC payloads, cache-key pass-through, stream-options SSE frame assembly) — router-side plumbing out of P2's protocols-crate scope; threaded through tool-loop continuation in build_next_request (needed for correctness) but not upstream.

Follow-ups noted (non-blocking)

  • ConversationRef dead-pub landed in cycle 1, removed in cycle 2 per Lead cycle-1 REJECT. P6 will re-introduce when it consumes the type.

Refs: audit task P2 · .claude/_audit/responses-api-gap-audit.md

Summary by CodeRabbit

  • New Features

    • Prompt caching with configurable retention ("in-memory" or "24h")
    • Context management support with compaction and optional compact-threshold
    • Enhanced streaming options: defaultable settings and optional obfuscation toggle
    • Multi-turn consistency: prompt, cache key/retention, safety label, streaming, and context settings persist across follow-ups
  • Tests

    • Added/updated tests for JSON round-trips, enum wire values, and omission of unset optional fields

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai

coderabbitai Bot commented Apr 21, 2026 •

Copy link
Copy Markdown

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
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Added optional Responses request fields (prompt, prompt_cache_key, prompt_cache_retention, safety_identifier, stream_options, context_management), new protocol types and StreamOptions defaulting, propagated these fields through request-building and Responses→Chat conversion logic, and updated tests for serialization and streaming behavior.

Changes

Cohort / File(s) Summary
Protocol Type Definitions
crates/protocols/src/common.rs
Derived Default for StreamOptions; added include_obfuscation: Option<bool> (skipped when None). Added PromptCacheRetention ("in-memory", "24h"), ContextManagementEntry (type, optional compact_threshold), and ContextManagementType (compaction).
Responses Request & Tests
crates/protocols/src/responses.rs
Extended ResponsesRequest with six optional fields: prompt, prompt_cache_key, prompt_cache_retention, safety_identifier, stream_options, context_management. Updated Default and added round-trip / omission unit tests.
gRPC Router — multi-turn propagation
model_gateway/src/routers/grpc/regular/responses/common.rs
build_next_request now copies the new top-level fields into follow-up ResponsesRequests so per-request knobs persist across continuations.
Responses → Chat conversion logic
model_gateway/src/routers/grpc/regular/responses/conversions.rs
When streaming, clones caller stream_options (or uses StreamOptions::default()), sets include_usage = Some(true) only if caller omitted it, and preserves caller include_obfuscation; non-streaming requests drop stream_options. Added unit tests.
Test updates (API & spec)
model_gateway/tests/api/responses_api_test.rs, model_gateway/tests/spec/chat_completion.rs
Populated new optional fields (often None) across many test literals; updated stream-option tests to use ..StreamOptions::default() with explicit include_usage: Some(true).

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Router as ModelGateway Router
    participant Protocols as Protocol Types
    participant Upstream as Responses API
    rect rgba(200,230,255,0.5)
    Client->>Router: send ResponsesRequest (may include prompt, prompt_cache_key, prompt_cache_retention, safety_identifier, stream_options, context_management)
    Router->>Protocols: validate/serialize request fields
    Protocols-->>Router: serialized ResponsesRequest
    Router->>Upstream: forward ResponsesRequest
    Upstream-->>Router: stream/response events
    Router->>Client: relay responses (including streaming)
    Router->>Router: build_next_request(...) preserves prompt/cache/stream/context for multi-turn
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • CatherineSue
  • key4ng

Poem

🐰 Hopping through structs with cheer,
New fields nestle, tidy and clear.
Cache and context find their place,
Streams keep state at steady pace,
A little hop — requests embrace!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: implementing six top-level optional fields (prompt, prompt_cache_key, prompt_cache_retention, safety_identifier, stream_options, context_management) in ResponsesRequest as part of P2 protocol support.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/audit-p2-top-level

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

@github-actions github-actions Bot added grpc gRPC client and router changes tests Test changes protocols Protocols crate changes model-gateway Model gateway crate changes labels Apr 21, 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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@model_gateway/tests/api/responses_api_test.rs`:
- Around line 107-112: The tests repeatedly construct ResponsesRequest with many
optional fields set to None; create a shared helper like fn
base_responses_request() -> ResponsesRequest that returns a ResponsesRequest
with common defaults (prompt: None, prompt_cache_key: None,
prompt_cache_retention: None, safety_identifier: None, stream_options: None,
context_management: None, etc.), then update only the fields each test needs
(using struct update syntax or a small builder) in the tests that currently
construct ResponsesRequest (references: ResponsesRequest occurrences in these
test sections). Replace the repeated None-initializations in each test with
calls to base_responses_request() and per-test overrides to reduce boilerplate
and future churn.
🪄 Autofix (Beta)

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: ASSERTIVE

Plan: Pro

Run ID: 39c58428-96fc-49b8-8d41-1797548134ab

📥 Commits

Reviewing files that changed from the base of the PR and between 9b4477b and 1730e5c.

📒 Files selected for processing (6)
  • crates/protocols/src/common.rs
  • crates/protocols/src/responses.rs
  • model_gateway/src/routers/grpc/regular/responses/common.rs
  • model_gateway/src/routers/grpc/regular/responses/conversions.rs
  • model_gateway/tests/api/responses_api_test.rs
  • model_gateway/tests/spec/chat_completion.rs

Comment on lines +107 to +112
prompt: None,
prompt_cache_key: None,
prompt_cache_retention: None,
safety_identifier: None,
stream_options: None,
context_management: None,

@coderabbitai coderabbitai Bot Apr 21, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider a shared test request builder to remove repeated None boilerplate.

The new optional fields are correctly set, but this repetition will make future request-shape updates noisy. A small helper that returns a base ResponsesRequest and per-test overrides would reduce churn.

Also applies to: 305-310, 435-440, 566-571, 621-626, 743-748, 884-889, 933-938, 1080-1085, 1194-1199, 1353-1358, 1539-1544, 1823-1828

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model_gateway/tests/api/responses_api_test.rs` around lines 107 - 112, The
tests repeatedly construct ResponsesRequest with many optional fields set to
None; create a shared helper like fn base_responses_request() ->
ResponsesRequest that returns a ResponsesRequest with common defaults (prompt:
None, prompt_cache_key: None, prompt_cache_retention: None, safety_identifier:
None, stream_options: None, context_management: None, etc.), then update only
the fields each test needs (using struct update syntax or a small builder) in
the tests that currently construct ResponsesRequest (references:
ResponsesRequest occurrences in these test sections). Replace the repeated
None-initializations in each test with calls to base_responses_request() and
per-test overrides to reduce boilerplate and future churn.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks. Agreed this is repetition, but a shared test-fixture builder is genuinely out of P2's scope — P2's charter is protocol schema additions; test-infrastructure refactoring across 13 callsites would balloon the blast radius. Tracking as a follow-up for a dedicated refactor(tests) PR. Leaving as-is.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@slin1237 Understood — keeping P2 focused on protocol schema additions is a sound call. A dedicated refactor(tests) PR targeting the shared fixture builder across those 13 callsites makes more sense than expanding the blast radius here. Feel free to track it as a follow-up when the time is right.


🧠 Learnings used
Learnt from: key4ng
Repo: lightseekorg/smg PR: 1106
File: e2e_test/responses/test_sampling_params.py:20-47
Timestamp: 2026-04-15T04:44:18.006Z
Learning: In repo lightseekorg/smg, `ResponsesResponse` proto only echoes back `temperature`, `top_p`, and `max_output_tokens`. Extension sampling fields (`top_k`, `min_p`, `frequency_penalty`, `presence_penalty`, `repetition_penalty`) are **not** part of the response schema and are therefore not present in `resp.*` in E2E tests (e.g., `e2e_test/responses/test_sampling_params.py`). Do not flag the absence of assertions for these fields in E2E response-sampling tests; builder pass-through for these fields is validated by unit tests in `crates/grpc_client/src/` instead.

Learnt from: zhaowenzi
Repo: lightseekorg/smg PR: 807
File: model_gateway/src/routers/openai/responses/streaming.rs:821-855
Timestamp: 2026-03-18T21:57:03.433Z
Learning: In repo lightseekorg/smg, file model_gateway/src/routers/openai/responses/streaming.rs: The early `return` statements inside `handle_streaming_with_tool_interception` (on `tx` send failures in `forward_streaming_event`, `send_mcp_list_tools_events`, and the `is_in_progress`/`mcp_list_tools_sent` branches) are pre-existing behavior that predates PR `#807`. They cause the persistence phase (final response / conversation-backed storage writes at the end of the tool loop) to be skipped when the client disconnects mid-stream with `store=true` or a conversation-backed request. This is a known pre-existing gap in the MCP streaming path, not a regression introduced by the storage context header changes in PR `#807`.

Learnt from: zhaowenzi
Repo: lightseekorg/smg PR: 1163
File: e2e_test/responses/test_tools_call.py:1126-1133
Timestamp: 2026-04-16T17:20:26.745Z
Learning: In repo lightseekorg/smg, `TestToolChoiceLocal.test_previous_response_id_mcp_binding_behavior_streaming` in `e2e_test/responses/test_tools_call.py` unconditionally calls `pytest.skip(...)` because regular gRPC MCP streaming responses are not persisted for `previous_response_id` resume (the response ID is emitted but the final response is not stored). This skip is intentional and kept broad for consistency; the persistence gap will be addressed in a dedicated follow-up PR. Do not flag this skip as overly broad or suggest narrowing it to seed from non-streaming responses.

Learnt from: vschandramourya
Repo: lightseekorg/smg PR: 978
File: model_gateway/src/routers/grpc/regular/streaming.rs:2362-2363
Timestamp: 2026-03-31T02:28:31.317Z
Learning: In repo lightseekorg/smg, file model_gateway/src/routers/grpc/regular/streaming.rs: When a stop sequence is detected mid-stream in `process_completion_streaming_chunks` (and equivalently in `process_streaming_chunks` for chat), the synthetic final-chunk send after a local stop match intentionally uses `let _ = tx.send(...)` (best-effort). The loop must fall through to the `ProtoResponseVariant::Complete` arm to record streaming metrics and call `mark_completed()`. Propagating a channel-closed error at this point would skip metrics recording and leave the request in a dangling state. The backend stream is already locally exhausted at the stop-match site (no wasted capacity). Do not flag this as a silent error or suggest converting it to a propagated `?` error.

Learnt from: slin1237
Repo: lightseekorg/smg PR: 1244
File: crates/protocols/src/builders/responses/response.rs:126-143
Timestamp: 2026-04-20T16:55:01.974Z
Learning: In repo lightseekorg/smg, `completed_at: Option<i64>` on `ResponsesResponse` (added in PR `#1244`, `crates/protocols/src/builders/responses/response.rs`) is intentionally NOT populated by any terminal-response builder in PR `#1244`. Wiring the gRPC regular (`conversions.rs`, `streaming.rs`) and Harmony (`processor.rs`, `non_streaming.rs`) terminal builders to call `.completed_at(...)` before `.build()` is explicitly deferred to BGM-PR-07 (`Worker Execution Core + Retry + Cancellation`) per `2026-04-17-background-mode-task-breakdown.md:170-193`. Do not re-flag `completed_at: None` in terminal responses as a missing population until BGM-PR-07 lands.

Learnt from: slin1237
Repo: lightseekorg/smg PR: 489
File: model_gateway/src/routers/grpc/client.rs:206-211
Timestamp: 2026-02-21T02:31:17.841Z
Learning: Repo: lightseekorg/smg
File: model_gateway/src/routers/grpc/client.rs
Context: GrpcClient::embed match arm
Learning: The catch-all panic in GrpcClient::embed for mismatched client/request types or unsupported embedding backends is intentional to catch invariant violations and unsupported configurations at development time. Converting this path to a returned error is out of scope for PR `#489`.

Learnt from: slin1237
Repo: lightseekorg/smg PR: 489
File: model_gateway/src/routers/grpc/client.rs:189-194
Timestamp: 2026-02-21T02:32:13.396Z
Learning: Repo: lightseekorg/smg PR: 489
File: model_gateway/src/routers/grpc/client.rs
Context: GrpcClient::generate fallback match arm
Learning: The catch-all panic for mismatched client/request types in GrpcClient::generate is intentional to enforce pipeline invariants; do not convert this to a returned error in PR `#489` or similar lint-only changes.

Learnt from: vschandramourya
Repo: lightseekorg/smg PR: 953
File: model_gateway/src/routers/grpc/regular/processor.rs:765-780
Timestamp: 2026-03-27T23:46:40.172Z
Learning: In repo lightseekorg/smg, file model_gateway/src/routers/grpc/regular/processor.rs: The finish_reason parsing in `process_non_streaming_completion_response` is intentionally NOT extracted to `utils::parse_finish_reason`. The completions endpoint uses OpenAI-style `Option<String>` from `openai_protocol::completion::CompletionChoice`, while `utils::parse_finish_reason` maps to the typed generate-endpoint format. These are different API contracts; do not flag the inline finish_reason parsing block in the completions processor as duplicate code or request extraction into the shared utility.

Learnt from: vschandramourya
Repo: lightseekorg/smg PR: 978
File: model_gateway/src/routers/grpc/regular/streaming.rs:2275-2276
Timestamp: 2026-03-31T03:43:30.981Z
Learning: In repo lightseekorg/smg, file model_gateway/src/routers/grpc/regular/streaming.rs: `logprobs: None` in `CompletionStreamChoice` within `process_completion_streaming_chunks` is intentional. Per-token logprob streaming is deferred across all streaming endpoints (completions, generate) because it requires backend-side plumbing not yet available. The same `// TODO: wire per-token logprob streaming` deferral exists in `process_generate_streaming` (around line 702). Non-streaming completions has the same TODO. Do not flag `logprobs: None` or the TODO comment in the completions or generate streaming paths as a missing feature or silent data loss until a dedicated follow-up PR wires backend per-token logprob support.

Learnt from: pallasathena92
Repo: lightseekorg/smg PR: 406
File: model_gateway/src/routers/openai/realtime/rest.rs:173-189
Timestamp: 2026-03-04T02:13:55.479Z
Learning: In repo lightseekorg/smg, file model_gateway/src/routers/openai/realtime/rest.rs: The proxy_response function intentionally forwards only the Content-Type header from upstream responses. For the realtime REST token-generation endpoints (client_secrets, sessions, transcription_sessions), the response is a simple JSON body (ephemeral token), so Content-Type is sufficient. Upstream rate-limit headers (x-ratelimit-*, retry-after) are not forwarded because the gateway has its own rate limiting. x-request-id forwarding is a deferred nice-to-have, not a correctness requirement.

Learnt from: vschandramourya
Repo: lightseekorg/smg PR: 915
File: model_gateway/src/routers/grpc/client.rs:387-423
Timestamp: 2026-03-26T17:06:14.307Z
Learning: In repo lightseekorg/smg, in `model_gateway/src/routers/grpc/client.rs` and the corresponding backend builders (`crates/grpc_client/src/sglang_scheduler.rs`, `vllm_engine.rs`, `trtllm_service.rs`): The per-backend divergence in handling `CompletionRequest.max_tokens == None` is intentional. SGLang and vLLM pass `None` through to their proto builders, while TRT-LLM falls back to `16`. This matches the pre-existing per-backend pattern used in the chat/messages request builders. Do not flag this divergence as a bug or request normalization at the `build_completion_request` dispatcher layer in `client.rs`.

Learnt from: XinyueZhang369
Repo: lightseekorg/smg PR: 723
File: model_gateway/tests/api/interactions_api_test.rs:316-346
Timestamp: 2026-03-11T05:32:45.536Z
Learning: In repo lightseekorg/smg, `test_interactions_multiple_workers` in `model_gateway/tests/api/interactions_api_test.rs` intentionally only verifies that requests succeed with multiple Gemini backends (connectivity), not load distribution. Load distribution across workers is covered separately in `tests/routing/load_balancing_test.rs`. Do not flag the absence of per-worker request count assertions in this test as a gap — distribution verification is deferred to a follow-up PR once the Gemini router implementation matures.

Learnt from: vschandramourya
Repo: lightseekorg/smg PR: 964
File: model_gateway/src/routers/grpc/pd_router.rs:233-240
Timestamp: 2026-03-28T04:56:07.632Z
Learning: In repo lightseekorg/smg, `GrpcPDRouter` (model_gateway/src/routers/grpc/pd_router.rs) intentionally passes `&self.retry_config` (the router-level default) to `RetryExecutor::execute_response_with_retry` for ALL endpoints (generate, chat, messages, completion). Per-model retry overrides via `WorkerRegistry::get_retry_config` are NOT applied in the PD router — that pattern is used only in `GrpcRouter` (regular mode). Adding per-model retry support to individual PD endpoints in isolation would be inconsistent; any such change must cover all PD endpoints in a dedicated follow-up PR. Do not flag the missing per-model retry lookup in any single PD router endpoint as a bug.

Learnt from: zhaowenzi
Repo: lightseekorg/smg PR: 1163
File: model_gateway/src/routers/common/mcp_utils.rs:56-67
Timestamp: 2026-04-17T18:06:31.006Z
Learning: In repo lightseekorg/smg, `inject_mcp_output_items` in `model_gateway/src/routers/common/mcp_utils.rs` (used by gRPC regular and Harmony response paths) does NOT need to filter `existing` output items (from `std::mem::take(output)`) with `is_client_visible_output_item` before appending them back. Unlike the OpenAI path (`inject_client_visible_mcp_output_items` in `crates/mcp/src/core/session.rs`), the gRPC response paths do not pre-populate `response.output` with internal `FunctionToolCall` entries before this helper is called, so there is no internal-item leakage risk. The e2e assertions in `e2e_test/responses/test_tools_call.py` confirm this. Do not apply the OpenAI-path filtering requirement to `inject_mcp_output_items` in the gRPC paths.

Learnt from: slin1237
Repo: lightseekorg/smg PR: 489
File: multimodal/tests/vision_golden_tests.rs:402-404
Timestamp: 2026-02-21T02:39:23.481Z
Learning: Repo lightseekorg/smg — For clippy-only/lint-enforcement PRs (e.g., PR `#489`), do not replace unwrap() with expect() across tests/benches when a crate-level `#![expect(clippy::unwrap_used)]` is present. Such per-call swaps are treated as out-of-scope stylistic changes. Example: multimodal/tests/vision_golden_tests.rs.

Learnt from: TingtingZhou7
Repo: lightseekorg/smg PR: 1057
File: model_gateway/src/routers/openai/mcp/tool_loop.rs:856-885
Timestamp: 2026-04-08T00:08:05.944Z
Learning: In repo lightseekorg/smg, `sanitize_builtin_tool_arguments` in `model_gateway/src/routers/openai/mcp/tool_loop.rs` intentionally drops image-generation options (size, quality, background, output_format, compression) when handling `ResponseFormat::ImageGenerationCall`, keeping only `model` (hardcoded `IMAGE_MODEL`) and `revised_prompt`. This is a deliberate scoped decision for the initial image-generation tool integration; per-option overrides/defaults are planned for a follow-up PR. Do not flag the truncation as a bug or request preservation of extra fields.

Learnt from: CatherineSue
Repo: lightseekorg/smg PR: 936
File: model_gateway/src/routers/gemini/router.rs:82-90
Timestamp: 2026-03-30T18:52:19.689Z
Learning: In repo lightseekorg/smg, the Gemini router's (`model_gateway/src/routers/gemini/router.rs`) use of `model_id` for per-model retry config lookup in `route_interactions` has a pre-existing limitation: when the request carries only an agent identifier, `get_retry_config(agent_id)` finds no match because agent→model resolution happens later inside `driver::execute` (in `worker_selection.rs`). This same limitation affects policy lookup and worker selection in the Gemini router. It is NOT introduced by PR `#936` and is a known accepted gap. The Gemini router is not actively used currently. Do not flag this as a bug introduced by per-model retry config changes to the Gemini router.

Learnt from: zhaowenzi
Repo: lightseekorg/smg PR: 1250
File: model_gateway/src/routers/openai/responses/history.rs:343-398
Timestamp: 2026-04-20T09:00:17.756Z
Learning: In repo lightseekorg/smg, `mcp_call_output_to_upstream_items` in `model_gateway/src/routers/openai/responses/history.rs` intentionally uses a redundant `item.get("output")` double-lookup and does not preserve the `error` field from stored `mcp_call` items during upstream replay (producing `"null"` instead). This is a deliberate scoping decision in PR `#1250`: the exercised paths always carry an `output` value, and widening replay behavior to handle missing-output/error cases is deferred to a follow-up PR. Do not re-flag the redundant lookup or the silent error discard as blocking issues until that follow-up work is done.

Learnt from: zhoug9127
Repo: lightseekorg/smg PR: 1061
File: model_gateway/src/routers/openai/mcp/tool_loop.rs:846-850
Timestamp: 2026-04-08T16:26:08.331Z
Learning: In repo lightseekorg/smg, `build_transformed_mcp_call_item` in `model_gateway/src/routers/openai/mcp/tool_loop.rs` intentionally omits the `server_label` field from transformed output items when the `ResponseFormat` is a builtin type (`WebSearchCall`, `CodeInterpreterCall`, `FileSearchCall`). This invariant is enforced by the test `build_transformed_mcp_call_item_does_not_add_server_label_for_builtin_formats`. As a result, the label-based filter `is_internal_mcp_response_item` (which checks `server_label`) will never accidentally hide builtin-routed call items — it is safe to rely on the absence of `server_label` for builtin items without adding an explicit type-gate. Do not flag `is_internal_mcp_response_item` as potentially hiding builtin output.

Learnt from: XinyueZhang369
Repo: lightseekorg/smg PR: 723
File: model_gateway/tests/api/interactions_api_test.rs:251-282
Timestamp: 2026-03-11T23:26:31.100Z
Learning: In repo lightseekorg/smg, the Gemini-specific header path (apply_provider_headers emitting x-goog-api-key for googleapis.com URLs, and extract_gemini_auth_header) is intentionally not covered by the mock-server integration tests in model_gateway/tests/api/interactions_api_test.rs (PR `#723`). Coverage against a real Gemini backend URL will be added in a follow-up e2e test PR. Do not flag the absence of this test coverage as a gap in PR `#723` or related PRs.

Learnt from: pallasathena92
Repo: lightseekorg/smg PR: 687
File: model_gateway/src/routers/openai/realtime/webrtc.rs:238-289
Timestamp: 2026-03-11T01:29:56.655Z
Learning: In repo lightseekorg/smg, file model_gateway/src/routers/openai/realtime/webrtc.rs: `Metrics::record_router_request` is already emitted in router.rs (around line 1152) before `handle_realtime_webrtc` is called, and `Metrics::record_router_error` is emitted inside `handle_realtime_webrtc` for the no-workers case. The missing instrumentation is success/duration recording after `setup_and_spawn_bridge` returns — this is a metrics improvement deferred to a follow-up PR, not a correctness gap. Do not flag missing success/duration metrics as a blocking issue for PR `#687`.

Learnt from: CatherineSue
Repo: lightseekorg/smg PR: 1031
File: model_gateway/src/routers/grpc/regular/streaming.rs:1579-1600
Timestamp: 2026-04-03T05:31:33.707Z
Learning: In repo lightseekorg/smg, `model_gateway/src/routers/grpc/regular/streaming.rs` (`process_messages_streaming_chunks`): The reasoning parser is gated on explicit user opt-in (`ThinkingConfig::Enabled`) via `separate_reasoning = matches!(&original_request.thinking, Some(ThinkingConfig::Enabled { .. }))`, NOT on `thinking_override` (which only controls whether `mark_reasoning_started()` is called on the parser, not whether the parser runs at all). Chat API uses the `separate_reasoning` field on `ChatCompletionRequest` instead. Do not suggest removing or replacing the `ThinkingConfig::Enabled` gate in the Messages path.

Learnt from: vschandramourya
Repo: lightseekorg/smg PR: 840
File: model_gateway/src/routers/grpc/pipeline.rs:645-675
Timestamp: 2026-03-24T00:18:24.771Z
Learning: In repo lightseekorg/smg, file model_gateway/src/routers/grpc/pipeline.rs: The `FinalResponse::Completion` match arm in `execute_completion` is intentionally unreachable in PR `#840`. `PreparationStage` and `ResponseProcessingStage` in the regular pipeline currently reject `RequestType::Completion` with "wrong_pipeline" errors, so no stage sets `FinalResponse::Completion` yet. This is deliberate scaffolding — completion-specific pipeline stages (preparation + response processing) will be added in follow-up PRs to make the match arm reachable. The `#[expect(dead_code, reason = "Completion pipeline entrypoint is introduced before later stacked PRs wire the router to call it")]` attribute on `execute_completion` documents this intent. Do not flag the match arm as unreachable dead code in PR `#840` or similar incremental completions PRs.

Learnt from: zhaowenzi
Repo: lightseekorg/smg PR: 938
File: model_gateway/src/routers/responses/handlers.rs:36-37
Timestamp: 2026-03-27T00:45:42.263Z
Learning: In repo lightseekorg/smg, file model_gateway/src/routers/responses/handlers.rs: The `delete_response` handler intentionally does a get_response existence check followed by delete_response (two-step, TOCTOU gap). Making delete_response idempotent (i.e., returning success when the record is already absent) requires a storage interface change and is deferred to a follow-up PR. Do not flag this as a blocking issue.

Learnt from: CatherineSue
Repo: lightseekorg/smg PR: 791
File: model_gateway/src/routers/grpc/harmony/stages/preparation.rs:118-124
Timestamp: 2026-03-17T20:14:15.295Z
Learning: In repo lightseekorg/smg, file model_gateway/src/routers/grpc/harmony/stages/preparation.rs (prepare_chat): The condition `if constraint.is_some() && body_ref.response_format.is_some()` that clears `response_format` only fires when `response_format` was the source of the structural-tag constraint. Protocol-level validation and the previous pipeline stage ensure that a request carrying both a tool constraint and a `response_format` is rejected before reaching this stage. Therefore the comment "If response_format was converted to a structural tag, clear it..." accurately describes the runtime behavior.

Learnt from: XinyueZhang369
Repo: lightseekorg/smg PR: 399
File: protocols/src/interactions.rs:505-509
Timestamp: 2026-02-19T03:08:50.192Z
Learning: In code reviews for Rust projects using the validator crate (v0.20.0), ensure that custom validation functions for numeric primitive types (e.g., f32, i32, u32, i16, etc.) accept the value by value, not by reference. Example: fn validate(value: f32) { ... }. The validator derive macro has a hardcoded list of numeric types that are passed by value, while all other types are passed by reference. Apply this guideline whenever validating numeric fields to align with the derive macro behavior.

Learnt from: slin1237
Repo: lightseekorg/smg PR: 489
File: model_gateway/src/core/token_bucket.rs:58-63
Timestamp: 2026-02-21T02:30:51.443Z
Learning: For lint-only/Clippy enforcement PRs in this repository, avoid introducing behavioral changes (e.g., new input validation or logic changes). Treat such PRs as non-functional changes and plan a separate follow-up issue/PR for hardening or behavior changes. This applies broadly to Rust files across the repo; during review, focus on lint/style corrections and clearly note any intentional exceptions. 

Learnt from: slin1237
Repo: lightseekorg/smg PR: 489
File: protocols/src/responses.rs:928-931
Timestamp: 2026-02-21T02:36:00.882Z
Learning: In Rust code across the repository, use the marker INVARIANT: to document assumptions in safe code. Reserve SAFETY: for explaining why unsafe blocks are sound. This improves clarity of invariants and safety reasoning. Example reference: protocols/src/responses.rs near validate_tool_choice_with_tools().

Learnt from: slin1237
Repo: lightseekorg/smg PR: 489
File: mesh/src/sync.rs:83-83
Timestamp: 2026-02-21T02:37:01.416Z
Learning: General Rust formatting rule: format! with implicit captures only supports simple identifiers, not full expressions like {state.model_id}. For cases where you want to interpolate a field or expression, bind the value first and interpolate the binding, e.g., let model_id = &state.model_id; and then use format!("policy:{}", model_id). In the specific file mesh/src/sync.rs, prefer format!("policy:{}", state.model_id) or bind to a local variable if you need named interpolation, to keep clarity and avoid unintended captures.

Learnt from: zhaowenzi
Repo: lightseekorg/smg PR: 807
File: model_gateway/src/middleware.rs:61-81
Timestamp: 2026-03-18T21:32:00.041Z
Learning: In Rust code using the http crate, HeaderMap::get() is effectively case-insensitive because HeaderName normalizes keys to lowercase on insertion and lookup. Do not require or perform explicit .to_lowercase() before HeaderMap::get() calls. Mark as not a concern for case-sensitivity in lookups; only consider normalization when inserting or comparing via HeaderName, not in lookups.

Learnt from: key4ng
Repo: lightseekorg/smg PR: 867
File: tui/src/app.rs:798-813
Timestamp: 2026-03-22T20:13:55.778Z
Learning: In this repo (lightseekorg/smg), treat the workspace `Cargo.toml`’s `package.rust-version` (MSRV) as the source of truth (e.g., `rust-version = "1.85"`). When reviewing Rust changes, do not flag usage of Rust language/library features that were stabilized on or before the MSRV (e.g., `Option::is_none_or`, stabilized in 1.82, is compatible with an MSRV of 1.85). Always verify the MSRV from the workspace `Cargo.toml` rather than relying on issue templates.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1730e5c62d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +188 to +190
Some(StreamOptions {
include_usage: Some(true),
..StreamOptions::default()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve caller stream options during Responses conversion

In responses_to_chat, the new ResponsesRequest.stream_options is overwritten with a fresh StreamOptions literal, so caller-provided values are discarded. A streaming request that sets "include_obfuscation": false is converted with include_obfuscation: None, meaning the new field added in this commit has no effect on the regular gRPC Responses path. Please merge req.stream_options into the constructed value (while still forcing include_usage if needed) instead of resetting from Default.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 6cdf09a. responses_to_chat now clones the caller's stream_options forward and only backfills include_usage=Some(true) when unset, so include_obfuscation=false (and any future StreamOptions fields) round-trip faithfully. Added three regression tests covering the obfuscation case, caller-set include_usage, and the non-streaming drop-through.

Comment on lines +813 to +816
/// Streaming-only options. Spec: body param `stream_options`.
/// On the Responses API the only documented field is `include_obfuscation`.
#[serde(skip_serializing_if = "Option::is_none")]
pub stream_options: Option<StreamOptions>,

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 Enforce stream requirement for Responses stream_options

The new stream_options field is documented as streaming-only, but no cross-parameter validation was added for Responses requests. As a result, requests with stream: false and stream_options set are accepted and then silently dropped during chat conversion, which differs from the explicit validation behavior already enforced in Chat/Completions. Adding a stream_options-requires-stream check in validate_responses_cross_parameters would prevent this silent no-op.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks for catching. This is legitimate validation hygiene, but cross-parameter validation is deliberately out of P2's schema-only scope — the P-group tasks close wire-format gaps, while semantic validation (including cross-param rules like "stream_options requires stream=true") belongs to a separate validation pass that hasn't been carved as an audit task yet. Filing as a follow-up rather than expanding P2 mid-flight.

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

Looks good — clean additive change with solid test coverage.

Summary: 0 🔴 Important · 0 🟡 Nit · 1 🟣 Pre-existing (not in diff)

The six new ResponsesRequest fields (prompt, prompt_cache_key, prompt_cache_retention, safety_identifier, stream_options, context_management) are correctly modeled, serialized, defaulted, and propagated through the gRPC tool-loop path. The new types (PromptCacheRetention, ContextManagementEntry, ContextManagementType) are straightforward serde data types with appropriate fail-fast behavior for unknown variants. StreamOptions extension with include_obfuscation + Default derive is handled cleanly in both the conversion and test paths.

🟣 Pre-existing note (outside this diff): persistence_utils.rs:145 and utils.rs:90-98 map original_body.user → safety_identifier in stored responses / response patching. Now that ResponsesRequest has an explicit safety_identifier field (documented as "replaces user"), a client that sends safety_identifier without user will have it silently dropped. Worth a follow-up to prefer safety_identifier.or(user).

@slin1237
slin1237 force-pushed the feat/audit-p2-top-level branch from 1730e5c to faa6d24 Compare April 21, 2026 05:26

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

Review Summary (incremental — force-push fallback to full diff)

Reviewed all 6 changed files (+312 / −3). Clean, well-structured PR that adds the 6 missing Responses API top-level fields with proper serde attributes, supporting types, round-trip tests, and multi-turn propagation.

Existing comments — my assessment

Bot Location Severity My take
chatgpt-codex-connector conversions.rs:190 P1 Agree partially. stream_options caller values are indeed discarded during Responses→Chat conversion, but include_obfuscation is a Responses-only concept with no Chat equivalent. The field IS correctly propagated through build_next_request for multi-turn continuations. PR explicitly scopes out runtime wiring. More accurately a P2 (known gap, not a regression).
chatgpt-codex-connector responses.rs:816 P2 Agree. Missing stream_options-requires-stream cross-parameter validation creates a parity gap with Chat (chat.rs:367) and Completions (completion.rs:168). Worth adding in a follow-up.
coderabbitai responses_api_test.rs:112 Nit Reasonable suggestion for a shared test builder, but low priority — the struct-literal pattern is compile-forced and catches missing fields.

What I checked and found clean

  • Type definitions: PromptCacheRetention serde renames (in-memory, 24h) match spec. ContextManagementType closed enum (no #[serde(other)]) aligns with P5 fail-fast direction.
  • StreamOptions extension: Default derive + include_obfuscation: Option<bool> with skip_serializing_if — all 3 construction sites updated with ..StreamOptions::default().
  • build_next_request (common.rs): 6 new fields moved (not cloned), consistent with existing pattern. Multi-turn continuations correctly preserve prompt template, cache key, safety ID, streaming options, and context management.
  • Tests: Round-trip (full payload + both enum variants), absent-field omission (skip_serializing_if verification), and 13 compile-forced test fixtures. Good coverage.
  • No new security concerns: All fields are Option<_> pass-through with skip_serializing_if. No injection surface.

Severity count (new issues this review)

🔴 Important: 0 · 🟡 Nit: 0 · 🟣 Pre-existing: 0

No new issues found beyond what's already flagged.

slin1237 added a commit that referenced this pull request Apr 21, 2026
… bot feedback)

What
----
- model_gateway/src/routers/grpc/regular/responses/conversions.rs:
  - `responses_to_chat` no longer overwrites `ResponsesRequest.stream_options`
    with a fresh `StreamOptions { include_usage: Some(true), ..default() }`
    literal. The caller's `stream_options` is now cloned forward and only
    `include_usage` is defaulted to `Some(true)` when the caller did not set
    it. Non-streaming requests still intentionally drop `stream_options`.
  - Drop the now test-only `StreamOptions` import from the module-level use
    block and re-import it inside `mod tests` so the non-test build does not
    emit `unused_import`.
  - Add three regression tests:
      * `test_stream_options_include_obfuscation_roundtrip` — proves
        `include_obfuscation: Some(false)` survives the conversion and
        `include_usage` still defaults to `Some(true)` when unset.
      * `test_stream_options_caller_include_usage_preserved` — proves the
        caller's `include_usage: Some(false)` is not clobbered.
      * `test_stream_options_non_streaming_dropped` — confirms `stream=false`
        yields `stream_options: None` even when the caller set fields.

Why
---
Codex bot (P1 Major) on PR #1278 flagged that a streaming request with
`stream_options.include_obfuscation=false` was being converted to a
ChatCompletionRequest whose `include_obfuscation` silently became `None`,
discarding caller intent. This is a wire-semantics bug introduced by the P2
schema additions: adding `stream_options` to `ResponsesRequest` without
threading it through the gRPC conversion path left the field accepted but
ignored.

How
---
Switch the literal construction to
`let mut opts = req.stream_options.clone().unwrap_or_default();` followed by
an `if opts.include_usage.is_none()` backfill. This preserves every caller-
supplied field (including future `StreamOptions` additions via `Default`)
while keeping the existing `include_usage=true` behaviour internal pipeline
consumers rely on for end-of-stream usage emission.

Scope
-----
Bug-fix only; no protocol schema changes, no new CLI flags, no bindings
updates. Cross-parameter validation (rejecting `stream_options` with
`stream=false`) is deliberately out of P2's schema-only charter and is
being tracked as a follow-up on the codex P2 dismissal.

Verify
------
- `cargo check --workspace --tests` → clean
- `cargo test -p smg --lib` → 559 passed (3 new)
- `cargo test -p openai-protocol` → 19 passed
- `cargo clippy -p smg -p openai-protocol --lib --bins --tests -- -D warnings` → clean
- `cargo +nightly fmt --all -- --check` → clean

Refs: #1278
Signed-off-by: Simo Lin <linsimo.mark@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6cdf09afd4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// Preserve caller-provided stream_options (e.g. `include_obfuscation: false`
// on the Responses API) and only default `include_usage` when the caller
// did not set it. Non-streaming requests intentionally drop stream_options.
stream_options: if is_streaming {

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 Forward safety and cache IDs in Responses conversion

When building ChatCompletionRequest in responses_to_chat, the new ResponsesRequest fields prompt_cache_key and safety_identifier are never copied, so they always fall back to None via ..Default::default(). In the regular gRPC Responses flow, requests that set either field will deserialize successfully but the backend never receives them, so prompt-cache routing and safety attribution are silently dropped.

Useful? React with 👍 / 👎.

@mergify

mergify Bot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Hi @slin1237, this PR has merge conflicts that must be resolved before it can be merged. Please rebase your branch:

git fetch origin main
git rebase origin/main
# resolve any conflicts, then:
git push --force-with-lease

@mergify mergify Bot added the needs-rebase PR has merge conflicts that need to be resolved label Apr 21, 2026
slin1237 added a commit that referenced this pull request Apr 21, 2026
… bot feedback)

What
----
- model_gateway/src/routers/grpc/regular/responses/conversions.rs:
  - `responses_to_chat` no longer overwrites `ResponsesRequest.stream_options`
    with a fresh `StreamOptions { include_usage: Some(true), ..default() }`
    literal. The caller's `stream_options` is now cloned forward and only
    `include_usage` is defaulted to `Some(true)` when the caller did not set
    it. Non-streaming requests still intentionally drop `stream_options`.
  - Drop the now test-only `StreamOptions` import from the module-level use
    block and re-import it inside `mod tests` so the non-test build does not
    emit `unused_import`.
  - Add three regression tests:
      * `test_stream_options_include_obfuscation_roundtrip` — proves
        `include_obfuscation: Some(false)` survives the conversion and
        `include_usage` still defaults to `Some(true)` when unset.
      * `test_stream_options_caller_include_usage_preserved` — proves the
        caller's `include_usage: Some(false)` is not clobbered.
      * `test_stream_options_non_streaming_dropped` — confirms `stream=false`
        yields `stream_options: None` even when the caller set fields.

Why
---
Codex bot (P1 Major) on PR #1278 flagged that a streaming request with
`stream_options.include_obfuscation=false` was being converted to a
ChatCompletionRequest whose `include_obfuscation` silently became `None`,
discarding caller intent. This is a wire-semantics bug introduced by the P2
schema additions: adding `stream_options` to `ResponsesRequest` without
threading it through the gRPC conversion path left the field accepted but
ignored.

How
---
Switch the literal construction to
`let mut opts = req.stream_options.clone().unwrap_or_default();` followed by
an `if opts.include_usage.is_none()` backfill. This preserves every caller-
supplied field (including future `StreamOptions` additions via `Default`)
while keeping the existing `include_usage=true` behaviour internal pipeline
consumers rely on for end-of-stream usage emission.

Scope
-----
Bug-fix only; no protocol schema changes, no new CLI flags, no bindings
updates. Cross-parameter validation (rejecting `stream_options` with
`stream=false`) is deliberately out of P2's schema-only charter and is
being tracked as a follow-up on the codex P2 dismissal.

Verify
------
- `cargo check --workspace --tests` → clean
- `cargo test -p smg --lib` → 559 passed (3 new)
- `cargo test -p openai-protocol` → 19 passed
- `cargo clippy -p smg -p openai-protocol --lib --bins --tests -- -D warnings` → clean
- `cargo +nightly fmt --all -- --check` → clean

Refs: #1278
Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
@slin1237
slin1237 force-pushed the feat/audit-p2-top-level branch from 6cdf09a to e622f89 Compare April 21, 2026 14:50
What:
- Extend `openai_protocol::common::StreamOptions` with an optional
  `include_obfuscation: Option<bool>` and derive `Default` so the struct
  is now the shared Chat-and-Responses stream-options type.
- Introduce `PromptCacheRetention` (`"in-memory"` | `"24h"`),
  `ContextManagementEntry { type, compact_threshold }`,
  `ContextManagementType::Compaction`, and a `ConversationRef`
  untagged union (`string | { id: string }`) with an `as_id()` helper
  in `openai_protocol::common` for downstream tasks (P6 consumes
  `ConversationRef`).
- Add six new top-level fields to `ResponsesRequest`:
  `prompt: Option<ResponsePrompt>`, `prompt_cache_key: Option<String>`,
  `prompt_cache_retention: Option<PromptCacheRetention>`,
  `safety_identifier: Option<String>`, `stream_options: Option<StreamOptions>`,
  `context_management: Option<Vec<ContextManagementEntry>>`, all
  `#[serde(skip_serializing_if = "Option::is_none")]`. Extend the
  `Default` impl accordingly.
- Thread the six new fields through the gRPC regular-mode tool-loop
  continuation (`build_next_request` in
  `model_gateway/src/routers/grpc/regular/responses/common.rs`) so
  multi-turn loops preserve prompt template, cache key, safety
  identifier, streaming options, and context-management config.
- Update `responses_to_chat` in
  `model_gateway/src/routers/grpc/regular/responses/conversions.rs`
  and two chat-completion spec tests to use
  `..StreamOptions::default()` (now that the struct has a `Default`
  impl) instead of field-exhaustive literals.
- Add three serde round-trip tests in `crates/protocols/src/responses.rs`
  covering the full P2 field set, the `"in-memory"` retention variant,
  and absent-field omission on the wire.

Why:
- The SMG protocol types were missing six top-level fields required by
  the OpenAI Responses API spec (§Body Parameters: `prompt`,
  `prompt_cache_key`, `prompt_cache_retention`, `safety_identifier`,
  `stream_options`, `context_management`). Today spec-valid requests
  carrying any of these are silently dropped during deserialization,
  which masks client intent and breaks upstream routing decisions
  (cache reuse, obfuscation, compaction thresholds) that depend on
  these knobs. Adding them as typed `Option<_>` fields makes the
  gateway forward them losslessly and gives future tasks (P6, routing
  wiring) a typed surface to consume.

How:
- Grouped the new Responses-only types in `common.rs` next to the
  existing `ResponsePrompt` / `PromptVariable` cluster so every
  Responses-API-shared type lives in one place.
- Reused the existing `common::StreamOptions` rather than introducing
  a Responses-specific duplicate: the OpenAI wire shape is identical
  modulo two optional fields, and collapsing to one type avoids drift
  between chat and responses streaming-options handling. `Default`
  derive lets existing call sites keep their explicit
  `include_usage` literal via `..StreamOptions::default()`.
- Defined `ConversationRef` now (explicitly listed in the P2 audit
  entry) even though no existing field uses it yet, so P6 can migrate
  `ResponsesRequest::conversation` to `Option<ConversationRef>`
  without reopening this PR; kept `ResponsesRequest::conversation`
  as `Option<String>` here to respect P6's dependency contract
  (P6 depends on P2 for the type; P6 owns the validator and
  `history.rs` persistence migration).
- Chose `ContextManagementType` as a closed enum (only `Compaction`)
  rather than `String`: the spec today enumerates exactly one value,
  and a closed enum aligns with P5's fail-fast-on-unknown direction
  (unknown values serde-fail instead of being silently accepted).
- All `Option<_>` fields use `skip_serializing_if = "Option::is_none"`
  so absent fields stay absent on the wire — verified by
  `test_responses_request_new_fields_omitted_when_absent`.

Refs: P2
Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
Tech Lead P2 cycle-1 REJECT: ConversationRef was `pub` with zero callers
— §7 "Every new pub item must be imported somewhere". P6 owns the
ConversationRef type AND its migration into ResponsesRequest::conversation;
pre-landing it here is speculative. Removed the enum + its impl + section
comment. No change to the 6 new top-level ResponsesRequest fields or
other P2 additions.

Refs: P2
Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
… bot feedback)

What
----
- model_gateway/src/routers/grpc/regular/responses/conversions.rs:
  - `responses_to_chat` no longer overwrites `ResponsesRequest.stream_options`
    with a fresh `StreamOptions { include_usage: Some(true), ..default() }`
    literal. The caller's `stream_options` is now cloned forward and only
    `include_usage` is defaulted to `Some(true)` when the caller did not set
    it. Non-streaming requests still intentionally drop `stream_options`.
  - Drop the now test-only `StreamOptions` import from the module-level use
    block and re-import it inside `mod tests` so the non-test build does not
    emit `unused_import`.
  - Add three regression tests:
      * `test_stream_options_include_obfuscation_roundtrip` — proves
        `include_obfuscation: Some(false)` survives the conversion and
        `include_usage` still defaults to `Some(true)` when unset.
      * `test_stream_options_caller_include_usage_preserved` — proves the
        caller's `include_usage: Some(false)` is not clobbered.
      * `test_stream_options_non_streaming_dropped` — confirms `stream=false`
        yields `stream_options: None` even when the caller set fields.

Why
---
Codex bot (P1 Major) on PR #1278 flagged that a streaming request with
`stream_options.include_obfuscation=false` was being converted to a
ChatCompletionRequest whose `include_obfuscation` silently became `None`,
discarding caller intent. This is a wire-semantics bug introduced by the P2
schema additions: adding `stream_options` to `ResponsesRequest` without
threading it through the gRPC conversion path left the field accepted but
ignored.

How
---
Switch the literal construction to
`let mut opts = req.stream_options.clone().unwrap_or_default();` followed by
an `if opts.include_usage.is_none()` backfill. This preserves every caller-
supplied field (including future `StreamOptions` additions via `Default`)
while keeping the existing `include_usage=true` behaviour internal pipeline
consumers rely on for end-of-stream usage emission.

Scope
-----
Bug-fix only; no protocol schema changes, no new CLI flags, no bindings
updates. Cross-parameter validation (rejecting `stream_options` with
`stream=false`) is deliberately out of P2's schema-only charter and is
being tracked as a follow-up on the codex P2 dismissal.

Verify
------
- `cargo check --workspace --tests` → clean
- `cargo test -p smg --lib` → 559 passed (3 new)
- `cargo test -p openai-protocol` → 19 passed
- `cargo clippy -p smg -p openai-protocol --lib --bins --tests -- -D warnings` → clean
- `cargo +nightly fmt --all -- --check` → clean

Refs: #1278
Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
@slin1237
slin1237 force-pushed the feat/audit-p2-top-level branch from e622f89 to 4e58478 Compare April 21, 2026 16:14
@mergify mergify Bot removed the needs-rebase PR has merge conflicts that need to be resolved label Apr 21, 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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/protocols/src/responses.rs`:
- Around line 2013-2144: Add a negative serde test that verifies deserializing a
ResponsesRequest with context_management[0].type set to an unknown value (e.g.,
"future_mode") fails; create a new test function (e.g.,
test_context_management_unknown_type_rejects) that builds a payload with
"context_management": [{"type":"future_mode"}], attempts
serde_json::from_value::<ResponsesRequest>(...), and asserts the result is an
error (is_err() or unwrap_err()), referencing ResponsesRequest and the
ContextManagementType enum to lock in closed-enum behavior and prevent
accidental #[serde(other)] regressions.
🪄 Autofix (Beta)

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: ASSERTIVE

Plan: Pro

Run ID: 184cba37-0877-48a3-b3dc-bab28a3af427

📥 Commits

Reviewing files that changed from the base of the PR and between e622f89 and 4e58478.

📒 Files selected for processing (6)
  • crates/protocols/src/common.rs
  • crates/protocols/src/responses.rs
  • model_gateway/src/routers/grpc/regular/responses/common.rs
  • model_gateway/src/routers/grpc/regular/responses/conversions.rs
  • model_gateway/tests/api/responses_api_test.rs
  • model_gateway/tests/spec/chat_completion.rs

Comment thread crates/protocols/src/responses.rs
@slin1237
slin1237 merged commit f9bcf7d into main Apr 21, 2026
38 checks passed
@slin1237
slin1237 deleted the feat/audit-p2-top-level branch April 21, 2026 16:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

grpc gRPC client and router changes model-gateway Model gateway crate changes protocols Protocols crate changes tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant