Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/adapters/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,26 @@ function stripItemIdsWhenUnstored(body: unknown): unknown {
return changed ? { ...body, input } : body;
}

/**
* Replayed web_search_call actions carry a singular `query`, but DeepSeek's Responses route
* requires `queries` and 400s without it (#930). Add the plural next to the singular. Key path
* only: forward mode keeps the hosted shape.
*/
function normalizeWebSearchCallActions(body: unknown): unknown {
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;

let changed = false;
const input = body.input.map(item => {
if (!isPlainObject(item) || item.type !== "web_search_call") return item;
const action = item.action;
if (!isPlainObject(action) || typeof action.query !== "string" || action.queries !== undefined) return item;
changed = true;
return { ...item, action: { ...action, queries: [action.query] } };
});

return changed ? { ...body, input } : body;
}

/**
* Replace proxy-minted compaction items (`encrypted_content` starting with `ocx1:`) with plain
* user messages before forwarding to the ChatGPT backend. Our envelope is transparent base64, not
Expand Down Expand Up @@ -1140,6 +1160,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
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

}
if (forward || parsed._previousResponseInputExpanded === true) {
outBody = repairOversizedReplayCallIds(outBody);
Expand Down
53 changes: 53 additions & 0 deletions tests/openai-responses-passthrough.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,59 @@ describe("DeepSeek Responses endpoint contract", () => {
});
});

/** Issue #930: DeepSeek's Responses route requires `queries` on replayed web_search_call actions. */
describe("web_search_call action replay (#930)", () => {
const keyProvider = {
adapter: "openai-responses",
baseUrl: "https://api.deepseek.com",
authMode: "key" as const,
apiKey: "sk-test",
responsesPath: "/responses",
};
const forwardProvider = {
adapter: "openai-responses",
baseUrl: "https://chatgpt.example/backend-api/codex",
authMode: "forward" as const,
};

function forwardedInput(provider: typeof keyProvider | typeof forwardProvider, input: unknown[]): Record<string, unknown>[] {
const request = createResponsesPassthroughAdapter(provider).buildRequest({
modelId: "deepseek-v4-flash",
context: { messages: [] },
stream: true,
options: {},
_rawBody: { model: "deepseek-v4-flash", stream: true, input },
}, { headers: new Headers({ authorization: "Bearer t" }) }) as { body: string };
return (JSON.parse(request.body) as { input: Record<string, unknown>[] }).input;
}

test("the key path adds queries next to the recorded singular query", () => {
const input = forwardedInput(keyProvider, [
{ type: "web_search_call", id: "ws_x", status: "completed", action: { type: "search", query: "test" } },
]);
expect(input[0].action).toEqual({ type: "search", query: "test", queries: ["test"] });
});

test("forward mode keeps the hosted shape", () => {
const input = forwardedInput(forwardProvider, [
{ type: "web_search_call", id: "ws_x", status: "completed", action: { type: "search", query: "test" } },
]);
expect(input[0].action).toEqual({ type: "search", query: "test" });
});

test("existing queries, actionless calls, and other items pass through untouched", () => {
const ready = { type: "search", queries: ["a", "b"] };
const input = forwardedInput(keyProvider, [
{ type: "web_search_call", id: "ws_a", status: "completed", action: ready },
{ type: "web_search_call", id: "ws_b", status: "completed" },
{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] },
]);
expect(input[0].action).toEqual(ready);
expect("action" in input[1]).toBe(false);
expect(input[2].type).toBe("message");
});
});

describe("OpenAI Responses passthrough sanitization", () => {
test("normalizes top-level function schemas in the serialized raw body (#745)", () => {
const validParameters = {
Expand Down
Loading