Skip to content

fix(openai-responses): mirror web_search_call query into queries - #934

Closed
DevMello wants to merge 1 commit into
lidge-jun:devfrom
DevMello:fix/930-web-search-call-queries
Closed

fix(openai-responses): mirror web_search_call query into queries#934
DevMello wants to merge 1 commit into
lidge-jun:devfrom
DevMello:fix/930-web-search-call-queries

Conversation

@DevMello

@DevMello DevMello commented Aug 3, 2026

Copy link
Copy Markdown

Summary

Fixes #930. Replayed web_search_call history carries a singular action.query, but DeepSeek's Responses route requires queries, so one web search made every later turn in the conversation fail with 400. The key path now adds queries next to the singular; forward mode is untouched.

Verification

  • New tests in tests/openai-responses-passthrough.test.ts: the mirror, forward mode staying untouched, and pass-through of everything else.
  • bun run test, typecheck, lint:gui, privacy:scan.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Summary by CodeRabbit

  • Bug Fixes

    • Improved compatibility with DeepSeek for replayed web search actions.
    • Preserved existing hosted search request formats and unrelated input data.
  • Tests

    • Added regression coverage for search actions with singular or multiple query fields, actionless calls, and forward-mode requests.

DeepSeek's native Responses route deserializes the plural `queries`
on replayed web_search_call search actions, so one web search made
every later turn in the conversation fail with 400 "missing field
`queries`". The key path now mirrors the recorded singular into
`queries` (additive; DeepSeek ignores the unknown singular). Forward
mode keeps the canonical hosted shape.

Fixes lidge-jun#930
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The Responses adapter now normalizes replayed web_search_call actions for non-forward providers. It adds queries: [query] when only query exists. Tests cover DeepSeek key-auth requests, forward mode, existing queries, actionless calls, and unrelated input.

Changes

Web search replay normalization

Layer / File(s) Summary
Normalize replayed search actions
src/adapters/openai-responses.ts
Lines 139-158 add a copy-on-write helper that adds queries: [query] to eligible web_search_call actions. Existing queries values and unrelated items remain unchanged.
Apply and validate normalization
src/adapters/openai-responses.ts, tests/openai-responses-passthrough.test.ts
Line 1163 applies normalization to non-forward requests before call-ID repair and sanitization. Lines 107-159 add regression coverage for key-auth and forward-mode behavior.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: lidge-jun, ingwannu, wibias

🚥 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 clearly describes mirroring the web search query into the required queries field.
Linked Issues check ✅ Passed The adapter normalization and regression tests address Issue #930 by adding queries for replayed DeepSeek web_search_call actions while preserving forward mode.
Out of Scope Changes check ✅ Passed The changes are limited to the requested adapter normalization and focused regression tests for replayed web_search_call compatibility.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@github-actions github-actions Bot added the bug Something isn't working label Aug 3, 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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/adapters/openai-responses.ts`:
- Line 1163: Gate the normalizeWebSearchCallActions call in the Responses
adapter behind an explicit provider capability, enabling it only for providers
that support action.queries, including DeepSeek as documented. Keep custom
key-auth providers and volcengine-agent-plan unmodified, and add a regression
test covering the custom-provider path to ensure no unsupported field is added.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6ae38226-ff5b-4262-a250-204e49133d64

📥 Commits

Reviewing files that changed from the base of the PR and between d74b73f and 8a72834.

📒 Files selected for processing (2)
  • src/adapters/openai-responses.ts
  • tests/openai-responses-passthrough.test.ts

parsed._openAiVirtualSelectedModelId,
);
outBody = normalizeImageGenClientTools(outBody);
outBody = normalizeWebSearchCallActions(outBody);

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

# Map the provider configuration type and all Responses adapter call sites.
ast-grep outline src --items all --type interface,type --match OcxProviderConfig
ast-grep run --pattern 'createResponsesPassthroughAdapter($PROVIDER)' --lang ts src tests

# Inspect configured key-auth Responses providers and their endpoint overrides.
rg -n -C 4 'adapter:\s*"openai-responses"|authMode:\s*"key"|responsesPath|baseUrl' \
  --glob '*.ts' --glob '*.json' --glob '*.yaml' --glob '*.yml' src tests

Repository: lidge-jun/opencodex

Length of output: 199


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== candidate file outline =='
ast-grep outline src/adapters/openai-responses.ts --items all 2>/dev/null | sed -n '1,220p'

echo '== target implementation =='
sed -n '1080,1195p' src/adapters/openai-responses.ts

echo '== provider configuration symbols =='
rg -n -C 5 'OcxProviderConfig|createResponsesPassthroughAdapter|normalizeWebSearchCallActions|openai-responses' src tests \
  --glob '*.ts' --glob '*.tsx' --glob '*.json' --glob '*.yaml' --glob '*.yml' | sed -n '1,320p'

echo '== provider configuration files =='
git ls-files | rg '(^|/)(config|providers?|models?|adapters?)(/|\.|$)|openai|deepseek' | sed -n '1,240p'

Repository: lidge-jun/opencodex

Length of output: 44253


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== normalization function =='
sed -n '130,175p' src/adapters/openai-responses.ts

echo '== provider type definition =='
rg -n -C 25 'interface OcxProviderConfig|type OcxProviderConfig' src/types.ts src --glob '*.ts' | sed -n '1,220p'

echo '== all openai-responses configurations and registry entries =='
rg -n -C 8 'openai-responses' src tests docs-site \
  --glob '*.ts' --glob '*.tsx' --glob '*.json' --glob '*.yaml' --glob '*.yml' --glob '*.md' | sed -n '1,420p'

echo '== web-search normalization tests and callers =='
rg -n -C 10 'normalizeWebSearchCallActions|web_search_call|queries' src tests \
  --glob '*.ts' --glob '*.tsx' | sed -n '1,500p'

Repository: lidge-jun/opencodex

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== complete Responses-related provider fields =='
sed -n '913,1010p' src/types.ts

echo '== registry entries whose adapter or model wire can select Responses =='
rg -n -C 12 'id: "deepseek"|adapter: "openai-responses"|modelAdapters|responsesPath|statelessResponses' src/providers src/config.ts src/router.ts \
  --glob '*.ts' | sed -n '1,520p'

echo '== adapter construction and wire selection =='
rg -n -C 14 'createResponsesPassthroughAdapter|resolveWireProtocolOverride|adapter === "openai-responses"|case "openai-responses"' src \
  --glob '*.ts' | sed -n '1,520p'

echo '== focused test files and relevant assertions =='
fd -t f -i 'responses|adapter' tests | sort
rg -l 'DeepSeek|deepseek|web_search_call|queries' tests --glob '*.ts' | sort

Repository: lidge-jun/opencodex

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== every registry Responses entry =='
python3 - <<'PY'
from pathlib import Path
p = Path("src/providers/registry.ts")
lines = p.read_text().splitlines()
for i, line in enumerate(lines):
    if 'adapter: "openai-responses"' in line:
        start = max(0, i - 8)
        end = min(len(lines), i + 28)
        print(f'--- lines {start+1}-{end} ---')
        for n in range(start, end):
            print(f'{n+1}:{lines[n]}')
PY

echo '== adapter resolver implementation =='
fd -t f -e ts src/server src | xargs rg -n -C 16 'function resolveModelAdapter|resolveModelAdapter|createResponsesPassthroughAdapter' | sed -n '1,360p'

echo '== focused response adapter tests =='
fd -t f -e ts tests | sort | rg 'response|adapter|passthrough|deepseek'
for f in $(fd -t f -e ts tests | sort | rg 'response|adapter|passthrough|deepseek'); do
  matches=$(rg -n 'web_search_call|normalizeWebSearch|queries|DeepSeek|deepseek|responsesPath|statelessResponses' "$f" || true)
  if [ -n "$matches" ]; then
    echo "--- $f ---"
    printf '%s\n' "$matches" | sed -n '1,180p'
  fi
done

echo '== source-level invariant probe =='
python3 - <<'PY'
from pathlib import Path
s = Path("src/adapters/openai-responses.ts").read_text()
assert 'outBody = normalizeWebSearchCallActions(outBody);' in s
branch = s[s.index('if (forward) {'):s.index('if (forward || parsed._previousResponseInputExpanded === true)')]
print('normalization branch contains:', 'normalizeWebSearchCallActions(outBody);' in branch)
print('branch condition:', branch.splitlines()[0].strip())
print('provider capability reference count:', sum(x in s for x in ['webSearch', 'web_search', 'supportsWeb']))
PY

Repository: lidge-jun/opencodex

Length of output: 35640


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== exact call-site lines =='
nl -ba src/adapters/openai-responses.ts | sed -n '1148,1170p'

echo '== focused query assertions =='
rg -n -C 8 'queries|web_search_call|query' tests/deepseek-inbound-wire.test.ts tests --glob '*.ts' \
  | rg -v 'format-result|angle brackets|long query' | sed -n '1,260p'

echo '== read-only behavior and reachability verifier =='
python3 - <<'PY'
from pathlib import Path
import re

source = Path("src/adapters/openai-responses.ts").read_text()
registry = Path("src/providers/registry.ts").read_text()
resolver = Path("src/server/adapter-resolve.ts").read_text()
config = Path("src/config.ts").read_text()

# Reproduce the normalizer's relevant deterministic behavior without executing repository code.
def normalize(body):
    if not isinstance(body, dict) or not isinstance(body.get("input"), list):
        return body
    changed = False
    out = []
    for item in body["input"]:
        if not isinstance(item, dict) or item.get("type") != "web_search_call":
            out.append(item)
            continue
        action = item.get("action")
        if not isinstance(action, dict) or not isinstance(action.get("query"), str) or "queries" in action:
            out.append(item)
            continue
        changed = True
        clone = dict(item)
        clone["action"] = {**action, "queries": [action["query"]]}
        out.append(clone)
    return {**body, "input": out} if changed else body

sample = {"input": [{"type": "web_search_call", "action": {"query": "q"}}]}
result = normalize(sample)
assert result["input"][0]["action"] == {"query": "q", "queries": ["q"]}
assert normalize({"input": [{"type": "web_search_call", "action": {"query": "q", "queries": ["existing"]}}]}) == {
    "input": [{"type": "web_search_call", "action": {"query": "q", "queries": ["existing"]}}]
}

# The source applies the normalizer in the non-forward branch and has no capability predicate.
assert "outBody = normalizeWebSearchCallActions(outBody);" in source
non_forward = source[source.index("if (forward) {"):source.index("if (forward || parsed._previousResponseInputExpanded === true)")]
assert "normalizeWebSearchCallActions(outBody);" in non_forward
assert not re.search(r"(supports|capabilit|webSearch|web_search)", non_forward, re.I)

# The built-in registry includes key-auth Responses providers, and custom configs are not
# constrained to the registry because the provider schema is passthrough.
assert 'id: "openai-apikey"' in registry and 'adapter: "openai-responses"' in registry
assert 'id: "volcengine-agent-plan"' in registry and 'adapter: "openai-responses"' in registry
assert 'authKind: "key"' in registry
assert '}).passthrough()' in config

# The resolver dispatches every provider whose effective adapter is openai-responses here.
assert 'case "openai-responses":' in resolver
assert 'return createResponsesPassthroughAdapter(providerConfig);' in resolver

print("normalizer adds queries to a missing singular-query action")
print("normalizer skips an existing queries field")
print("non-forward branch has no provider capability gate")
print("key-auth and custom openai-responses configurations reach this adapter")
PY

Repository: lidge-jun/opencodex

Length of output: 225


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== exact call-site lines =='
awk 'NR >= 1148 && NR <= 1170 { print NR ":" $0 }' src/adapters/openai-responses.ts

echo '== focused query assertions =='
rg -n -C 8 'queries|web_search_call|query' tests/deepseek-inbound-wire.test.ts \
  | sed -n '1,260p'

echo '== read-only behavior and reachability verifier =='
python3 - <<'PY'
from pathlib import Path
import re

source = Path("src/adapters/openai-responses.ts").read_text()
registry = Path("src/providers/registry.ts").read_text()
resolver = Path("src/server/adapter-resolve.ts").read_text()
config = Path("src/config.ts").read_text()

def normalize(body):
    if not isinstance(body, dict) or not isinstance(body.get("input"), list):
        return body
    changed = False
    out = []
    for item in body["input"]:
        if not isinstance(item, dict) or item.get("type") != "web_search_call":
            out.append(item)
            continue
        action = item.get("action")
        if not isinstance(action, dict) or not isinstance(action.get("query"), str) or "queries" in action:
            out.append(item)
            continue
        changed = True
        clone = dict(item)
        clone["action"] = {**action, "queries": [action["query"]]}
        out.append(clone)
    return {**body, "input": out} if changed else body

sample = {"input": [{"type": "web_search_call", "action": {"query": "q"}}]}
result = normalize(sample)
assert result["input"][0]["action"] == {"query": "q", "queries": ["q"]}
existing = {"input": [{"type": "web_search_call", "action": {"query": "q", "queries": ["existing"]}}]}
assert normalize(existing) == existing

assert "outBody = normalizeWebSearchCallActions(outBody);" in source
non_forward = source[source.index("if (forward) {"):source.index("if (forward || parsed._previousResponseInputExpanded === true)")]
assert "normalizeWebSearchCallActions(outBody);" in non_forward
assert not re.search(r"(supports|capabilit|webSearch|web_search)", non_forward, re.I)

assert 'id: "openai-apikey"' in registry
assert 'id: "volcengine-agent-plan"' in registry
assert 'authKind: "key"' in registry
assert '}).passthrough()' in config
assert 'case "openai-responses":' in resolver
assert 'return createResponsesPassthroughAdapter(providerConfig);' in resolver

print("normalizer adds queries to a missing singular-query action")
print("normalizer skips an existing queries field")
print("non-forward branch has no provider capability gate")
print("key-auth and custom openai-responses configurations reach this adapter")
PY

Repository: lidge-jun/opencodex

Length of output: 1339


Gate normalizeWebSearchCallActions by provider capability. At src/adapters/openai-responses.ts:1163, the adapter adds action.queries to every non-forward Responses provider. Only DeepSeek documents this requirement, while custom key-auth providers and volcengine-agent-plan can also use this adapter. A strict upstream can reject the added field. Add an explicit capability and enable it only for compatible providers, with a custom-provider regression test.

🤖 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 `@src/adapters/openai-responses.ts` at line 1163, Gate the
normalizeWebSearchCallActions call in the Responses adapter behind an explicit
provider capability, enabling it only for providers that support action.queries,
including DeepSeek as documented. Keep custom key-auth providers and
volcengine-agent-plan unmodified, and add a regression test covering the
custom-provider path to ensure no unsupported field is added.

Source: Path instructions

@lidge-jun

Copy link
Copy Markdown
Owner

Closing as already fixed — #932 landed the same repair about an hour before this opened (6a7351b4d), and I'm sorry you spent the effort on a duplicate.

Your diagnosis was identical and correct: replayed web_search_call actions carry only singular query, DeepSeek requires queries, and the plural has to be added next to the singular rather than replacing it. Your normalizeWebSearchCallActions() and the backfillWebSearchQueries() now on dev are the same function.

Two differences worth naming, since one of them is a real judgment call:

Placement. Yours runs inside the key-auth branch, with the reasoning that forward mode keeps the hosted shape. Mine runs unconditionally. I went that way because a review confirmed OpenAI's current schema carries optional query and optional queries on the same object with no exclusivity — the official Node SDK models both independently, and codex-rs has a round-trip fixture containing both. So the canonical forward path tolerates the extra key, and running unconditionally also covers a noncanonical forward provider that happens to be strict. If you think forward mode should stay byte-identical on principle rather than on tolerance, that is a fair argument and worth making.

Creation-time half. #932 also changed webSearchAction() in the bridge so newly created single-query items carry both keys from the start. The adapter repair handles history recorded before that; the bridge change stops new history from needing repair at all. Notably the batch case deliberately does not gain a singular query — codex-rs renders the plural form as "<first> ..." only when query is absent and queries.len() > 1.

You also filed #912, which is on dev now and closed #888. Thanks for both — the image-forwarding fix in particular had a clean ablation and made a review easy.

@lidge-jun lidge-jun closed this Aug 3, 2026
@DevMello
DevMello deleted the fix/930-web-search-call-queries branch August 3, 2026 07:54
@DevMello

DevMello commented Aug 3, 2026

Copy link
Copy Markdown
Author

No problem! Glad someone got to fixing it!

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants