Skip to content

feat: Add OpenAI Responses API support and refactor protocol handling#363

Merged
lizhengfeng101 merged 16 commits into
alibaba:mainfrom
stay-foolish-forever:feat/response-api-support
Jul 15, 2026
Merged

feat: Add OpenAI Responses API support and refactor protocol handling#363
lizhengfeng101 merged 16 commits into
alibaba:mainfrom
stay-foolish-forever:feat/response-api-support

Conversation

@stay-foolish-forever

Copy link
Copy Markdown
Contributor

Description

This PR adds support for the OpenAI Responses API (/v1/responses) as a third LLM protocol, enabling code review with GPT-5.x / o-series models that use the Responses API rather than Chat Completions.

New protocol: openai-responses

Sits alongside the existing anthropic and openai (Chat Completions) protocols. Users can now select between all three via configuration.

  • New OpenAIResponsesClient (stateless replay):
    • System messages are mapped to instructions; tool calls become function_call items keyed by CallID so the agent loop pairs results correctly.
    • store=false (privacy); prompt_cache_key derived from sha256(instructions)[:32].
  • Protocol handling refactor (backward-compatible):
    • Introduces NormalizeProtocol / ValidateProtocol helpers in internal/llm/protocol.go.
    • The registry uses canonical protocol constants; the resolver normalizes inputs everywhere.
    • llm.protocol config field + OCR_LLM_PROTOCOL env var take priority over the legacy use_anthropic / OCR_USE_ANTHROPIC (kept working via mirroring).
    • anthropic-vertex is rejected with a friendly "not yet implemented" message.
  • TUI / CLI plumbing: the provider TUI exposes all three protocols (Custom & Manual modes); provider_tui_test.go added.
  • Docs: protocol reference, config examples, env var table, and Responses API notes (store=false caching caveat, cache key derivation) updated across en/zh-CN/ko-KR/ja-JP/ru-RU READMEs.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Refactoring (no functional changes)
  • Documentation update
  • CI / Build / Tooling

How Has This Been Tested?

  • make test passes locally
  • Manual testing (describe below)
    Tested using anthropic, openai, openai-responses endpoints to ensure that all the three protocol functions correctly and previous behavior do not change

Checklist

  • My code follows the project's coding style (go fmt, go vet)
  • I have performed a self-review of my code
  • I have added tests that prove my fix is effective or my feature works
  • New and existing unit tests pass locally with my changes
  • I have updated the documentation accordingly (if applicable)
  • I have signed the CLA

Related Issues

Add `openai-responses` as a third LLM protocol alongside `anthropic` and
`openai-chat-completions`, enabling code review via the OpenAI Responses
API (/v1/responses) for GPT-5.x / o-series models.

Protocol naming refactor (backward-compatible):
- Canonicalize "openai" -> "openai-chat-completions" (alias still accepted)
- Add NormalizeProtocol / ValidateProtocol / IsAnthropicProtocol helpers
- Registry uses canonical constants; resolver normalizes everywhere

New OpenAIResponsesClient (stateless replay, per DESIGN_STATE_CACHE_PHASE.md):
- system messages -> Instructions; tool calls -> function_call items keyed
  by CallID so the agent loop pairs results correctly
- store=false (privacy); prompt_cache_key = sha256(instructions)[:32]
- Phase fields (commentary/final_answer) dropped with TODO for gpt-5.3-codex+

Config plumbing:
- llm.protocol field + OCR_LLM_PROTOCOL env (priority over use_anthropic /
  OCR_USE_ANTHROPIC); TUI exposes all three protocols in Custom & Manual
- anthropic-vertex rejected with friendly "not yet implemented" message

Docs: protocol reference, config examples, env var table, and Responses API
notes (store=false caching caveat, cache key derivation, Phase TODO) updated
across en/zh-CN/ko-KR/ja-JP/ru-RU READMEs.
…quest.CacheKey

Replace per-turn sha256 computation inside buildResponsesParams with a
precomputed cache key that callers compute once per session and pass
through ChatRequest.CacheKey (json:"-"). The key now incorporates the
first user message alongside instructions, so different files under
review land in distinct cache buckets — the previous instructions-only
key was identical across all files.

Changes:
- ChatRequest gains CacheKey string field (json:"-", zero impact on
  Chat Completions / Anthropic clients which never read it)
- New llm.ComputeCacheKey helper: sha256(instructions + "\x00" +
  firstUser)[:32]
- responses_client.go: reads req.CacheKey directly, removes promptCacheKey
  function and first-user-message scanning
- loop.go: RunPerFile computes cacheKey once before the loop, reuses
  every turn
- All 8 remaining call sites (agent, scan, relocation, compression,
  llm_cmd) compute once at request construction
- Update PLAN_RESPONSES_SUPPORT.md and DESIGN_STATE_CACHE_PHASE.md to
  reflect the precomputed scheme
- Update tests: passthrough tests for client, dedicated TestComputeCacheKey
…ssion ID for cache key

Two changes to maximize backward compatibility and simplify the design:

1. Protocol naming: revert ProtocolOpenAIChatCompletions value from
   "openai-chat-completions" back to "openai". Old config files with
   protocol: "openai" are now identical to what new configs write —
   zero behavioral difference. The alias direction in NormalizeProtocol
   is reversed: "openai-chat-completions" -> "openai" (for configs
   written during this branch's testing phase only).

2. Cache key: replace content-based sha256 hash (ComputeCacheKey) with a
   random UUID session ID. The agent loop generates one UUID per file in
   RunPerFile and passes it via ChatRequest.SessionID; the Responses
   client uses it as prompt_cache_key. Single-turn call sites no longer
   set a cache key (no multi-turn caching benefit). This removes the
   need to scan messages or compute hashes, and eliminates collision
   risk between files with similar content.

   ChatRequest.CacheKey is renamed to SessionID to reflect its actual
   semantic — a per-session identifier that the Responses client
   repurposes as prompt_cache_key.

Also updates PLAN_RESPONSES_SUPPORT.md, all 5 README translations,
test expectations, and promotes google/uuid to a direct dependency.
- provider_cmd: clear stale use_anthropic when switching to openai-responses
- resolver: validate preset protocol with ValidateProtocol for consistency
- responses_client: swap usage mapping to resolveUsage-first (matches OpenAIClient)
- responses_client: map failed/cancelled statuses to 'error' finish reason
- usage_resolver: add Responses API field paths (input_tokens, output_tokens,
  input_tokens_details.cached_tokens)
- add tests for all four fixes
- config_cmd: 'ocr config set llm.protocol' now mirrors use_anthropic
  (anthropic -> true, OpenAI family -> false) for backward compat with
  older binaries that predate llm.protocol
- provider_cmd: openai-responses now sets use_anthropic=false instead of
  nil, so older binaries fall back to the OpenAI family rather than
  wrongly defaulting to anthropic
- update tests for both write paths
- config_cmd: 'ocr config set llm.use_anthropic' now mirrors protocol
  (true -> anthropic, false -> openai) so the two fields never disagree,
  matching the reverse llm.protocol mirroring added previously
- without this, setting use_anthropic=true while protocol=openai-responses
  left a contradictory config that misled older binaries into using the
  anthropic protocol against an OpenAI endpoint
- extend tests to cover both values and stale-protocol overwrite
The comment claimed unknown values are 'returned unchanged', but the
default branch lowercases and trims them (corroborated by the
'gRPC -> grpc' test). Update the wording to describe the actual
behavior so callers aren't misled about round-trip fidelity.
# Conflicts:
#	internal/llm/client_test.go
@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

🔍 OpenCodeReview found 8 issue(s) in this PR.

  • ✅ Successfully posted inline: 5 comment(s)
  • 📝 In summary (no line info): 1 comment(s)
  • ❌ Failed to post inline: 2 comment(s)

📄 internal/llm/resolver.go

⚠️ GitHub could not post this as an inline comment: No line information provided

Minor: NormalizeProtocol computes strings.ToLower(strings.TrimSpace(raw)) twice — once as the switch expression and again in the default case. Consider extracting it into a local variable to avoid redundant computation and improve readability.

func NormalizeProtocol(raw string) string {
    normalized := strings.ToLower(strings.TrimSpace(raw))
    switch normalized {
    case "":
        return ""
    case ProtocolAnthropic:
        return ProtocolAnthropic
    // ...
    default:
        return normalized
    }
}

📄 internal/llm/providers.go (L37-L37)

⚠️ GitHub could not post this as an inline comment: Unprocessable Entity: "Line could not be resolved"

Inconsistency: The "edenai" provider still uses the hardcoded string literal "openai" instead of the ProtocolOpenAIChatCompletions constant. All other providers in this registry have been updated to use the constant. While the values are currently identical, using the constant ensures consistency and prevents future divergence if the canonical value ever changes.

💡 Suggested Change

Before:

		Protocol:    "openai",

After:

		Protocol:    ProtocolOpenAIChatCompletions,

📄 cmd/opencodereview/provider_cmd.go (L139-L139)

⚠️ GitHub could not post this as an inline comment: Unprocessable Entity: "Line could not be resolved"

Minor inconsistency: this prints result.protocol (the raw value from the TUI) rather than the normalized protocol variable that was actually saved to config. While in practice they'll be identical (since result.protocol comes from cpProtocols[] which already holds canonical constants), using the normalized variable here would be more consistent and defensive against future changes where the TUI might pass non-canonical values.

💡 Suggested Change

Before:

	fmt.Printf("Protocol: %s\n", result.protocol)

After:

	fmt.Printf("Protocol: %s\n", protocol)

Comment on lines +339 to 342
Supported keys: provider, model, providers.<name>.<field>, custom_providers.<name>.<field>, mcp_servers.<name>.<field>, llm.url, llm.auth_token, llm.auth_header, llm.model, llm.protocol, llm.use_anthropic, llm.extra_body, llm.extra_headers, language, telemetry.enabled, telemetry.exporter, telemetry.otlp_endpoint, telemetry.content_logging
Provider fields: api_key, url, protocol, model, models, auth_header, extra_body, extra_headers
Protocol values: anthropic, openai, openai-responses
MCP server fields: command, args, env, tools, setup`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor consistency issue: The help text here now documents "Protocol values: anthropic, openai, openai-responses", but the error message in config_cmd.go (line 401) lists llm.protocol as a supported key without including the valid protocol values. Consider adding the same "Protocol values" line to the error message in setConfigValue() so users who type an invalid key or value see the same guidance in both places.

Comment thread internal/llm/protocol.go Outdated
Comment on lines +44 to +45
default:
return strings.ToLower(strings.TrimSpace(raw))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor: The default branch redundantly recomputes strings.ToLower(strings.TrimSpace(raw)), which was already computed as the switch expression. Extract the normalized value into a local variable to avoid the duplicate work and make the intent clearer:

func NormalizeProtocol(raw string) string {
    normalized := strings.ToLower(strings.TrimSpace(raw))
    switch normalized {
    case "":
        return ""
    case ProtocolAnthropic:
        return ProtocolAnthropic
    // ...
    default:
        return normalized
    }
}

This also prevents a subtle class of future bugs where someone modifies the switch expression but forgets the default branch uses its own independent transformation.

Comment on lines +367 to +371
if b {
cfg.Llm.Protocol = llm.ProtocolAnthropic
} else {
cfg.Llm.Protocol = llm.ProtocolOpenAIChatCompletions
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Potential data loss: When use_anthropic is set to false, the protocol is unconditionally overwritten to openai (chat completions). However, if the user previously configured protocol = "openai-responses", this mirroring silently downgrades it to "openai".

Since the resolver already prioritizes Protocol over UseAnthropic, consider only updating Protocol when it is currently empty or matches one of the two legacy values (anthropic/openai). Alternatively, skip the protocol overwrite entirely when use_anthropic is being set, since Protocol takes priority in the resolver anyway. At minimum, avoid overwriting openai-responses with openai.

Suggestion:

Suggested change
if b {
cfg.Llm.Protocol = llm.ProtocolAnthropic
} else {
cfg.Llm.Protocol = llm.ProtocolOpenAIChatCompletions
}
// Only mirror to protocol if protocol is unset or already in the
// legacy pair (anthropic / openai). Avoid clobbering openai-responses.
if cfg.Llm.Protocol == "" || cfg.Llm.Protocol == llm.ProtocolAnthropic || cfg.Llm.Protocol == llm.ProtocolOpenAIChatCompletions {
if b {
cfg.Llm.Protocol = llm.ProtocolAnthropic
} else {
cfg.Llm.Protocol = llm.ProtocolOpenAIChatCompletions
}
}

Comment thread internal/llm/responses_client.go Outdated
Comment on lines +71 to +74
// sdkBaseURL strips the /responses suffix so the SDK can re-append it.
func (c *OpenAIResponsesClient) sdkBaseURL() string {
return strings.TrimSuffix(strings.TrimRight(c.cfg.URL, "/"), "/responses")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Dead code: this method is defined but never called. The constructor at line 33 computes the SDK base URL using a local variable instead. Either remove this method or use it in the constructor for consistency. Note also that this method applies TrimRight before TrimSuffix, while the constructor does not — if someone later switches to calling this method, the behavior could subtly differ for URLs with trailing slashes.

Comment thread internal/llm/responses_client.go Outdated
cfg.Timeout = 5 * time.Minute
}
ensureResponsesEndpoint(&cfg)
sdkBaseURL := strings.TrimSuffix(cfg.URL, "/responses")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Inconsistent URL normalization compared to NewOpenAIClient. In NewOpenAIClient (client.go:309), the SDK base URL is derived as strings.TrimSuffix(strings.TrimRight(cfg.URL, "/"), "/chat/completions") — note the TrimRight before TrimSuffix. Here, only TrimSuffix is used. While ensureResponsesEndpoint guarantees no trailing slash at this point, matching the defensive pattern from NewOpenAIClient would make the code more robust against future refactoring.

Suggestion:

Suggested change
sdkBaseURL := strings.TrimSuffix(cfg.URL, "/responses")
sdkBaseURL := strings.TrimSuffix(strings.TrimRight(cfg.URL, "/"), "/responses")

- config: preserve openai-responses when setting llm.use_anthropic=false
  (only mirror to openai when protocol is unset or a legacy anthropic/openai)
- config: add Protocol values guidance to unknown-key error message
- protocol: extract normalized local var in NormalizeProtocol
- responses_client: align SDK base URL trimming with NewOpenAIClient
- responses_client: drop unused test-only sdkBaseURL method
- providers: edenai now uses ProtocolOpenAIChatCompletions like the rest
  of the registry instead of the "openai" string literal
- provider_cmd: print the normalized protocol variable (what is actually
  saved) instead of the raw TUI value
Comment on lines +81 to +86
var opts []openaiopt.RequestOption
for k, v := range c.cfg.ExtraBody {
opts = append(opts, openaiopt.WithJSONSet(k, v))
}

sdkResp, err := c.sdk.Responses.New(ctx, params, opts...)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

When existing provider config contains extra_body: {"stream": true},these options are still passed to Responses.New.The API then returns SSE while New expects a regular JSON response, so every completion fails.

Comment on lines +86 to +91
sdkResp, err := c.sdk.Responses.New(ctx, params, opts...)
if err != nil {
return nil, err
}

return c.mapResponsesResponse(sdkResp), nil

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The Responses API can return HTTP 200 with status failed/cancelled, or queued/in_progress when background mode is enabled, so Responses.New can have a nil Go error here. This code converts those into a normal ChatResponse; callers do not inspect FinishReason, causing ocr llm test to report success and the review loop to retry or execute output instead of surfacing or polling the response.

…es client

Address two PR review comments on OpenAI Responses API support:

1. extra_body.stream=true was forwarded to Responses.New, making the API
   return SSE while the SDK expects JSON and breaking every call. Skip the
   'stream' key (like OpenAIClient treats it as a non-forwarded key) while
   still forwarding other extra_body entries.

2. The Responses API returns HTTP 200 even for failed/cancelled (terminal)
   and queued/in_progress (background) states, so the SDK reports nil error.
   Surface these as real errors so callers branching on err != nil (ocr llm
   test, review loop) fail instead of treating a dead response as success.

Add table-driven tests covering both fixes.

@lizhengfeng101 lizhengfeng101 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@lizhengfeng101 lizhengfeng101 merged commit f58b0f2 into alibaba:main Jul 15, 2026
2 checks passed
lizhengfeng101 pushed a commit that referenced this pull request Jul 15, 2026
Update features.feat3Desc and docs.configManualCustomNote across all
three language files (en, zh, ja) to include the openai-responses
protocol added in PR #363.

Closes #372
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.

3 participants