Skip to content
Merged
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
5 changes: 5 additions & 0 deletions docs-site/src/content/docs/ja/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ interface ProviderAdapter {
先立って、同じキーで同一リクエストを待機して再送します。カスタム `runTurn` トランスポートは
HTTP リトライ ループの対象外です。

- DeepSeek のステートレス Responses パーサーは、プロバイダーにスコープされた履歴正規化を受けます: フックで
注入されたコンテキストは、あいまいさのない tool-call/result バッチの後に移動します。並列呼び出しは、
それぞれの出力の前にグループ化されたままなので、すべての呼び出しが推論を含むアシスタントターンにとどまり
ます。寛容なプロバイダーと、重複・欠落・順序不正の call ID は元の入力順を保持します。

- `forward` URL → `{baseUrl}/responses`。`key` provider はデフォルトで従来の `{baseUrl}/v1/responses` 構築を使います。
- `key` provider は検証済みの相対 `responsesPath` を設定できます。adapter は `baseUrl` 末尾の `/` を 1 つ除き、`{trimmedBaseUrl}{responsesPath}` に送信します。Ark Agent Plan では `baseUrl: "https://ark.cn-beijing.volces.com/api/plan/v3"` と `responsesPath: "/responses"` を使います。
- `forward` モードでは安全なヘッダー許可リスト(`FORWARD_HEADERS`)だけを中継します。authorization、ChatGPT account id、OpenAI beta/originator/session ヘッダーが対象です。この ChatGPT ログイン経路は [サイドカー](/ja/guides/sidecars/) にも使われます。
Expand Down
5 changes: 5 additions & 0 deletions docs-site/src/content/docs/ko/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ interface ProviderAdapter {
같은 키로 동일 요청을 대기 후 재전송합니다. 커스텀 `runTurn` 전송은 HTTP 재시도 루프에
포함되지 않습니다.

- DeepSeek의 stateless Responses 파서는 제공자 범위의 기록 정규화를 받습니다: 훅으로
주입된 컨텍스트는 명확한 tool-call/result 배치 뒤로 이동합니다. 병렬 호출은 각 결과 앞에
함께 묶여 있어 모든 호출이 추론을 담은 어시스턴트 턴에 남습니다. 관대한 제공자와 중복되거나
누락되거나 순서가 잘못된 call ID는 원래 입력 순서를 유지합니다.

- `forward` URL → `{baseUrl}/responses`. `key` provider는 기본적으로 기존 `{baseUrl}/v1/responses` 구성을 사용합니다.
- `key` provider는 검증된 상대 `responsesPath`를 설정할 수 있습니다. adapter는 `baseUrl` 끝의 `/` 하나를 제거하고 `{trimmedBaseUrl}{responsesPath}`로 전송합니다. Ark Agent Plan은 `baseUrl: "https://ark.cn-beijing.volces.com/api/plan/v3"`와 `responsesPath: "/responses"`를 사용합니다.
- `forward` 모드에서는 안전한 헤더 허용 목록(`FORWARD_HEADERS`)만 중계합니다. authorization,
Expand Down
5 changes: 5 additions & 0 deletions docs-site/src/content/docs/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ waits and replays the identical request on the same key before any other handlin
the translated `openai-chat` / Anthropic request path. Custom `runTurn` transports are not part
of the HTTP retry loop.

- DeepSeek's stateless Responses parser receives provider-scoped history normalization: hook-injected
context moves after an unambiguous tool-call/result batch. Parallel calls remain grouped before
their matching outputs so every call stays in the reasoning-bearing assistant turn. Tolerant
providers and ambiguous duplicate, missing, or out-of-order call IDs keep their original input order.

Comment on lines +61 to +65

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.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

mapfile -t files < <(
  fd -t f -i 'adapters\.md$' docs-site/src/content/docs \
    | rg '/(ja|ko|ru|zh-cn)/' || true
)

((${`#files`[@]})) || exit 0

rg -n -i \
  'immediate|adjacent|parallel|reasoning|duplicate|missing|out.of.order|original input order' \
  "${files[@]}"

Repository: lidge-jun/opencodex

Length of output: 209


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' 'Localized adapter pages:'
find docs-site/src/content/docs -type f \( -iname 'adapters.md' -o -iname 'adapters.mdx' \) \
  -print | sort

printf '%s\n' 'Potentially contradictory localized claims:'
find docs-site/src/content/docs -type f \( -iname 'adapters.md' -o -iname 'adapters.mdx' \) \
  -print0 |
  while IFS= read -r -d '' file; do
    case "$file" in
      */ja/*|*/ko/*|*/ru/*|*/zh-cn/*)
        rg -n -i \
          'immediate|adjacent|parallel|reasoning|duplicate|missing|out[ -]?of[ -]?order|original input order|call.?result|tool.?call|pair|順序|並列|重複|欠落|последователь|параллел|дублик|пропущ|поряд|调用|并行|重复|缺失|顺序' \
          "$file" || true
        ;;
    esac
  done

Repository: lidge-jun/opencodex

Length of output: 4134


🏁 Script executed:

#!/bin/bash
set -euo pipefail

files=(
  docs-site/src/content/docs/ja/reference/adapters.md
  docs-site/src/content/docs/ko/reference/adapters.md
  docs-site/src/content/docs/ru/reference/adapters.md
  docs-site/src/content/docs/zh-cn/reference/adapters.md
)

for file in "${files[@]}"; do
  printf '\n--- %s ---\n' "$file"
  rg -n -i -C 4 \
    'deepseek|responses|openai.?responses|hook|injected|context|history|normaliz|tool.?call|tool.?result|call.?id|immediate|adjacent|order|pair|parallel|duplicate|missing|out[ -]?of[ -]?order' \
    "$file" || true
done

Repository: lidge-jun/opencodex

Length of output: 19297


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' 'English reference section:'
sed -n '44,70p' docs-site/src/content/docs/reference/adapters.md

printf '%s\n' 'Implementation and test references:'
rg -n -i \
  'deepseek|responsesPath|hook.?inject|tool.?call|tool.?result|call.?id|history normalization|normalize.*history|original input order|reasoning-bearing|passthrough' \
  --glob '!docs-site/src/content/docs/**' .

Repository: lidge-jun/opencodex

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' 'English reference section:'
sed -n '44,70p' docs-site/src/content/docs/reference/adapters.md

printf '%s\n' 'Relevant source files and focused matches:'
rg -l -i \
  'deepseek|hook.?inject|original input order|reasoning-bearing|responses normalization|normalize.*(history|input)|passthrough' \
  src tests docs-site/src/content/docs/reference/adapters.md \
  | sort | head -100

Repository: lidge-jun/opencodex

Length of output: 5234


Document the DeepSeek exception in all localized adapter pages. Update ja/reference/adapters.md:46, ko/reference/adapters.md:52-53, ru/reference/adapters.md:56-57, and zh-cn/reference/adapters.md:49-50 to explain history normalization, parallel-call grouping, and input-order preservation for ambiguous IDs.

🤖 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 `@docs-site/src/content/docs/reference/adapters.md` around lines 61 - 65,
Update the localized adapter pages at the specified reference sections to
include the DeepSeek stateless Responses parser exception: describe
provider-scoped history normalization, moving hook-injected context after
unambiguous tool-call/result batches, grouping parallel calls with matching
outputs, and preserving original input order for tolerant providers or ambiguous
duplicate, missing, or out-of-order call IDs. Keep the localized wording
consistent with the existing English adapter documentation.

Sources: Path instructions, Learnings

- `forward` URL → `{baseUrl}/responses`. A `key` provider defaults to the legacy `{baseUrl}/v1/responses` construction.
- A `key` provider may set a validated relative `responsesPath`; the adapter removes one trailing slash from `baseUrl` and sends `{trimmedBaseUrl}{responsesPath}`. For Ark Agent Plan, use `baseUrl: "https://ark.cn-beijing.volces.com/api/plan/v3"` with `responsesPath: "/responses"`.
- In `forward` mode only a safe header allowlist is relayed (`FORWARD_HEADERS`): authorization,
Expand Down
6 changes: 6 additions & 0 deletions docs-site/src/content/docs/ru/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ interface ProviderAdapter {
том же ключе, как и в переводимом пути `openai-chat`/Anthropic. Пользовательские транспорты
`runTurn` в цикл HTTP-повторов не входят.

- Stateless-парсер DeepSeek Responses получает нормализацию истории на уровне провайдера:
контекст, внедрённый хуком, переносится после однозначного батча call/result. Параллельные вызовы
остаются сгруппированными перед своими результатами, поэтому каждый вызов сохраняет свой
один assistant-ход с рассуждениями. Толерантные провайдеры и неоднозначные (дублирующиеся,
отсутствующие или неупорядоченные) идентификаторы call сохраняют исходный порядок входа.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- URL для `forward` → `{baseUrl}/responses`. Провайдер с `key` по умолчанию сохраняет прежнее построение `{baseUrl}/v1/responses`.
- Провайдер с `key` может задать проверенный относительный `responsesPath`: адаптер удаляет один завершающий `/` из `baseUrl` и отправляет запрос на `{trimmedBaseUrl}{responsesPath}`. Для Ark Agent Plan используйте `baseUrl: "https://ark.cn-beijing.volces.com/api/plan/v3"` и `responsesPath: "/responses"`.
- В режиме `forward` ретранслируется только безопасный allowlist заголовков (`FORWARD_HEADERS`):
Expand Down
4 changes: 4 additions & 0 deletions docs-site/src/content/docs/zh-cn/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ interface ProviderAdapter {
会等待并先于其他处理或故障转移,在相同 key 上重放完全相同请求,与翻译后的
`openai-chat`/Anthropic 请求路径一致。自定义 `runTurn` 传输不在 HTTP 重试循环之内。

- DeepSeek 的 stateless Responses parser 会收到按 provider 范围的历史归一化:hook 注入的上下文会移动到
明确的 tool-call/result 批次之后。并行调用保持在其对应输出之前分组,因此每个调用都留在承载
推理的 assistant 回合中。宽容的 provider 和歧义的(重复、缺失或乱序的)call ID 保留原始输入顺序。

- `forward` URL → `{baseUrl}/responses`。`key` provider 默认保留原有的 `{baseUrl}/v1/responses` 构造。
- `key` provider 可设置经过验证的相对 `responsesPath`;adapter 会移除 `baseUrl` 末尾的一个 `/`,并向 `{trimmedBaseUrl}{responsesPath}` 发送请求。Ark Agent Plan 使用 `baseUrl: "https://ark.cn-beijing.volces.com/api/plan/v3"` 和 `responsesPath: "/responses"`。
- `forward` 模式只会转发安全的 header allowlist(`FORWARD_HEADERS`):authorization、ChatGPT
Expand Down
4 changes: 4 additions & 0 deletions docs-site/src/content/docs/zh-tw/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ interface ProviderAdapter {
**不經轉換**地流式傳回。
**認證:** `forward`(轉發呼叫方 header)或 `key`。

- DeepSeek 的 stateless Responses parser 會收到按 provider 範圍的歷史正規化:hook 注入的內容會移動到
明確的 tool-call/result 批次之後。並行呼叫保持在其對應輸出之前分組,因此每個呼叫都留在承載
推理的 assistant 回合中。寬容的 provider 和歧義的(重複、缺失或亂序的)call ID 保留原始輸入順序。

- `forward` URL → `{baseUrl}/responses`。`key` provider 預設保留原有的 `{baseUrl}/v1/responses` 構造。
- `key` provider 可設定經過驗證的相對 `responsesPath`;adapter 會移除 `baseUrl` 末尾的一個 `/`,並向 `{trimmedBaseUrl}{responsesPath}` 傳送請求。Ark Agent Plan 使用 `baseUrl: "https://ark.cn-beijing.volces.com/api/plan/v3"` 和 `responsesPath: "/responses"`。
- `forward` 模式只會轉發安全的 header allowlist(`FORWARD_HEADERS`):authorization、ChatGPT
Expand Down
76 changes: 59 additions & 17 deletions src/adapters/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,15 +540,15 @@ function repairOrphanedInputItems(body: unknown, dropReasoning: boolean): unknow
}

/**
* Make unambiguous Responses tool pairs adjacent for upstream parsers that require it.
* Make unambiguous Responses tool batches contiguous for upstream parsers that require it.
*
* [Decision Log]
* - 목적과 의도: Keep Codex hook-injected developer context without letting it make a strict upstream reject the matching tool result.
* - 기존 구현 및 제약 조건: The orphan repair verifies only pair presence; globally reordering valid history would change tolerant providers unnecessarily.
* - 검토한 주요 대안: Reorder every Responses request, drop the intervening message, or gate a lossless reorder behind provider capability metadata.
* - 선택한 방식: Reorder only unique call/result pairs for providers that explicitly require adjacency, preserving every intervening item immediately after the result.
* - 다른 대안 대신 이 방식을 선택한 이유: The provider gate limits semantic blast radius, while refusing ambiguous duplicate ids avoids guessing which result belongs to which call.
* - 장점, 단점 및 영향: DeepSeek receives the adjacency its parser requires; tolerant providers stay byte/order equivalent. Ambiguous duplicate ids still fail upstream rather than being silently rewritten.
* - 목적과 의도: Keep Codex hook-injected developer context without splitting a parallel tool-call turn away from its reasoning or making a strict upstream reject matching results.
* - 기존 구현 및 제약 조건: The orphan repair verifies only pair presence, while the original pair-by-pair reorder turned `reasoning, call A, call B, output A, output B` into two assistant turns and made DeepSeek reject call B for missing reasoning (#1477).
* - 검토한 주요 대안: Disable parallel calls (DeepSeek always enables them); duplicate reasoning per call; reorder each pair; or normalize the complete unambiguous call batch.
* - 선택한 방식: Treat calls emitted before the first matched result as one batch, emit all calls followed by their matched outputs, and preserve intervening non-tool items immediately after the batch.
* - 다른 대안 대신 이 방식을 선택한 이유: Batch normalization matches the Responses parallel-call shape without fabricating reasoning, while the provider gate and unique-pair requirement keep the blast radius narrow.
* - 장점, 단점 및 영향: DeepSeek keeps one reasoning-bearing assistant turn for parallel calls and still accepts hook-interleaved single calls; tolerant providers stay byte/order equivalent, and duplicate, missing, or backwards call/result pairs are not guessed.
*/
function normalizeResponsesToolResultAdjacency(body: unknown): unknown {
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
Expand Down Expand Up @@ -576,24 +576,66 @@ function normalizeResponsesToolResultAdjacency(body: unknown): unknown {
}
}

const movedOutputIndices = new Set<number>();
const outputAfterCall = new Map<number, unknown>();
const pairs: Array<{ callIndex: number; outputIndex: number }> = [];
for (const [key, callIndices] of calls) {
const outputIndices = outputs.get(key);
if (callIndices.length !== 1 || outputIndices?.length !== 1) continue;
if (!outputIndices) return body;
if (callIndices.length !== 1 || outputIndices.length !== 1) return body;
const callIndex = callIndices[0]!;
const outputIndex = outputIndices[0]!;
if (outputIndex === callIndex + 1) continue;
movedOutputIndices.add(outputIndex);
outputAfterCall.set(callIndex, input[outputIndex]);
if (outputIndex <= callIndex) return body;
pairs.push({ callIndex, outputIndex });
}
if (movedOutputIndices.size === 0) return body;
// Reject any collected output that lacks exactly one matching call. A lone or
// duplicated output is ambiguous, and normalizing on top of it could sever a
// result from the reasoning-bearing call turn it belongs to.
for (const [key, outputIndices] of outputs) {
const callIndices = calls.get(key);
if (!callIndices || callIndices.length !== 1 || outputIndices.length !== 1) return body;
}
pairs.sort((left, right) => left.callIndex - right.callIndex);

const movedIndices = new Set<number>();
const batchAt = new Map<number, unknown[]>();
for (let cursor = 0; cursor < pairs.length;) {
const group = [pairs[cursor]!];
let firstOutputIndex = pairs[cursor]!.outputIndex;
let next = cursor + 1;
while (next < pairs.length && pairs[next]!.callIndex < firstOutputIndex) {
group.push(pairs[next]!);
firstOutputIndex = Math.min(firstOutputIndex, pairs[next]!.outputIndex);
next += 1;
}

// Within one reasoning turn the outputs must appear in the same order as their
// calls. If they are reversed, normalizing would fabricate a new output order;
// leave the ambiguous history untouched instead.
for (let groupIndex = 1; groupIndex < group.length; groupIndex += 1) {
if (group[groupIndex]!.outputIndex < group[groupIndex - 1]!.outputIndex) return body;
}

const batch = [
...group.map(pair => input[pair.callIndex]),
...group.map(pair => input[pair.outputIndex]),
];
const anchor = group[0]!.callIndex;
const alreadyContiguous = batch.every((item, offset) => input[anchor + offset] === item);
if (!alreadyContiguous) {
batchAt.set(anchor, batch);
for (const pair of group) {
movedIndices.add(pair.callIndex);
movedIndices.add(pair.outputIndex);
}
}
cursor = next;
}
if (batchAt.size === 0) return body;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const normalized: unknown[] = [];
for (let index = 0; index < input.length; index += 1) {
if (movedOutputIndices.has(index)) continue;
normalized.push(input[index]);
if (outputAfterCall.has(index)) normalized.push(outputAfterCall.get(index));
const batch = batchAt.get(index);
if (batch) normalized.push(...batch);
if (!movedIndices.has(index)) normalized.push(input[index]);
}
return { ...body, input: normalized };
}
Expand Down
7 changes: 4 additions & 3 deletions src/providers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,8 @@ export interface ProviderRegistryEntry {
*/
statelessResponses?: boolean;
/**
* Responses parser requires a matched tool result directly after its call. This is
* seeded/backfilled like other fixed upstream wire-contract capabilities.
* Responses parser requires an unambiguous call batch and its matched result batch
* to stay contiguous. This is seeded/backfilled like other fixed wire capabilities.
*/
requiresAdjacentResponsesToolResults?: boolean;
/**
Expand Down Expand Up @@ -1462,7 +1462,8 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
// server." https://api-docs.deepseek.com/api/create-response/
statelessResponses: true,
// DeepSeek rejects a valid Codex continuation when hook-provided developer
// context is persisted between a call and its matching result (#1292).
// context splits a call from its result (#1292); parallel calls remain one
// reasoning-bearing assistant batch rather than being split per pair (#1477).
requiresAdjacentResponsesToolResults: true,
/* [Decision Log]
- 목적: DeepSeek V4 thinking mode multi-turn/tool-call requests must replay prior assistant reasoning_content.
Expand Down
6 changes: 3 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1227,9 +1227,9 @@ export interface OcxProviderConfig {
*/
statelessResponses?: boolean;
/**
* Responses upstream whose parser requires each tool result to immediately follow
* its matching call. When enabled, only unambiguous matched pairs are reordered;
* intervening messages are preserved after the result instead of being dropped.
* Responses upstream whose parser requires an unambiguous call batch and its matched
* result batch to remain contiguous. Hook-injected context that splits the batch is
* preserved after it, and parallel calls stay together with the reasoning turn that produced them.
*/
requiresAdjacentResponsesToolResults?: boolean;
/**
Expand Down
15 changes: 15 additions & 0 deletions structure/04_transports-and-sidecars.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,21 @@ replays are explicit and receive the same repair.
These compatibility guards are covered by focused tests and should stay close to the adapters that
need them.

DeepSeek's stateless Responses compatibility pass normalizes only unambiguous tool-call batches.
Calls emitted before the first matched output stay together as one assistant batch, followed by
their outputs in call order; hook-injected messages that split the batch move after it without being
dropped. This preserves #1292's single-call adjacency repair without splitting a same-turn parallel
batch away from its preceding plaintext reasoning (#1477). Tolerant providers never enter this pass,
and duplicate, missing, or backwards call/result pairs are left for the upstream to reject rather than guessed.

[Decision Log]
- 목적과 의도: Preserve DeepSeek reasoning replay for parallel tool calls while retaining the provider-scoped repair for hook-interleaved results.
- 기존 구현 및 제약 조건: Pair-by-pair adjacency fixed one call but split parallel calls into separate assistant turns; DeepSeek always enables parallel tool calling and merges adjacent reasoning and calls into one assistant message.
- 검토한 주요 대안: Disable parallel calls, duplicate reasoning, remove the #1292 repair, or normalize one unambiguous call/output batch.
- 선택한 방식: Group calls that occur before the first matched output, emit the call batch followed by outputs in call order, and retain intervening non-tool items after the batch.
- 다른 대안 대신 이 방식을 선택한 이유: The batch shape matches the documented Responses contract without inventing reasoning or reintroducing hook-interleaving failures.
- 장점, 단점 및 영향: Sequential and parallel tool continuations both retain their reasoning contract; only the declared strict provider changes order, and ambiguous histories still fail closed upstream.

## Cursor parameterized models

Cursor Router's parameterized `default` model is represented in Codex by four catalog rows:
Expand Down
Loading
Loading