Skip to content

[Bug]: service_factory.py hardcodes reasoning_effort="minimal" for every gpt-5* model — the API rejects it, the 400 is swallowed, and the agent never replies #721

Description

@AAlexxis222

Deployment mode: cloud-hosted (app.dograh.com) where observed; the responsible code is identical in the self-hosted main (api/services/pipecat/service_factory.py:972-977 as of b3bb7328).

Environment: app.dograh.com, backend v1.45.0, org on model-configurations v2
(source: "organization_v2"), BYOK mode pipeline, LLM provider openai, valid OpenAI key.

What happens

With model: gpt-5.6-luna, every turn dies silently: the user message is accepted
(rtf-user-transcription event) but no assistant reply ever follows — no error event, and
usage_info.llm stays {}. Example runs (workflow 11090): 659381, 659382, 659470.
Same workflow and config with gpt-4.1-mini: replies normally (run 659384).

Root cause

1. Where the value comes from (source, v1.45.0)

api/services/pipecat/service_factory.py:972-979:

if "gpt-5" in model:
    return OpenAILLMService(
        api_key=api_key,
        settings=OpenAILLMSettings(
            model=model,
            extra={"reasoning_effort": "minimal", "verbosity": "low"},
        ),
        **kwargs,
    )

Two properties of this branch matter:

  • It is a substring match on "gpt-5", so it captures every current and future gpt-5.x
    model, including gpt-5.6-luna.
  • The value is hardcoded, not configurable. There is no way for an operator to override it
    from the workflow or the org configuration.

2. Why the value is wrong, per OpenAI's own documentation

Two independent problems, both documented. Neither is our interpretation — these are quotes:

(a) minimal is not a supported value for these models. The model page for gpt-5.6-luna
states: "Reasoning.effort supports: none, low, medium (default), high, xhigh, and max."
(https://developers.openai.com/api/docs/models/gpt-5.6-luna.md; identical wording on
gpt-5.6-sol and gpt-5.6-terra; gpt-5.5 lists the same set minus max.) minimal survives in
the generic enum of the reasoning guide, but is absent from every current model page.

(b) Even a valid effort breaks tool calling on this endpoint. From the Responses migration
guide, section Responses benefits → Additional differences, second bullet:

Reasoning models have a richer experience in the Responses API with improved tool usage.
Starting with GPT-5.4, Chat Completions does not support tool calling with reasoning_effort
values other than none.

(https://developers.openai.com/api/docs/guides/migrate-to-responses.md) OpenAI applies this in
their own sample code on that page: the Chat Completions example that uses function tools passes
reasoning_effort: :none.

So changing "minimal" to "medium" would not fix this. On /v1/chat/completions with tools,
the only accepted value is none; anything else requires /v1/responses.

The sharpest form of the bug: medium is the documented default for gpt-5.6-luna. If
service_factory.py simply did not send the field at all, the model would fall back to
medium — which on our own bench is the best-performing setting (34/40 vs 19/40 at none).
The hardcode is strictly worse than sending nothing.

3. Confirmed on the wire

We pointed the leg's base_url at a logging relay and captured the exact upstream exchange. The
pipeline sends reasoning_effort: "minimal" together with 2 function tools to
/v1/chat/completions, and OpenAI rejects it twice over:

  1. 400 unsupported_value — gpt-5.6 models dropped "minimal"; supported values are now
    none / low / medium / high / xhigh.
  2. After mapping minimal→low: 400"Function tools with reasoning_effort are not supported for
    gpt-5.6-luna in /v1/chat/completions. To use function tools, use /v1/responses or set
    reasoning_effort to 'none'."

With the relay rewriting reasoning_effort"none", the same pipeline works end-to-end and the
agent replies normally (run 659477).

Note on evidence: the two quotes in §2 are OpenAI's published rules; the 400 above is our
first-hand runtime observation that those rules bite, with the model named. They corroborate each
other and are not the same kind of evidence — we keep them separate deliberately.

4. Why it is invisible

The 400 is swallowed: no error event on the run, no annotation, usage_info.llm empty. From the
operator's side it is indistinguishable from "the model decided not to answer". This is what made
the bug expensive to diagnose — several days, and it needed a proxy to see at all.

Suggested fixes

Immediate (one line): stop sending the field. Dropping reasoning_effort from the gpt-5*
branch makes each model use its own documented default (medium for the 5.6 family), which is both
valid and — on our bench — the best-performing setting. If an explicit value is preferred on
/v1/chat/completions with tools, the only accepted one is "none".

Proper: expose reasoning_effort as a configuration knob on the OpenAI LLM leg rather than a
constant. It is not a cosmetic setting: on our own bench (40 cases, same workflow, same prompt,
gpt-5.6-luna) the effort axis moved the score from 19/40 at none to 34/40 at medium.
A hardcoded value removes the single most consequential dial on the model.

Structural: the fix already exists inside your own fork, unused.

pipecat/src/pipecat/services/openai/responses/llm.py provides OpenAIResponsesLLMService with a
real reasoning.effort setting (lines 93, 126, 528), which is the documented way to combine
function tools with actual reasoning effort. As of v1.45.0 nothing under api/ imports it
service_factory.py only ever constructs OpenAILLMService (chat/completions).

Honest caveat: we verified that the service exists in the fork and that Dograh does not use it.
We have not executed a swap, so we cannot claim it is a drop-in — the tools adapter and the
context format on the Responses path would need checking. We are flagging the asset, not
promising the patch.

Observability (independent of the above): surface upstream 4xx errors to the run as an error
event or annotation instead of a silent no-reply. Even with the effort bug fixed, any future
provider-side rejection would be equally invisible.

Adjacent findings in the same file (offered, not demanded)

While auditing v1.45.0 we noticed the same hardcoding pattern elsewhere in
api/services/pipecat/service_factory.py, which suggests a general design question rather than a
one-off:

  • temperature=0.1 is fixed for 7 providers (:983, :989, :998, :1005, :1012, :1020, :1052).
  • stability=0.8 and similarity_boost=0.75 are fixed for the ElevenLabs TTS leg (:631, :633).
  • For the Deepgram Flux STT leg, eot_timeout_ms=3000, eot_threshold=0.7 and
    eager_eot_threshold=0.5 are fixed. Two of those differ from Deepgram's own documented defaults
    (eot_timeout_ms 5000; eager_eot_threshold unset, i.e. eager mode off), so Dograh ships a
    more aggressive turn cut-off than Deepgram's baseline and enables eager mode by default. We see
    this as user-visible: it is the shape of "the agent interrupts me while I spell out a number".

We would be glad to contribute a PR that turns these into configurable fields with the current
values as defaults, so behaviour is unchanged for existing users.

Related (second issue, happy to file separately)

Per-workflow workflow_configurations model overrides (v1-style, written via PUT /workflow/{id})
persist but are ignored by the runtime once the org is on model-configurations v2 — and the legacy
PUT /api/v1/user/configurations/user returns 200 while writing nothing. If v1 config surfaces are
dead post-migration, a 4xx or a deprecation response would save integrators days of debugging.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions