Skip to content
Open
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
1 change: 1 addition & 0 deletions docs-site/src/content/docs/ja/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ token の代わりに使えます。すべての候補は timing side channel
| `modelReasoningEfforts?` | `Record<string,string[]>` | モデル別 reasoning ラベル。空配列はそのモデルの effort control を隠します。 |
| `modelSupportsReasoningSummaries?` | `Record<string,boolean>` | モデル別 reasoning summary capability。`false` にすると summary 対応を広告せず、`openai-responses` リクエスト前に summary-delivery フィールドを除去します。 |
| `modelReasoningSummaryDelivery?` | `Record<string,"sequential" \| "sequential_cutoff" \| "concurrent" \| "concurrent_cutoff">` | モデル別の Responses delivery enum。設定したモデルは summary 対応を維持し、既存の `stream_options.reasoning_summary_delivery` だけを書き換えます。同じモデルの summary capability を `false` にはできません。 |
| `modelPreferHostedTools?` | `Record<string,string[]>` | hosted tool namespace を予約する非 forward Responses gateway 向けの完全一致モデル opt-in。現在は `["image_generation"]` のみを受け付けます。一致したモデルは `openai-responses` wire を使い、その hosted tool をサポートする必要があります。競合するクライアント `image_gen` 宣言を除去し、呼び出し元の tool choice を維持するため selector も書き換えます。OpenAI API の仮想 `-pro` モデルでは、まず選択した公開 ID に一致させ、解決後のベース wire-model ID をフォールバックとして使用します。`modelAdapters` は公開 ID、次にベース ID の順に解決し、後者の結果が最終 wire を決めます。未設定のモデルは通常の alias 動作を維持します。 |
| `reasoningEffortMap?` | `Record<string,string>` | プロバイダー単位の reasoning ラベル wire alias。上流が別の値を要求するときだけ使います。 |
| `modelReasoningEffortMap?` | `Record<string,Record<string,string>>` | モデル別 reasoning ラベル wire alias。 |
| `noReasoningModels?` | `string[]` | reasoning/thinking パラメータを拒否するモデル。アダプターが `reasoning_effort` を削除します。 |
Expand Down
1 change: 1 addition & 0 deletions docs-site/src/content/docs/ko/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ token 대신 쓸 수 있습니다. 모든 후보는 timing side channel을 막
| `modelReasoningEfforts?` | `Record<string,string[]>` | 모델별 reasoning 레이블. 빈 배열은 해당 모델의 effort control을 숨깁니다. |
| `modelSupportsReasoningSummaries?` | `Record<string,boolean>` | 모델별 reasoning summary capability. 모델 값을 `false`로 두면 summary 지원을 알리지 않고 `openai-responses` 요청 전에 summary-delivery 필드를 제거합니다. |
| `modelReasoningSummaryDelivery?` | `Record<string,"sequential" \| "sequential_cutoff" \| "concurrent" \| "concurrent_cutoff">` | 모델별 Responses delivery enum입니다. 설정된 모델은 summary 지원을 유지하며 기존 `stream_options.reasoning_summary_delivery` 값만 바꿉니다. 같은 모델의 summary capability를 `false`로 설정할 수 없습니다. |
| `modelPreferHostedTools?` | `Record<string,string[]>` | hosted tool namespace를 예약하는 non-forward Responses gateway용 정확한 모델 ID opt-in입니다. 현재 `["image_generation"]`만 허용하며, 일치하는 모델은 `openai-responses` wire를 사용하고 해당 hosted tool을 지원해야 합니다. 충돌하는 클라이언트 `image_gen` 선언을 제거하고 호출자의 tool choice를 유지하도록 selector도 다시 씁니다. OpenAI API 가상 `-pro` 모델은 선택한 공개 ID를 먼저 일치시키고, 해석된 기본 wire-model ID를 대체값으로 사용합니다. `modelAdapters`는 공개 ID를 먼저, 그 다음 기본 ID를 해석하며, 두 번째 결과가 최종 wire를 결정합니다. 설정하지 않은 모델은 일반 alias 동작을 유지합니다. |
| `modelAdapters?` | `Record<string,string>` | 여러 wire를 쓰는 모델이 한 게이트웨이에 섞여 있을 때의 모델별 wire 지정. 키는 upstream native 모델 ID이고 값은 `openai-chat` 또는 `openai-responses`만 허용합니다. `web_search` 같은 hosted tool 때문에 한 모델만 Responses API가 필요할 때 씁니다. upstream이 wire를 고정한 모델과 canonical ChatGPT forward provider에서는 override가 거부됩니다. |
| `reasoningEffortMap?` | `Record<string,string>` | 프로바이더 단위 reasoning 레이블 wire alias. 업스트림이 다른 값을 요구할 때만 사용합니다. |
| `modelReasoningEffortMap?` | `Record<string,Record<string,string>>` | 모델별 reasoning 레이블 wire alias. |
Expand Down
1 change: 1 addition & 0 deletions docs-site/src/content/docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ or bind the forward explicitly to loopback (`ssh -L 127.0.0.1:20100:localhost:10
| `modelSupportsReasoningSummaries?` | `Record<string,boolean>` | Model-specific reasoning-summary capability. Set a model to `false` to stop advertising summaries and strip summary-delivery fields before an `openai-responses` request. |
| `modelReasoningSummaryDelivery?` | `Record<string,"sequential" \| "sequential_cutoff" \| "concurrent" \| "concurrent_cutoff">` | Model-specific Responses delivery enum. A configured model stays summary-capable and only an existing `stream_options.reasoning_summary_delivery` is rewritten; do not combine it with a `false` summary capability for the same model. |
| `modelAdapters?` | `Record<string,string>` | Per-model wire override for a gateway that fronts models speaking different wires. Keys are upstream native model ids; values must be `openai-chat` or `openai-responses`. Useful when one model needs the Responses API for hosted tools such as `web_search` while its siblings are fine on chat completions. Models the upstream pins to a single wire, and the canonical ChatGPT forward provider, reject overrides. |
| `modelPreferHostedTools?` | `Record<string,string[]>` | Exact-model opt-in for non-forward Responses gateways that reserve a hosted-tool namespace. Currently accepts only `["image_generation"]`; a matching model must use the `openai-responses` wire and support that hosted tool. It removes colliding client `image_gen` declarations and rewrites their selectors to preserve caller tool choice. For OpenAI API virtual `-pro` models, the selected public ID is matched first and the resolved base wire-model ID is a fallback. `modelAdapters` resolves the public ID first, then the base ID; the second resolution determines the final wire. Other models retain normal alias behavior. |
| `reasoningEffortMap?` | `Record<string,string>` | Provider-wide wire aliases for reasoning labels. Use only when the upstream expects a different value. |
| `modelReasoningEffortMap?` | `Record<string,Record<string,string>>` | Model-specific wire aliases for reasoning labels. |
| `noReasoningModels?` | `string[]` | Models that reject a reasoning/thinking param — the adapter drops `reasoning_effort` for them. |
Expand Down
1 change: 1 addition & 0 deletions docs-site/src/content/docs/ru/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ x-opencodex-api-key: your-secret-token
| `modelReasoningEfforts?` | `Record<string,string[]>` | Метки рассуждений для конкретных моделей. Пустой список скрывает управление уровнем рассуждений для этой модели. |
| `modelSupportsReasoningSummaries?` | `Record<string,boolean>` | Поддержка reasoning summary для отдельных моделей. Значение `false` отключает объявление summary и удаляет поля summary-delivery перед запросом `openai-responses`. |
| `modelReasoningSummaryDelivery?` | `Record<string,"sequential" \| "sequential_cutoff" \| "concurrent" \| "concurrent_cutoff">` | Enum доставки Responses для отдельных моделей. Настроенная модель сохраняет поддержку summary, а адаптер меняет только уже существующее `stream_options.reasoning_summary_delivery`. Нельзя одновременно отключить summary для той же модели. |
| `modelPreferHostedTools?` | `Record<string,string[]>` | Opt-in для точного model ID в non-forward Responses gateway, который резервирует namespace hosted tool. Сейчас допускается только `["image_generation"]`; совпавшая модель должна использовать wire `openai-responses` и поддерживать этот hosted tool. Прокси удаляет конфликтующие клиентские объявления `image_gen` и переписывает их selectors, сохраняя caller tool choice. Для виртуальных моделей OpenAI API `-pro` сначала сопоставляется выбранный публичный ID, а затем в качестве fallback используется ID базовой wire-модели. `modelAdapters` сначала разрешается по публичному ID, затем по базовому ID; второй результат определяет итоговый wire. Остальные модели сохраняют обычное alias-поведение. |
| `reasoningEffortMap?` | `Record<string,string>` | Wire-алиасы меток рассуждений на уровне провайдера. Используйте только когда вышестоящая сторона ожидает другое значение. |
| `modelReasoningEffortMap?` | `Record<string,Record<string,string>>` | Wire-алиасы меток рассуждений для конкретных моделей. |
| `noReasoningModels?` | `string[]` | Модели, отклоняющие параметр reasoning/thinking — адаптер удаляет для них `reasoning_effort`. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ x-opencodex-api-key: your-secret-token
| `modelReasoningEfforts?` | `Record<string,string[]>` | 模型级 reasoning label。空数组会隐藏该模型的 effort 控件。 |
| `modelSupportsReasoningSummaries?` | `Record<string,boolean>` | 模型级 reasoning summary 能力。设为 `false` 时不再声明 summary 支持,并在 `openai-responses` 请求前移除 summary-delivery 字段。 |
| `modelReasoningSummaryDelivery?` | `Record<string,"sequential" \| "sequential_cutoff" \| "concurrent" \| "concurrent_cutoff">` | 模型级 Responses delivery enum。已配置模型保持 summary 能力,适配器只改写现有的 `stream_options.reasoning_summary_delivery`;同一模型不能同时将 summary 能力设为 `false`。 |
| `modelPreferHostedTools?` | `Record<string,string[]>` | 非 forward Responses gateway 的精确模型 ID opt-in,用于上游预留 hosted tool namespace 的情况。目前只支持 `["image_generation"]`;匹配模型必须使用 `openai-responses` wire 且支持该 hosted 工具。它会移除冲突的客户端 `image_gen` 声明,并改写其 selector 以保持调用方的 tool choice。对于 OpenAI API 的虚拟 `-pro` 模型,先匹配所选公开 ID,未命中时才使用解析出的基础 wire-model ID 作为回退。`modelAdapters` 会先按公开 ID、再按基础 ID 解析;后一次结果决定最终 wire。未配置模型保持普通 alias 行为。 |
| `reasoningEffortMap?` | `Record<string,string>` | provider 级 reasoning label wire alias。只在上游需要不同值时使用。 |
| `modelReasoningEffortMap?` | `Record<string,Record<string,string>>` | 模型级 reasoning label wire alias。 |
| `noReasoningModels?` | `string[]` | 拒绝 reasoning/thinking 参数的模型;adapter 会为它们移除 `reasoning_effort`。 |
Expand Down
136 changes: 118 additions & 18 deletions src/adapters/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxP
import { catalogModelSupportsReasoningSummaries } from "../codex/catalog";
import { COMPACT_PROMPT, decodeCompactionSummary, SUMMARY_PREFIX } from "../responses/compaction";
import { collectResponsesToolGroups } from "../responses/tool-groups";
import { isHostedToolUnsupportedForModel } from "../responses/hosted-tool-policy";
import { decodeServerSentEvents } from "../lib/sse-decoder";
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
import { OCX_REASONING_PREFIX } from "../responses/reasoning-envelope";
Expand Down Expand Up @@ -147,18 +148,6 @@ function scrubOcxCompactionItems(body: unknown): unknown {
return changed ? { ...body, input } : body;
}

/**
* Hosted (OpenAI-executed) tool types that specific native slugs reject at request time. Codex
* attaches these for app skills (e.g. `image_generation` for imagegen) regardless of the target
* model, and the passthrough path forwards the raw body untouched — so a slug that doesn't support
* the tool 400s (`Tool 'image_generation' is not supported with gpt-5.3-codex-spark.`). Each entry
* maps a model-slug matcher to the hosted tool types that must be stripped before forwarding.
* Extend this when another native slug rejects a hosted tool (e.g. `code_interpreter`).
*/
const UNSUPPORTED_HOSTED_TOOLS: ReadonlyArray<{ match: (model: string) => boolean; tools: ReadonlySet<string> }> = [
{ match: model => model.includes("codex-spark"), tools: new Set(["image_generation", "tool_search"]) },
];

/**
* Strip unsupported `reasoning` sub-parameters for native slugs that reject them (e.g. Spark).
* codex-rs injects `reasoning.context` and `reasoning.summary` based on catalog flags; Spark's
Expand Down Expand Up @@ -532,6 +521,114 @@ function declaresImageGenClientTool(tool: unknown): boolean {
return isImageGenClientName(tool.name);
}

/** Rewrite client image-gen selectors to the hosted tool without widening caller restrictions. */
function preferHostedImageGenToolChoice(toolChoice: unknown): unknown {
if (!isPlainObject(toolChoice)) return toolChoice;
if ((toolChoice.type === "function" || toolChoice.type === "custom") && typeof toolChoice.name === "string") {
return isImageGenClientName(toolChoice.name) ? { type: HOSTED_IMAGE_GENERATION_TOOL } : toolChoice;
}
if (toolChoice.type !== "allowed_tools" || !Array.isArray(toolChoice.tools)) return toolChoice;
const hasHostedImageTool = toolChoice.tools.some(tool => isPlainObject(tool) && tool.type === HOSTED_IMAGE_GENERATION_TOOL);
let changed = false;
let addedHostedImageTool = false;
const tools: unknown[] = [];
for (const tool of toolChoice.tools) {
const isClientImageTool = isPlainObject(tool)
&& (tool.type === "function" || tool.type === "custom")
&& typeof tool.name === "string"
&& isImageGenClientName(tool.name);
if (!isClientImageTool) {
tools.push(tool);
continue;
}
changed = true;
if (!hasHostedImageTool && !addedHostedImageTool) {
tools.push({ type: HOSTED_IMAGE_GENERATION_TOOL });
addedHostedImageTool = true;
}
}
return changed ? { ...toolChoice, tools } : toolChoice;
}

/**
* Some Responses-compatible gateways reserve the hosted image namespace even when the request
* does not explicitly declare `image_generation`. For an explicitly configured model, remove only
* colliding client declarations so the gateway's hosted tool can take precedence.
*/
function preferConfiguredHostedTools(
body: unknown,
provider: OcxProviderConfig,
modelId: string,
selectedModelId?: string,
): unknown {
// A virtual model's advertised id takes precedence over its resolved wire-model id.
const preferredTools = (selectedModelId ? provider.modelPreferHostedTools?.[selectedModelId] : undefined)
?? provider.modelPreferHostedTools?.[modelId];
if (!preferredTools?.includes(HOSTED_IMAGE_GENERATION_TOOL) || !isPlainObject(body)) return body;

const stripGroup = (tools: unknown[]): unknown[] => {
const filtered = tools.filter(tool => !declaresImageGenClientTool(tool));
return filtered.length === tools.length ? tools : filtered;
};

let changed = false;
let tools = body.tools;
let strippedTopLevelImageGenTool = false;
if (Array.isArray(body.tools)) {
tools = stripGroup(body.tools);
strippedTopLevelImageGenTool = tools !== body.tools;
changed ||= strippedTopLevelImageGenTool;
}

let input = body.input;
let strippedAdditionalToolsIndex = -1;
if (Array.isArray(body.input)) {
let nestedChanged = false;
const mappedInput = body.input.map((item, index) => {
if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) return item;
const nestedTools = stripGroup(item.tools);
if (nestedTools === item.tools) return item;
if (strippedAdditionalToolsIndex === -1) strippedAdditionalToolsIndex = index;
nestedChanged = true;
return { ...item, tools: nestedTools };
});
if (nestedChanged) {
input = mappedInput;
changed = true;
}
}

const hasToolChoice = Object.hasOwn(body, "tool_choice");
const toolChoice = hasToolChoice ? preferHostedImageGenToolChoice(body.tool_choice) : body.tool_choice;
const toolChoiceChanged = hasToolChoice && toolChoice !== body.tool_choice;
const hasHostedImageGenTool = (toolGroup: unknown): boolean => Array.isArray(toolGroup)
&& toolGroup.some(tool => isPlainObject(tool) && tool.type === HOSTED_IMAGE_GENERATION_TOOL);
const hasHostedImageGenDeclaration = hasHostedImageGenTool(tools)
|| (Array.isArray(input) && input.some(item => isPlainObject(item)
&& item.type === "additional_tools"
&& hasHostedImageGenTool(item.tools)));
if ((strippedTopLevelImageGenTool || strippedAdditionalToolsIndex >= 0) && !hasHostedImageGenDeclaration) {
if (strippedTopLevelImageGenTool && Array.isArray(tools)) {
tools = [...tools, { type: HOSTED_IMAGE_GENERATION_TOOL }];
} else if (strippedAdditionalToolsIndex >= 0 && Array.isArray(input)) {
input = input.map((item, index) => index === strippedAdditionalToolsIndex
&& isPlainObject(item)
&& Array.isArray(item.tools)
? { ...item, tools: [...item.tools, { type: HOSTED_IMAGE_GENERATION_TOOL }] }
: item);
}
}
changed ||= toolChoiceChanged;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (!changed) return body;
const next: Record<string, unknown> = {
...body,
...(Array.isArray(body.tools) ? { tools } : {}),
...(Array.isArray(body.input) ? { input } : {}),
};
if (toolChoiceChanged) next.tool_choice = toolChoice;
return next;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Lower one complete Codex image-gen namespace to public Responses function tools.
*
Expand Down Expand Up @@ -790,13 +887,9 @@ function normalizeImageGenClientTools(body: unknown): unknown {
function stripUnsupportedHostedTools(body: unknown): unknown {
if (!isPlainObject(body) || !Array.isArray(body.tools)) return body;
const model = typeof body.model === "string" ? body.model : "";
const unsupported = UNSUPPORTED_HOSTED_TOOLS.filter(e => e.match(model));
if (unsupported.length === 0) return body;

const tools = body.tools.filter(t => {
const type = isPlainObject(t) && typeof t.type === "string" ? t.type : undefined;
if (!type) return true;
return !unsupported.some(e => e.tools.has(type));
return !type || !isHostedToolUnsupportedForModel(model, type);
});
return tools.length === body.tools.length ? body : { ...body, tools };
}
Expand Down Expand Up @@ -926,8 +1019,15 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
if (forward) {
outBody = repairOrphanedInputItems(outBody, unexpandedMiss);
outBody = stripUnsupportedForwardParams(outBody);
} else {
outBody = preferConfiguredHostedTools(
outBody,
provider,
parsed.modelId,
parsed._openAiVirtualSelectedModelId,
);
outBody = normalizeImageGenClientTools(outBody);
}
else outBody = normalizeImageGenClientTools(outBody);
if (forward || parsed._previousResponseInputExpanded === true) {
outBody = repairOversizedReplayCallIds(outBody);
}
Expand Down
Loading
Loading