Skip to content

feat(llm): add openai-responses provider (OpenAI Responses API) - #3121

Merged
nicoloboschi merged 1 commit into
mainfrom
feat/openai-responses-provider
Aug 3, 2026
Merged

feat(llm): add openai-responses provider (OpenAI Responses API)#3121
nicoloboschi merged 1 commit into
mainfrom
feat/openai-responses-provider

Conversation

@nicoloboschi

@nicoloboschi nicoloboschi commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

What

Adds a new LLM provider openai-responses that calls the OpenAI Responses API (client.responses.create/v1/responses) instead of chat/completions.

Why

Reasoning models such as gpt-5.6-terra reject reasoning_effort combined with function tools on /v1/chat/completions:

reasoning_effort="low"   -> HTTP 400
reasoning_effort absent  -> HTTP 400
reasoning_effort="none"  -> succeeds

Reflect is a tool-calling search loop, so on chat/completions the only non-400 setting ("none") also strips reasoning from reflect's final synthesis (single effort knob per op). The Responses API models the chain-of-thought as a first-class reasoning item, so reasoning and function tools coexist — reflect's search loop runs with a real reasoning_effort and still calls tools.

Design

OpenAIResponsesLLM is a standalone LLMInterface implementation. It is OpenAI-only and deliberately does not subclass the multi-vendor OpenAICompatibleLLM (chat/completions) class — so it can never route through chat.completions and carries none of the groq/ollama/deepseek special-casing. It reuses only a few provider-agnostic pure helpers (text-tag stripping, quota-defer parsing). The shared chat retry loop is untouched (no regression risk to the ~13 existing OpenAI-compatible providers).

Translations:

  • chat messages → Responses input items (assistant tool_callsfunction_call; role="tool"function_call_output keyed by call_id)
  • nested {"type":"function","function":{…}} tools → flattened {"type":"function","name":…,"parameters":…}
  • flat reasoning_effortreasoning={"effort": …}
  • response_formattext={"format": {…}} (strict json_schema, or the soft schema-in-prompt + json_object fallback)
  • reads response.output_text + function_call items from response.output; usage from usage.input_tokens/output_tokens (reasoning split out of visible output)

Conversation is replayed statelessly each turn (store=False, no previous_response_id); server-side reasoning reuse across turns is a future optimization.

Generic LLM config flags

Honors extra_body, timeout, and per-call temperature / max_completion_tokens / max_retries. It additionally wires two generic flags the chat/completions path drops:

  • openai_service_tier → the native Responses service_tier param (e.g. flex)
  • default_headers → the OpenAI SDK client (proxy / request-tracing)

OpenAI-compatible endpoints

Like the openai (Chat Completions) provider, openai-responses honors a custom HINDSIGHT_API_LLM_BASE_URL, so any OpenAI-compatible endpoint that exposes /v1/responses (gateways, Azure-style deployments) works the same way as a Chat Completions one.

Docs

  • Models page (developer/models.mdx): new provider in the grid, a dedicated "OpenAI Responses API" tip, and the OpenAI-Compatible tip now states explicitly that the openai provider uses Chat Completions while openai-responses uses the Responses API — both usable against compatible custom endpoints.
  • configuration.md provider list + Chat-Completions/Responses compatible-endpoint examples.
  • .env.example (+ embed template sync); docs skill mirror regenerated.

Wiring

  • Provider registration + factory dispatch (llm_wrapper.py).
  • PROVIDER_DEFAULT_MODELS["openai-responses"] = "gpt-5.6".
  • openai floor bumped to >=1.66.0 for the Responses API surface.

Tests

tests/test_openai_responses_provider.py (19 tests) mock responses.create and cover registration/wiring, request shaping (reasoning object, temperature omission for reasoning models, max_output_tokens, strict/soft structured output, truncation → OutputTooLongError), message-history translation, tool-choice mapping, response parsing, and the generic-flag wiring (extra_body, service_tier, default_headers). The load-bearing test pins that reasoning and tools are sent together on the tool path.

Live validation (real gpt-5.6-terra)

  • plain + reasoning, strict structured output ✅
  • reasoning + tools together (reasoning_tokens > 0 in the same request as a function call) ✅
  • stateless function_call / function_call_output replay → final answer
  • full run_reflect_agent end-to-end (stubbed retrieval, no DB): the agent drove the Responses tool loop through recall to a correct synthesized answer ✅

Usage

export HINDSIGHT_API_LLM_PROVIDER=openai-responses
export HINDSIGHT_API_LLM_MODEL=gpt-5.6            # or gpt-5.6-terra
export HINDSIGHT_API_LLM_REASONING_EFFORT=high    # now rides alongside tools

🤖 Generated with Claude Code

@nicoloboschi
nicoloboschi force-pushed the feat/openai-responses-provider branch 2 times, most recently from 3b48773 to cc4b14b Compare August 3, 2026 09:42
Add a provider that talks exclusively to the OpenAI Responses API
(`client.responses.create` → `/v1/responses`) — never chat/completions.

Motivation: reasoning models such as gpt-5.6-terra reject `reasoning_effort`
combined with function tools on `/v1/chat/completions` (HTTP 400 unless
`reasoning_effort="none"`, see #2983). Reflect is a tool-calling search loop, so
that constraint forces the whole reflect operation — including the final
synthesis — to run with reasoning disabled. The Responses API models the
chain-of-thought as a first-class reasoning item, so reasoning and tools coexist;
reflect's search loop can now run with a real reasoning effort.

`OpenAIResponsesLLM` is a standalone `LLMInterface` implementation (OpenAI-only;
it deliberately does NOT subclass the multi-vendor chat/completions provider, so
it can never route through `chat.completions` and carries none of the
groq/ollama/deepseek special-casing). It reuses only provider-agnostic pure
helpers (text cleanup, quota-defer parsing). It translates the engine's
chat-shaped inputs:
- chat messages → `input` items (assistant `tool_calls` → `function_call`,
  `role="tool"` → `function_call_output` keyed by `call_id`),
- nested `{"type":"function","function":{...}}` tools → flattened
  `{"type":"function","name":...,"parameters":...}`,
- flat `reasoning_effort` → a `reasoning={"effort": ...}` object,
- `response_format` → `text={"format": {...}}` (strict json_schema or the soft
  schema-in-prompt + json_object fallback),
- reads `response.output_text` + `function_call` items from `response.output`.

Generic LLM config flags are honored: `extra_body`, `timeout`, per-call
`temperature`/`max_completion_tokens`/`max_retries`. It also wires two flags the
chat/completions path drops — `openai_service_tier` (as the native Responses
`service_tier`) and `default_headers` (on the SDK client).

The conversation is replayed statelessly each turn (`store=False`, no
`previous_response_id`); server-side reasoning reuse across turns is left as a
future optimization.

Wiring: provider registration + dispatch, `PROVIDER_DEFAULT_MODELS` default
(`gpt-5.6`), docs + `.env.example` (+ embed template sync), and an `openai`
floor bump to `>=1.66.0` for the Responses API surface. Unit tests mock
`responses.create` (incl. the generic-flag wiring) and pin that reasoning + tools
are sent together on the tool path — the combination chat/completions rejects.

Validated live against real gpt-5.6-terra: plain + reasoning, strict structured
output, reasoning+tools together with a stateless function_call replay, and a
full run_reflect_agent end-to-end (stubbed retrieval, no DB) — tools drove to a
correct synthesized answer.
@nicoloboschi
nicoloboschi force-pushed the feat/openai-responses-provider branch from cc4b14b to abcac65 Compare August 3, 2026 10:17
@nicoloboschi
nicoloboschi merged commit 55883dc into main Aug 3, 2026
103 of 104 checks passed
@nicoloboschi
nicoloboschi deleted the feat/openai-responses-provider branch August 3, 2026 12:34
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