Skip to content

(MOT-3961) feat: expose model-specific reasoning efforts - #473

Merged
ytallo merged 2 commits into
mainfrom
feat/model-specific-efforts
Jul 10, 2026
Merged

(MOT-3961) feat: expose model-specific reasoning efforts#473
ytallo merged 2 commits into
mainfrom
feat/model-specific-efforts

Conversation

@ytallo

@ytallo ytallo commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • carry model-specific reasoning effort options through provider discovery, the router, harness requests, and the console; Codex discovery now keeps current model aliases and validates each model's native effort set
  • use the OpenAI Responses API by default while preserving Chat Completions compatibility for configured legacy endpoints; Luna tool calls on Chat Completions now omit reasoning effort to avoid rejected turns
  • consolidate model and effort selection into one compact picker with provider accordions, selected-model focus, inline effort controls, and a compact approval dropdown
  • add a harness cargo-clean target for all workers or an explicit worker subset

Impact

Users see only supported reasoning efforts for the selected model, can move directly from model selection to effort selection, and no longer hit the Luna function-tools plus reasoning-effort failure on the default OpenAI transport.

Validation

  • pnpm test --run — 72 files, 927 tests
  • pnpm exec biome check and pnpm typecheck
  • cargo test — 79 library, 2 binary, 6 integration, and 4 schema tests
  • cargo clippy --all-targets -- -D warnings and cargo fmt --check
  • make -n cargo-clean and selected-worker dry run
  • Storybook browser check for provider collapsing, selected-model focus, model-to-effort focus, and closing on effort selection

Fixes MOT-3961

Summary by CodeRabbit

  • New Features
    • Model selection now displays provider groups and model-specific reasoning-effort options.
    • Added support for OpenAI Responses API streaming, including text, reasoning, tool calls, and usage.
    • Reasoning-effort selections are forwarded to supported providers.
  • Improvements
    • Provider availability updates without unnecessarily refreshing the model catalog.
    • Permission modes now use a compact dropdown control.
  • Bug Fixes
    • Improved streamed tool-call handling and validation of unsupported reasoning options.
  • Documentation
    • Updated provider and model capability documentation.

@vercel

vercel Bot commented Jul 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview, Comment Jul 10, 2026 9:33pm
workers-tech-spec Ready Ready Preview, Comment Jul 10, 2026 9:33pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds provider-native reasoning-effort metadata and forwarding, introduces OpenAI Responses API support with SSE handling, updates Codex reasoning validation, and redesigns console model and permission pickers.

Changes

Reasoning metadata and forwarding

Layer / File(s) Summary
Model contracts and catalog
console/web/src/types/chat.ts, console/web/src/lib/models-catalog.ts, llm-router/src/types/model.rs, harness/src/types/model.rs, tech-specs/...
Models and catalog entries now expose provider-native reasoning efforts with descriptions and validation.
Harness request propagation
console/web/src/lib/backend/real.ts, harness/src/functions/send.rs, harness/src/types/turn.rs, harness/src/turn_loop.rs, harness/src/clients/router.rs
Provider options flow from chat requests into turn records and router payloads.
Codex native effort handling
provider-openai-codex/src/{discovery,reasoning,request,stream_fn}.rs
Codex discovers supported native efforts, validates requested values, and forwards validated strings.
Schemas and provider metadata
llm-router/tests/golden/schemas/*, harness/tests/golden/schemas/*, provider-*/tests/golden/schemas/*
Golden schemas include reasoning-effort model metadata and provider options.
Console picker behavior
console/web/src/components/chat/*, console/web/src/hooks/use-model-picker-source.ts, console/web/src/components/permissions/*
The model picker uses provider groups and effort-specific controls, while provider availability updates independently from catalog refreshes.

OpenAI Responses support

Layer / File(s) Summary
Endpoint selection and request construction
provider-openai/src/config.rs, provider-openai/src/discovery.rs, provider-openai/src/request.rs
Responses is the default transport; custom endpoints select Responses or Chat Completions request shapes.
Wire serialization and streaming
provider-openai/src/wire/*, provider-openai/src/sse.rs, provider-openai/src/upstream.rs
Messages, tools, reasoning blocks, function calls, usage, completion, and incomplete Responses events are serialized or parsed.
Compatibility and integration coverage
provider-openai/src/stream_fn.rs, provider-openai/tests/integration.rs, provider-openai/src/*
Chat Completions tool compatibility remains supported, with unit and end-to-end Responses coverage.

Supporting updates

Layer / File(s) Summary
Runtime compatibility fixes
provider-llamacpp/src/sse.rs, provider-*/src/curated.rs, provider-*/src/discovery.rs
Model initializers include the new optional field, and streamed llama.cpp function-call deltas include call identifiers.
Developer tooling
harness/Makefile
A validated cargo-clean target cleans selected worker crates.

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

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant Console
  participant RealBackend
  participant Harness
  participant Router
  participant OpenAI
  Console->>RealBackend: Select model and reasoning effort
  RealBackend->>Harness: harness::send with provider_options
  Harness->>Router: router::chat with provider_options
  Router->>OpenAI: Responses or Chat Completions request
  OpenAI-->>Router: SSE response events
  Router-->>Console: Streamed assistant events
Loading

Poem

A rabbit hops through efforts bright,
From default dusk to xhigh light.
Responses stream and tools align,
Provider paths now neatly shine.
“Hop hooray!” the picker sings.

🚥 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 is concise and accurately summarizes the main change: exposing model-specific reasoning efforts.
Docstring Coverage ✅ Passed Docstring coverage is 97.62% 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.
✨ 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/model-specific-efforts

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.

@github-actions

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 41 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

@ytallo
ytallo marked this pull request as ready for review July 10, 2026 21:34
@ytallo
ytallo merged commit e4ce49f into main Jul 10, 2026
54 checks passed

@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: 4

🧹 Nitpick comments (4)
harness/src/turn_loop.rs (1)

335-339: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Silent fallback to None on serialization failure.

serde_json::to_value(options).ok() swallows any serialization error, silently omitting provider_options from the request instead of surfacing it. Given options is a BTreeMap<String, Value>, serialization is practically infallible today, so this is low risk, but a tracing::warn! on the Err branch would make a future regression (e.g. a key type change) visible instead of silently dropping provider-native options from the call.

♻️ Optional: surface serialization failures
-        provider_options: record
-            .options
-            .provider_options
-            .as_ref()
-            .and_then(|options| serde_json::to_value(options).ok()),
+        provider_options: record.options.provider_options.as_ref().and_then(|options| {
+            serde_json::to_value(options)
+                .inspect_err(|e| tracing::warn!(error = %e, "failed to serialize provider_options"))
+                .ok()
+        }),
🤖 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 `@harness/src/turn_loop.rs` around lines 335 - 339, In the provider_options
construction within the turn-loop request, replace the silent
serde_json::to_value(options).ok() fallback with explicit error handling that
emits a tracing::warn! containing the serialization error, while still returning
None on failure; keep successful serialization unchanged.
harness/Makefile (1)

81-93: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared Cargo worker loop.

checkmake flags this new target for exceeding the maximum body length, and its validation/iteration logic duplicates build. Move the common worker-manifest loop into a Make macro or helper script so both targets remain concise and consistent.

🤖 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 `@harness/Makefile` around lines 81 - 93, Extract the duplicated worker
validation and iteration logic from cargo-clean and build into a shared Make
macro or helper script. Preserve manifest existence checks, worker reporting,
and command execution behavior, then have both targets invoke the shared helper
so each target stays within checkmake’s body-length limit.

Source: Linters/SAST tools

provider-openai-codex/src/reasoning.rs (1)

212-250: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding edge-case tests for native_reasoning_effort.

The current test covers the happy path (supported effort accepted) and the rejection path (unsupported effort rejected). Several branches lack coverage: provider_options is None, reasoning_effort key absent, non-string value, empty/whitespace string, and model_meta is None (cold-start permissive path). These are the input-validation boundaries most likely to regress.

🧪 Suggested additional tests
#[test]
fn native_effort_absent_when_no_provider_options() {
    assert_eq!(native_reasoning_effort(None, None), Ok(None));
}

#[test]
fn native_effort_absent_when_key_missing() {
    assert_eq!(
        native_reasoning_effort(Some(&serde_json::json!({})), None),
        Ok(None)
    );
}

#[test]
fn native_effort_rejects_non_string() {
    assert!(native_reasoning_effort(
        Some(&serde_json::json!({ "reasoning_effort": 42 })),
        None,
    )
    .unwrap_err()
    .contains("non-empty string"));
}

#[test]
fn native_effort_rejects_empty_or_whitespace() {
    assert!(native_reasoning_effort(
        Some(&serde_json::json!({ "reasoning_effort": "  " })),
        None,
    )
    .is_err());
}

#[test]
fn native_effort_permissive_without_catalog() {
    assert_eq!(
        native_reasoning_effort(
            Some(&serde_json::json!({ "reasoning_effort": "anything" })),
            None,
        ),
        Ok(Some("anything".into()))
    );
}
🤖 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 `@provider-openai-codex/src/reasoning.rs` around lines 212 - 250, Add edge-case
unit tests alongside native_effort_uses_model_specific_catalog for
native_reasoning_effort: cover None provider_options, a missing reasoning_effort
key, non-string values, empty or whitespace-only strings, and valid arbitrary
efforts when model_meta is None. Assert the expected Ok(None), validation
errors, or permissive Ok(Some(...)) results for each case.
provider-openai/src/stream_fn.rs (1)

213-238: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add test case for effort = None.

The test only covers Some("high"). Add a case for None to verify the guard does not activate when no reasoning effort is present.

♻️ Proposed additional test assertions
     assert_eq!(
         compatible_reasoning_effort(
             ApiMode::ChatCompletions,
             "gpt-5.6-luna",
             false,
             Some("high")
         ),
         (Some("high"), false)
     );
+    // None effort should not trigger the guard
+    assert_eq!(
+        compatible_reasoning_effort(
+            ApiMode::ChatCompletions,
+            "gpt-5.6-luna",
+            true,
+            None,
+        ),
+        (None, false)
+    );
 }
🤖 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 `@provider-openai/src/stream_fn.rs` around lines 213 - 238, Add an assertion in
the existing luna_tools_disable_effort_only_on_chat_completions test for
compatible_reasoning_effort with ApiMode::ChatCompletions, model "gpt-5.6-luna",
tools enabled, and effort None; verify it returns (None, false), confirming the
guard does not activate without a reasoning effort.
🤖 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 `@console/web/src/hooks/use-model-picker-source.ts`:
- Around line 73-97: Update the initial snapshot logic in the useEffect using
fetchProviderList so a provider event arriving during the request does not
discard the fetched provider list. When the snapshot resolves, reconcile it with
the current presentProviders by provider id, preserving the freshest
event-driven available state while using snapshot metadata such as display_name
and supports_model_listing; ensure newly event-created entries are replaced or
enriched rather than retaining fabricated values. Keep cancellation handling
intact and apply the same reconciliation on successful snapshot completion even
when providerEventVersion.current differs from snapshotVersion.

In `@provider-openai/src/sse.rs`:
- Around line 484-510: Handle the “content_filter” reason in the
response.incomplete branch of the SSE event handler as a graceful completion,
matching the Chat Completions behavior: set StopReason::End, emit the
appropriate warning, close any open block, and push Stop and Done events instead
of AssistantMessageEvent::Error. Preserve the existing max_output_tokens
handling and error behavior for other reasons.
- Around line 58-60: The has_content method must match build_content by counting
only function_calls entries with a non-empty function_id, preventing empty
entries from being treated as content. Also update response.incomplete handling
to recognize content_filter separately from unknown reasons, returning the
documented non-hard-failure behavior instead of Error; use the relevant
build_content and incomplete-response matching logic in
provider-openai/src/sse.rs.

In `@provider-openai/src/stream_fn.rs`:
- Around line 24-40: Update compatible_reasoning_effort so needs_luna_guard only
activates when effort.is_some() and the requested effort is not "none"; preserve
the existing ChatCompletions, tools, and Luna model checks, and continue
returning the original None unchanged when no effort was requested.

---

Nitpick comments:
In `@harness/Makefile`:
- Around line 81-93: Extract the duplicated worker validation and iteration
logic from cargo-clean and build into a shared Make macro or helper script.
Preserve manifest existence checks, worker reporting, and command execution
behavior, then have both targets invoke the shared helper so each target stays
within checkmake’s body-length limit.

In `@harness/src/turn_loop.rs`:
- Around line 335-339: In the provider_options construction within the turn-loop
request, replace the silent serde_json::to_value(options).ok() fallback with
explicit error handling that emits a tracing::warn! containing the serialization
error, while still returning None on failure; keep successful serialization
unchanged.

In `@provider-openai-codex/src/reasoning.rs`:
- Around line 212-250: Add edge-case unit tests alongside
native_effort_uses_model_specific_catalog for native_reasoning_effort: cover
None provider_options, a missing reasoning_effort key, non-string values, empty
or whitespace-only strings, and valid arbitrary efforts when model_meta is None.
Assert the expected Ok(None), validation errors, or permissive Ok(Some(...))
results for each case.

In `@provider-openai/src/stream_fn.rs`:
- Around line 213-238: Add an assertion in the existing
luna_tools_disable_effort_only_on_chat_completions test for
compatible_reasoning_effort with ApiMode::ChatCompletions, model "gpt-5.6-luna",
tools enabled, and effort None; verify it returns (None, false), confirming the
guard does not activate without a reasoning effort.
🪄 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: CHILL

Plan: Pro

Run ID: 157d79b4-bc16-4aca-bdbb-96e9690f1bd7

📥 Commits

Reviewing files that changed from the base of the PR and between 1e52245 and cbff05c.

⛔ Files ignored due to path filters (1)
  • provider-openai/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (63)
  • console/web/src/components/chat/Composer.stories.tsx
  • console/web/src/components/chat/ModelPicker.tsx
  • console/web/src/components/permissions/PermissionModePicker.tsx
  • console/web/src/hooks/use-model-picker-source.ts
  • console/web/src/lib/backend/harness-send.ts
  • console/web/src/lib/backend/real-metadata.test.ts
  • console/web/src/lib/backend/real.ts
  • console/web/src/lib/backend/types.ts
  • console/web/src/lib/models-catalog.test.ts
  • console/web/src/lib/models-catalog.ts
  • console/web/src/types/chat.ts
  • harness/Makefile
  • harness/src/clients/router.rs
  • harness/src/functions/send.rs
  • harness/src/subagent.rs
  • harness/src/turn_loop.rs
  • harness/src/types/mod.rs
  • harness/src/types/model.rs
  • harness/src/types/turn.rs
  • harness/tests/golden/schemas/harness.send.json
  • llm-router/src/catalog/queries.rs
  • llm-router/src/types/model.rs
  • llm-router/tests/golden/schemas/router.models.get.json
  • llm-router/tests/golden/schemas/router.models.list.json
  • llm-router/tests/golden/schemas/router.models.reconcile.json
  • llm-router/tests/golden/schemas/router.provider.register.json
  • provider-anthropic/src/discovery.rs
  • provider-anthropic/src/thinking.rs
  • provider-anthropic/tests/golden/schemas/provider.anthropic.stream.json
  • provider-llamacpp/src/discovery.rs
  • provider-llamacpp/src/sse.rs
  • provider-llamacpp/tests/golden/schemas/provider.llamacpp.stream.json
  • provider-openai-codex/src/discovery.rs
  • provider-openai-codex/src/reasoning.rs
  • provider-openai-codex/src/request.rs
  • provider-openai-codex/src/stream_fn.rs
  • provider-openai-codex/tests/golden/schemas/provider.openai-codex.stream.json
  • provider-openai/Cargo.toml
  • provider-openai/README.md
  • provider-openai/iii.worker.yaml
  • provider-openai/src/config.rs
  • provider-openai/src/curated.rs
  • provider-openai/src/discovery.rs
  • provider-openai/src/lib.rs
  • provider-openai/src/main.rs
  • provider-openai/src/manifest.rs
  • provider-openai/src/reasoning.rs
  • provider-openai/src/request.rs
  • provider-openai/src/sse.rs
  • provider-openai/src/stream_fn.rs
  • provider-openai/src/surface.rs
  • provider-openai/src/upstream.rs
  • provider-openai/src/wire/messages.rs
  • provider-openai/src/wire/mod.rs
  • provider-openai/src/wire/tools.rs
  • provider-openai/tests/golden/schemas/provider.openai.stream.json
  • provider-openai/tests/integration.rs
  • provider-xai/src/curated.rs
  • provider-xai/tests/golden/schemas/provider.xai.stream.json
  • provider-zai/src/curated.rs
  • provider-zai/tests/golden/schemas/provider.zai.stream.json
  • tech-specs/2026-06-agentic/README.md
  • tech-specs/2026-06-agentic/llm-router.md

Comment on lines +73 to +97
// One initial snapshot. Subsequent availability changes are applied from
// `router::provider::changed`, so model refreshes never re-read this list.
useEffect(() => {
if (backendId !== 'real' || !harnessAvailable) {
setPresentProviders([])
return
}
let cancelled = false
const snapshotVersion = providerEventVersion.current
void fetchProviderList()
.then((providers) => {
if (!cancelled && providerEventVersion.current === snapshotVersion) {
setPresentProviders(providers)
}
})
.catch(() => {
if (!cancelled && providerEventVersion.current === snapshotVersion) {
setPresentProviders([])
}
})
return () => {
cancelled = true
}
}, [backendId, harnessAvailable])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Snapshot is discarded (not merged) if any provider event lands first — can leave presentProviders incomplete.

The staleness guard on Lines 84-90 compares providerEventVersion.current to the version captured before the fetch, but on mismatch it drops the entire fetched snapshot rather than merging it with whatever partial updates already landed. Since subscribeProviderChanges is wired up asynchronously in a sibling effect (Lines 121-145) and both effects fire on mount, a provider::changed event that arrives before the snapshot promise resolves will bump the version and cause the full list from fetchProviderList() to be thrown away — leaving presentProviders with only the one entry the event patched (built from an empty starting array), not the complete provider list.

Compounding this, the "add new provider" branch (Lines 132-140) fabricates display_name: provider (raw id) and supports_model_listing: true for any provider not yet known, and there's no other path that refreshes this metadata after mount — so an event-created entry can persist with guessed data indefinitely.

Consider merging the snapshot into current state (e.g. reconcile by id, preferring the freshest available flag per entry) instead of an all-or-nothing overwrite/discard, so a late-arriving event never causes the base list to vanish.

💡 Sketch: merge instead of discard
     void fetchProviderList()
       .then((providers) => {
-        if (!cancelled && providerEventVersion.current === snapshotVersion) {
-          setPresentProviders(providers)
-        }
+        if (cancelled) return
+        setPresentProviders((current) => {
+          // Merge: snapshot supplies full/accurate entries; keep any
+          // `available` flips applied by events that arrived after the
+          // snapshot version was captured.
+          const byId = new Map(current.map((p) => [p.id, p]))
+          return providers.map((p) => {
+            const patched = byId.get(p.id)
+            return patched && providerEventVersion.current !== snapshotVersion
+              ? { ...p, available: patched.available }
+              : p
+          })
+        })
       })
📝 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
// One initial snapshot. Subsequent availability changes are applied from
// `router::provider::changed`, so model refreshes never re-read this list.
useEffect(() => {
if (backendId !== 'real' || !harnessAvailable) {
setPresentProviders([])
return
}
let cancelled = false
const snapshotVersion = providerEventVersion.current
void fetchProviderList()
.then((providers) => {
if (!cancelled && providerEventVersion.current === snapshotVersion) {
setPresentProviders(providers)
}
})
.catch(() => {
if (!cancelled && providerEventVersion.current === snapshotVersion) {
setPresentProviders([])
}
})
return () => {
cancelled = true
}
}, [backendId, harnessAvailable])
// One initial snapshot. Subsequent availability changes are applied from
// `router::provider::changed`, so model refreshes never re-read this list.
useEffect(() => {
if (backendId !== 'real' || !harnessAvailable) {
setPresentProviders([])
return
}
let cancelled = false
const snapshotVersion = providerEventVersion.current
void fetchProviderList()
.then((providers) => {
if (cancelled) return
setPresentProviders((current) => {
// Merge: snapshot supplies full/accurate entries; keep any
// `available` flips applied by events that arrived after the
// snapshot version was captured.
const byId = new Map(current.map((p) => [p.id, p]))
return providers.map((p) => {
const patched = byId.get(p.id)
return patched && providerEventVersion.current !== snapshotVersion
? { ...p, available: patched.available }
: p
})
})
})
.catch(() => {
if (!cancelled && providerEventVersion.current === snapshotVersion) {
setPresentProviders([])
}
})
return () => {
cancelled = true
}
}, [backendId, harnessAvailable])
🤖 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 `@console/web/src/hooks/use-model-picker-source.ts` around lines 73 - 97,
Update the initial snapshot logic in the useEffect using fetchProviderList so a
provider event arriving during the request does not discard the fetched provider
list. When the snapshot resolves, reconcile it with the current presentProviders
by provider id, preserving the freshest event-driven available state while using
snapshot metadata such as display_name and supports_model_listing; ensure newly
event-created entries are replaced or enriched rather than retaining fabricated
values. Keep cancellation handling intact and apply the same reconciliation on
successful snapshot completion even when providerEventVersion.current differs
from snapshotVersion.

Comment on lines +58 to +60
pub fn has_content(&self) -> bool {
!self.text.is_empty() || !self.thinking.is_empty() || !self.function_calls.is_empty()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate relevant definitions and call sites.
rg -n "has_content\\(|build_content\\(|ensure_function_call\\(|function_id|response\\.incomplete|incomplete_details|Done vs synthetic error|synthetic error|handle_responses_event" provider-openai/src -S

# Show the surrounding code for the relevant file.
sed -n '1,220p' provider-openai/src/sse.rs

# Show the upstream fallback logic if present.
sed -n '1,260p' provider-openai/src/upstream.rs

Repository: iii-hq/workers

Length of output: 19889


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find exact locations in provider-openai/src where has_content is used.
rg -n "has_content\\(" provider-openai/src -S

# Show the specific surrounding sections from upstream.rs if found.
grep -n "has_content()" -n provider-openai/src/upstream.rs || true

Repository: iii-hq/workers

Length of output: 320


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== sse.rs =="
nl -ba provider-openai/src/sse.rs | sed -n '1,220p'

echo
echo "== upstream.rs matches =="
rg -n "has_content\\(|Done|synthetic error|response\\.incomplete|incomplete_details|content_filter|max_output_tokens|function_id|function_calls" provider-openai/src/upstream.rs provider-openai/src -S

Repository: iii-hq/workers

Length of output: 205


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the Responses-event branch and its tests around response.incomplete.
sed -n '340,540p' provider-openai/src/sse.rs

echo
echo "== tests around response.incomplete =="
sed -n '780,860p' provider-openai/src/sse.rs

Repository: iii-hq/workers

Length of output: 10492


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the exact section around handle_responses_event and response.incomplete.
sed -n '350,520p' provider-openai/src/sse.rs

echo
echo "== response.incomplete tests =="
sed -n '820,860p' provider-openai/src/sse.rs

Repository: iii-hq/workers

Length of output: 8051


🌐 Web query:

OpenAI Responses API incomplete_details.reason documented values max_output_tokens content_filter

💡 Result:

In the OpenAI Responses API, the incomplete_details.reason field provides the specific reason why a response has terminated with a status of "incomplete" [1][2]. According to official API documentation, the documented string values for this field are [1]: - max_output_tokens: Indicates that the response was truncated because the model reached the configured max_output_tokens limit [1][2]. Note that this limit applies to all tokens generated, including non-visible tokens like formatting, delimiters, and reasoning tokens [3]. - content_filter: Indicates that the response was terminated due to a content policy violation [1][2]. When a response status is "incomplete," the incomplete_details object will contain this reason [1]. Developers are advised to inspect this field to determine the appropriate handling logic (e.g., whether to retry with adjusted parameters) [4][5][6]. In some cases, particularly with "max_output_tokens," a response may contain reasoning items but no message items, resulting in an empty output_text [5][7]. Additionally, while these are the primary documented reasons, client implementations should be prepared for potential null values or future-added enum members if using older SDK versions [8][9].

Citations:


Align has_content() with build_content() and avoid hard-errors for documented content_filter incompletes

  • has_content() still counts any non-empty function_calls vec, but build_content() drops entries with an empty function_id. That can make the stream-end fallback emit Done with no content instead of the intended error.
  • response.incomplete currently treats every non-max_output_tokens reason as Error, but content_filter is a documented Responses reason. Handle it separately instead of converting it into a hard failure.
🤖 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 `@provider-openai/src/sse.rs` around lines 58 - 60, The has_content method must
match build_content by counting only function_calls entries with a non-empty
function_id, preventing empty entries from being treated as content. Also update
response.incomplete handling to recognize content_filter separately from unknown
reasons, returning the documented non-hard-failure behavior instead of Error;
use the relevant build_content and incomplete-response matching logic in
provider-openai/src/sse.rs.

Comment on lines +484 to +510
"response.incomplete" => {
responses_usage(event, state);
let reason = event
.pointer("/response/incomplete_details/reason")
.and_then(Value::as_str)
.unwrap_or("incomplete");
if reason == "max_output_tokens" {
state.stop_reason = StopReason::Length;
state.native_stop_reason = Some(reason.to_string());
close_open_block(state, model, &mut events);
events.push(AssistantMessageEvent::Stop {
stop_reason: StopReason::Length,
error_message: None,
error_kind: None,
});
events.push(AssistantMessageEvent::Done {
message: build_final(state, model),
});
} else {
let message = format!("OpenAI response incomplete: {reason}");
state.stop_reason = StopReason::Error;
state.error_message = Some(message.clone());
let mut error = build_final(state, model);
error.error_kind = Some(classify(None, &message));
events.push(AssistantMessageEvent::Error { error });
}
}

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 | ⚡ Quick win

Handle content_filter as a graceful stop
incomplete_details.reason can be content_filter on the Responses API, so this branch should mirror the Chat Completions path (StopReason::End + warning) instead of turning it into AssistantMessageEvent::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 `@provider-openai/src/sse.rs` around lines 484 - 510, Handle the
“content_filter” reason in the response.incomplete branch of the SSE event
handler as a graceful completion, matching the Chat Completions behavior: set
StopReason::End, emit the appropriate warning, close any open block, and push
Stop and Done events instead of AssistantMessageEvent::Error. Preserve the
existing max_output_tokens handling and error behavior for other reasons.

Comment on lines +24 to +40
fn compatible_reasoning_effort(
api_mode: ApiMode,
model: &str,
has_tools: bool,
effort: Option<&'static str>,
) -> (Option<&'static str>, bool) {
let needs_luna_guard = api_mode == ApiMode::ChatCompletions
&& has_tools
&& model.to_ascii_lowercase().contains("luna")
&& effort != Some("none");
if needs_luna_guard {
(Some("none"), true)
} else {
(effort, false)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard activates when effort is None, adding reasoning_effort: "none" unnecessarily.

When effort is None (no reasoning effort requested), effort != Some("none") evaluates to true, so the guard activates and returns (Some("none"), true). This adds reasoning_effort: "none" to the request body and emits a misleading "reasoning effort disabled" warning even though no reasoning effort was requested. For reasoning models with an empty effort ladder (where reasoning_effort_for returned None), this could cause an API rejection since the model doesn't accept the parameter at all.

🐛 Proposed fix: add `effort.is_some()` to the guard condition
 fn compatible_reasoning_effort(
     api_mode: ApiMode,
     model: &str,
     has_tools: bool,
     effort: Option<&'static str>,
 ) -> (Option<&'static str>, bool) {
     let needs_luna_guard = api_mode == ApiMode::ChatCompletions
         && has_tools
         && model.to_ascii_lowercase().contains("luna")
-        && effort != Some("none");
+        && effort.is_some()
+        && effort != Some("none");
     if needs_luna_guard {
         (Some("none"), true)
     } else {
         (effort, false)
     }
 }
📝 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
fn compatible_reasoning_effort(
api_mode: ApiMode,
model: &str,
has_tools: bool,
effort: Option<&'static str>,
) -> (Option<&'static str>, bool) {
let needs_luna_guard = api_mode == ApiMode::ChatCompletions
&& has_tools
&& model.to_ascii_lowercase().contains("luna")
&& effort != Some("none");
if needs_luna_guard {
(Some("none"), true)
} else {
(effort, false)
}
}
fn compatible_reasoning_effort(
api_mode: ApiMode,
model: &str,
has_tools: bool,
effort: Option<&'static str>,
) -> (Option<&'static str>, bool) {
let needs_luna_guard = api_mode == ApiMode::ChatCompletions
&& has_tools
&& model.to_ascii_lowercase().contains("luna")
&& effort.is_some()
&& effort != Some("none");
if needs_luna_guard {
(Some("none"), true)
} else {
(effort, false)
}
}
🤖 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 `@provider-openai/src/stream_fn.rs` around lines 24 - 40, Update
compatible_reasoning_effort so needs_luna_guard only activates when
effort.is_some() and the requested effort is not "none"; preserve the existing
ChatCompletions, tools, and Luna model checks, and continue returning the
original None unchanged when no effort was requested.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant