Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ namespace 付き combo alias はその namespace prefix に selector を再利
| `noTopPModels?` | `string[]` |発信者指定の`top_p`を拒否するモデル。 |
| `noPenaltyModels?` | `string[]` |存在/周波数ペナルティを拒否するモデル。 |
| `parallelToolCalls?` | `boolean` |並列ツール呼び出しを切り替えます。 OpenAI Chat はデフォルトでオンになっています。非チャット アダプターは明示的な `true` でのみアドバタイズします。 |
| `responsesItemIdRepair?` | `{ message?: string[]; reasoning?: string[]; repairMissingTerminalIds?: boolean }` |正確なプレースホルダー ID および欠落している端末 ID に対するダウンストリーム SSE 修復はデフォルトで無効になっています。関数呼び出し ID は決して書き換えられません。 |
| `responsesItemIdRepair?` | `{ message?: string[]; reasoning?: string[]; repairMissingTerminalIds?: boolean; rewriteNonCanonicalIds?: boolean }` |正確なプレースホルダー ID、欠落している端末 ID、および非標準の UUID 形式 response/item ID(`rewriteNonCanonicalIds`)に対するダウンストリーム SSE 修復はデフォルトで無効です。関数呼び出し ID は決して書き換えられません。 |
| `autoToolChoiceOnlyModels?` | `string[]` | `tool_choice` が `auto` または `none` のみを受け入れるモデル。強制的な選択は格下げされます。 |
| `preserveReasoningContentModels?` | `string[]` |チャット履歴に以前のアシスタント `reasoning_content` が必要なモデル。 |
| `thinkingToggleModels?` | `string[]` |エフォート ラダーではなく `thinking.enabled` を使用してモデルをチャットします。 |
Expand Down Expand Up @@ -197,14 +197,15 @@ Anthropic アカウント ポリシーのリスクを理解していない限り
"responsesItemIdRepair": {
"reasoning": ["rs_0"],
"message": ["msg_0"],
"repairMissingTerminalIds": true
"repairMissingTerminalIds": true,
"rewriteNonCanonicalIds": true
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
}
}
```

プレースホルダー リストは完全に一致します。通常/ステートフル応答プロバイダーのフィールドを未設定のままにして、パススルーがバイトごとに同一になるようにします
プレースホルダー リストは完全に一致します。DeepSeek Responses のように UUID 形式の response/item ID と生の `reasoning_text` ストリームを返すゲートウェイでは `rewriteNonCanonicalIds` を有効にします。OpenCodex はそれらの ID を Codex 向けプレフィックスへ書き換え、reasoning を `encrypted_content` に正規化しつつ replay 用の平文 `content` を保持し、終端の `[DONE]` を補完します。通常/ステートフルな Responses プロバイダーではこのフィールドを未設定のままにして、パススルーがバイト単位で同一になるようにします

## Cursor プロバイダー (`adapter: "cursor"`)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ target도 selector로 재사용할 수 없습니다. raw account id와 email은
| `noTopPModels?` | `string[]` | 호출자가 지정한 `top_p`를 거부하는 모델입니다. |
| `noPenaltyModels?` | `string[]` | presence/frequency penalty를 허용하지 않는 모델입니다. |
| `parallelToolCalls?` | `boolean` | 병렬 도구 호출을 켜거나 끕니다. OpenAI Chat은 기본으로 켜져 있고, 비-chat 어댑터는 명시적으로 `true`일 때만 이를 노출합니다. |
| `responsesItemIdRepair?` | `{ message?: string[]; reasoning?: string[]; repairMissingTerminalIds?: boolean }` | 기본값이 꺼진 downstream SSE 복구입니다. 정확한 자리표시자 id와 누락된 종료 id를 복구합니다. function-call id는 다시 쓰지 않습니다. |
| `responsesItemIdRepair?` | `{ message?: string[]; reasoning?: string[]; repairMissingTerminalIds?: boolean; rewriteNonCanonicalIds?: boolean }` | 기본값이 꺼진 downstream SSE 복구입니다. 정확한 자리표시자 id, 누락된 종료 id, 비표준 UUID 스타일 response/item id(`rewriteNonCanonicalIds`)를 복구합니다. function-call id는 다시 쓰지 않습니다. |
| `autoToolChoiceOnlyModels?` | `string[]` | `tool_choice`가 `auto` 또는 `none`만 받는 모델입니다. 강제 선택은 낮은 수준으로 바뀝니다. |
| `preserveReasoningContentModels?` | `string[]` | chat 기록에서 이전 assistant `reasoning_content`가 필요한 모델입니다. |
| `thinkingToggleModels?` | `string[]` | effort 계층 대신 `thinking.enabled`를 쓰는 chat 모델입니다. |
Expand Down Expand Up @@ -200,14 +200,15 @@ Anthropic 계정 정책 위험을 이해하지 못한다면 이 기능은 꺼두
"responsesItemIdRepair": {
"reasoning": ["rs_0"],
"message": ["msg_0"],
"repairMissingTerminalIds": true
"repairMissingTerminalIds": true,
"rewriteNonCanonicalIds": true
}
}
}
}
```

자리표시자 목록은 정확히 일치해야 합니다. 일반적인 상태 유지형 Responses 공급자에서는 필드를 설정하지 마십시오. 그래야 passthrough가 바이트 단위로 동일하게 유지됩니다.
자리표시자 목록은 정확히 일치해야 합니다. DeepSeek Responses처럼 UUID 형식 response/item id와 원본 `reasoning_text` 스트림을 반환하는 게이트웨이에서는 `rewriteNonCanonicalIds`를 켜십시오. OpenCodex는 해당 id를 Codex 친화적 prefix로 다시 쓰고, reasoning을 `encrypted_content`로 정규화하면서 replay용 평문 `content`는 유지하며, 종료용 `[DONE]`을 보장합니다. 일반/상태 유지형 Responses 공급자에서는 필드를 설정하지 마십시오. 그래야 passthrough가 바이트 단위로 동일하게 유지됩니다.

## Cursor 공급자 (`adapter: "cursor"`)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ differing backup and rewrites known legacy namespaced selected ids to bare ids.
| `noTopPModels?` | `string[]` | Models that reject caller-specified `top_p`. |
| `noPenaltyModels?` | `string[]` | Models that reject presence/frequency penalties. |
| `parallelToolCalls?` | `boolean` | Toggle parallel tool calls. OpenAI Chat defaults on; non-chat adapters advertise only on explicit `true`. |
| `responsesItemIdRepair?` | `{ message?: string[]; reasoning?: string[]; repairMissingTerminalIds?: boolean }` | Disabled-by-default downstream SSE repair for exact placeholder ids and missing terminal ids. Function-call ids are never rewritten. |
| `responsesItemIdRepair?` | `{ message?: string[]; reasoning?: string[]; repairMissingTerminalIds?: boolean; rewriteNonCanonicalIds?: boolean }` | Disabled-by-default downstream SSE repair for exact placeholder ids, missing terminal ids, and non-canonical UUID-style response/item ids (`rewriteNonCanonicalIds`). Function-call ids are never rewritten. |
| `autoToolChoiceOnlyModels?` | `string[]` | Models whose `tool_choice` accepts only `auto` or `none`; forced choices are downgraded. |
| `preserveReasoningContentModels?` | `string[]` | Models requiring prior assistant `reasoning_content` in chat history. |
| `thinkingToggleModels?` | `string[]` | Chat models using `thinking.enabled` rather than an effort ladder. |
Expand Down Expand Up @@ -237,15 +237,15 @@ For a broken `openai-responses` gateway, repair belongs on the provider object:
"responsesItemIdRepair": {
"reasoning": ["rs_0"],
"message": ["msg_0"],
"repairMissingTerminalIds": true
"repairMissingTerminalIds": true,
"rewriteNonCanonicalIds": true
}
}
}
}
```

Placeholder lists are exact matches. Leave the field unset for normal/stateful Responses providers
so passthrough stays byte-for-byte identical.
Placeholder lists are exact matches. Set `rewriteNonCanonicalIds` for gateways such as DeepSeek Responses that emit UUID item/response ids and raw `reasoning_text` streams; OpenCodex rewrites those ids to Codex-friendly prefixes, folds reasoning into `encrypted_content` (while keeping plaintext `content` for replay), and ensures a terminal `[DONE]`. Leave the field unset for normal/stateful Responses providers so passthrough stays byte-for-byte identical.

## Cursor provider (`adapter: "cursor"`)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ cross-route credential fallback не существует. Строки API GPT-
| `noTopPModels?` | `string[]` | Модели, отвергающие переданный вызывающей стороной `top_p`. |
| `noPenaltyModels?` | `string[]` | Модели, отвергающие penalty presence/frequency. |
| `parallelToolCalls?` | `boolean` | Переключатель parallel tool call'ов. Для OpenAI Chat по умолчанию включено; не-chat adapter'ы рекламируют это только при явном `true`. |
| `responsesItemIdRepair?` | `{ message?: string[]; reasoning?: string[]; repairMissingTerminalIds?: boolean }` | По умолчанию выключенная downstream SSE-repair для exact placeholder-id и отсутствующих terminal-id. Function-call id никогда не переписываются. |
| `responsesItemIdRepair?` | `{ message?: string[]; reasoning?: string[]; repairMissingTerminalIds?: boolean; rewriteNonCanonicalIds?: boolean }` | По умолчанию выключенная downstream SSE-repair для exact placeholder-id, отсутствующих terminal-id и неканонических UUID-style response/item id (`rewriteNonCanonicalIds`). Function-call id никогда не переписываются. |
| `autoToolChoiceOnlyModels?` | `string[]` | Модели, у которых `tool_choice` принимает только `auto` или `none`; forced choice понижается. |
| `preserveReasoningContentModels?` | `string[]` | Модели, которым нужен предыдущий assistant `reasoning_content` в chat history. |
| `thinkingToggleModels?` | `string[]` | Chat-модели, использующие `thinking.enabled` вместо effort-ladder. |
Expand Down Expand Up @@ -245,14 +245,15 @@ Beijing, а `alibaba-token-plan-intl` обслуживает междунаро
"responsesItemIdRepair": {
"reasoning": ["rs_0"],
"message": ["msg_0"],
"repairMissingTerminalIds": true
"repairMissingTerminalIds": true,
"rewriteNonCanonicalIds": true
}
}
}
}
```

Списки placeholder'ов сравниваются по exact-match. Для обычных/stateful Responses-провайдеров это
Списки placeholder'ов сравниваются по exact-match. Для шлюзов вроде DeepSeek Responses, которые возвращают UUID-style response/item id и сырые `reasoning_text` потоки, включайте `rewriteNonCanonicalIds`: OpenCodex переписывает id в Codex-friendly prefix, нормализует reasoning в `encrypted_content` (сохраняя plaintext `content` для replay) и гарантирует terminal `[DONE]`. Для обычных/stateful Responses-провайдеров это
поле оставляйте unset, чтобы passthrough оставался byte-for-byte идентичным.

## Провайдер Cursor (`adapter: "cursor"`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ pool account id(不能是内部 `__main__`),或用 `"@main"` 表示 Codex
| `noTopPModels?` | `string[]` | 会拒绝调用方指定 `top_p` 的模型。 |
| `noPenaltyModels?` | `string[]` | 会拒绝 presence/frequency penalty 的模型。 |
| `parallelToolCalls?` | `boolean` | 切换并行工具调用。OpenAI Chat 默认开启;非 chat 适配器只有显式 `true` 时才会声明支持。 |
| `responsesItemIdRepair?` | `{ message?: string[]; reasoning?: string[]; repairMissingTerminalIds?: boolean }` | 默认关闭的下游 SSE 修复,用于精确占位 id 和缺失的终止 id。function-call id 永远不会被重写。 |
| `responsesItemIdRepair?` | `{ message?: string[]; reasoning?: string[]; repairMissingTerminalIds?: boolean; rewriteNonCanonicalIds?: boolean }` | 默认关闭的下游 SSE 修复,用于精确占位 id、缺失的终止 id,以及非标准 UUID 风格的 response/item id(`rewriteNonCanonicalIds`)。function-call id 永远不会被重写。 |
| `autoToolChoiceOnlyModels?` | `string[]` | `tool_choice` 只接受 `auto` 或 `none` 的模型;强制选择会被降级。 |
| `preserveReasoningContentModels?` | `string[]` | 需要在聊天历史中保留先前 assistant `reasoning_content` 的模型。 |
| `thinkingToggleModels?` | `string[]` | 使用 `thinking.enabled` 而不是 effort 阶梯的 chat 模型。 |
Expand Down Expand Up @@ -193,14 +193,15 @@ affinity。这些策略不能规避 provider enforcement。
"responsesItemIdRepair": {
"reasoning": ["rs_0"],
"message": ["msg_0"],
"repairMissingTerminalIds": true
"repairMissingTerminalIds": true,
"rewriteNonCanonicalIds": true
}
}
}
}
```

占位列表必须精确匹配。对于正常/有状态的 Responses 提供者,请保持该字段未设置,以便转发能保持逐字节一致。
占位列表必须精确匹配。对 DeepSeek Responses 这类会返回 UUID item/response id 与原始 `reasoning_text` 流的网关,请设置 `rewriteNonCanonicalIds`:OpenCodex 会把 id 改写成 Codex 友好前缀,把 reasoning 折叠进 `encrypted_content`(同时保留明文 `content` 便于回放),并补齐终端 `[DONE]`。对于正常/有状态的 Responses 提供者,请保持该字段未设置,以便转发能保持逐字节一致。

## Cursor 提供者(`adapter: "cursor"`)

Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,7 @@ const providerConfigSchema = z.object({
message: z.array(z.string().min(1)).optional(),
reasoning: z.array(z.string().min(1)).optional(),
repairMissingTerminalIds: z.boolean().optional(),
rewriteNonCanonicalIds: z.boolean().optional(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Document the new repair option

Custom Responses providers must explicitly set responsesItemIdRepair.rewriteNonCanonicalIds, but this new user-facing configuration field is not documented anywhere under docs-site/; users installing the released package therefore have no shipped reference for enabling the DeepSeek compatibility behavior or understanding its stream normalization effects. Add it to the relevant provider/configuration documentation.

AGENTS.md reference: src/AGENTS.md:L28-L28

Useful? React with 👍 / 👎.

}).strict().optional(),
}).passthrough();

Expand Down
8 changes: 8 additions & 0 deletions src/providers/derive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,14 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
// the entry so an explicit user value stays distinguishable from the default.
if (prov.supportsServiceTier === undefined && entry.supportsServiceTier !== undefined) prov.supportsServiceTier = entry.supportsServiceTier;
if (prov.preserveResponsesReasoningContent === undefined && entry.preserveResponsesReasoningContent !== undefined) prov.preserveResponsesReasoningContent = entry.preserveResponsesReasoningContent;
if (prov.responsesItemIdRepair === undefined && entry.responsesItemIdRepair !== undefined) {
const repair = entry.responsesItemIdRepair;
prov.responsesItemIdRepair = {
...repair,
...(repair.message ? { message: [...repair.message] } : {}),
...(repair.reasoning ? { reasoning: [...repair.reasoning] } : {}),
};
}
if (!prov.autoToolChoiceOnlyModels && seed.autoToolChoiceOnlyModels) prov.autoToolChoiceOnlyModels = [...seed.autoToolChoiceOnlyModels];
if (!prov.preserveReasoningContentModels && seed.preserveReasoningContentModels) prov.preserveReasoningContentModels = [...seed.preserveReasoningContentModels];
if (!prov.reasoningSplitModels && seed.reasoningSplitModels) prov.reasoningSplitModels = [...seed.reasoningSplitModels];
Expand Down
10 changes: 10 additions & 0 deletions src/providers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ export interface ProviderRegistryEntry {
supportsServiceTier?: boolean;
/** Registry default for plaintext reasoning replay; see `OcxProviderConfig.preserveResponsesReasoningContent`. Registry-only like `supportsServiceTier`. */
preserveResponsesReasoningContent?: boolean;
/** Registry default for client-facing Responses item-id repair (fill-only into runtime provider config). */
responsesItemIdRepair?: import("../types").ResponsesItemIdRepairConfig;
modelDiscovery?: ProviderModelDiscoverySpec;
contextWindow?: number;
modelContextWindows?: Record<string, number>;
Expand Down Expand Up @@ -1093,6 +1095,14 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
// route REQUIRES replay on tool-call continuations is an inference from the
// Chat Thinking-Mode docs, not a confirmed Responses contract.)
preserveResponsesReasoningContent: true,
// DeepSeek Responses emits UUID item/response ids and raw reasoning_text streams that
// leave Codex App/CLI stuck on Thinking/Working even after HTTP 200 (#938 family).
// Enable the client-facing SSE repair by default for the built-in provider so Codex
// Desktop/CLI do not need a hand-edited provider config for the native route.
responsesItemIdRepair: {
rewriteNonCanonicalIds: true,
repairMissingTerminalIds: true,
},
// "The API is stateless: responses and conversations are not stored on the
// server." https://api-docs.deepseek.com/api/create-response/
statelessResponses: true,
Expand Down
24 changes: 21 additions & 3 deletions src/server/relay-eager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export type EagerRelayHooks = {
};

export type EagerRelayOptions = {
/** Optional trailer emitted after rewrite flush (e.g. data: [DONE]). */
trailer?: string | (() => string | undefined);
/** Bounded client queue in bytes; producer pauses above it. Default 8 MiB. */
maxQueueBytes?: number;
/** Transient-budget owner for the inline-rewrite frame buffer. */
Expand Down Expand Up @@ -123,14 +125,16 @@ export function relaySseEagerBounded(
if (!next) break;
const payload = sseDataPayload(next.block);
const rewrittenPayload = payload === null ? null : rewrite!(payload);
frameBuffer = next.rest;
// Returning null from rewrite drops the entire SSE event.
if (payload !== null && rewrittenPayload === null) continue;
// Replace only on an actual change: replaceSseDataPayload collapses
// multi-data-line events and normalizes newline style even when the
// payload is identical, which corrupts valid streams.
const block = payload !== null && rewrittenPayload !== payload
? replaceSseDataPayload(next.block, rewrittenPayload!)
const block = payload !== null && rewrittenPayload !== null && rewrittenPayload !== payload
? replaceSseDataPayload(next.block, rewrittenPayload)
: next.block;
out += block + next.delimiter;
frameBuffer = next.rest;
}
if (rewriteBudget) {
const remaining = rewriteEncoder!.encode(frameBuffer).byteLength;
Expand All @@ -149,6 +153,14 @@ export function relaySseEagerBounded(
const payload = sseDataPayload(tail);
if (payload !== null) {
const rewrittenPayload = rewrite(payload);
if (rewrittenPayload === null) {
frameBuffer = "";
if (rewriteBudget && frameBufferBytes > 0) {
rewriteBudget.releaseRetained(frameBufferBytes, { kind: "live_transient" });
}
frameBufferBytes = 0;
return rewriteEncoder!.encode("");
}
if (rewrittenPayload !== payload) tail = replaceSseDataPayload(tail, rewrittenPayload);
}
frameBuffer = "";
Expand Down Expand Up @@ -222,6 +234,12 @@ export function relaySseEagerBounded(
queuedBytes += tail.byteLength;
try { controllerRef?.enqueue(tail); } catch { /* client already gone */ }
}
const trailer = typeof opts?.trailer === "function" ? opts.trailer() : opts?.trailer;
if (trailer && !cancelled) {
const trailerBytes = new TextEncoder().encode(trailer);
queuedBytes += trailerBytes.byteLength;
try { controllerRef?.enqueue(trailerBytes); } catch { /* client already gone */ }
}
}
if (!hooks.sawTerminal() && !cancelled && !upstream.signal.aborted) {
syntheticKind = "incomplete";
Expand Down
Loading
Loading