fix(openai-responses): mirror web_search_call query into queries - #934
fix(openai-responses): mirror web_search_call query into queries#934DevMello wants to merge 1 commit into
Conversation
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
📝 WalkthroughWalkthroughThe Responses adapter now normalizes replayed ChangesWeb search replay normalization
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
src/adapters/openai-responses.tstests/openai-responses-passthrough.test.ts
| parsed._openAiVirtualSelectedModelId, | ||
| ); | ||
| outBody = normalizeImageGenClientTools(outBody); | ||
| outBody = normalizeWebSearchCallActions(outBody); |
There was a problem hiding this comment.
🎯 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 testsRepository: 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' | sortRepository: 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']))
PYRepository: 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")
PYRepository: 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")
PYRepository: 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
|
Closing as already fixed — #932 landed the same repair about an hour before this opened ( Your diagnosis was identical and correct: replayed 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 Creation-time half. #932 also changed You also filed #912, which is on |
|
No problem! Glad someone got to fixing it! |
Summary
Fixes #930. Replayed
web_search_callhistory carries a singularaction.query, but DeepSeek's Responses route requiresqueries, so one web search made every later turn in the conversation fail with 400. The key path now addsqueriesnext to the singular; forward mode is untouched.Verification
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
Summary by CodeRabbit
Bug Fixes
Tests