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,6 +84,7 @@ account を削除しても mapping は保持され、同じ id を再追加す
| `modelSupportsReasoningSummaries?` | `Record<string, boolean>` |モデルを `false` に設定して、概要の広告を停止し、概要配信フィールドを削除します。 |
| `modelReasoningSummaryDelivery?` | `Record<string, "sequential" \| "sequential_cutoff" \| "concurrent" \| "concurrent_cutoff">` |モデルごとの応答配信列挙型。既存の配信フィールドを書き換えます。 |
| `modelAdapters?` | `Record<string, string>` | 混合配線ゲートウェイのモデルごとの `openai-chat` または `openai-responses` 配線オーバーライド。明示的なエントリはレジストリのデフォルトを破ります。DeepSeek のプリセットは `deepseek-v4-flash` のネイティブ Responses を選択でき、GitHub Copilot は GPT-5 ファミリー (`gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`) を Responses 専用デフォルトとして宣言します。これらのモデルはエージェント トラフィックで `/chat/completions` を拒否するためです。`gpt-5.4-nano` のようなビルトイン デフォルトのないモデルはここでオプトインできます。単線アップストリーム ピンと正規の ChatGPT 転送はオーバーライドを拒否します。 |
| `modelResponsesUpstreamStreaming?` | `Record<string, boolean>` | forward 以外の `openai-responses` プロバイダー向けモデル別 upstream Responses ポリシーです。`false` は upstream に bounded JSON を要求し、検証済み terminal オブジェクトを streaming client 用 Responses イベントへ再構成します。`true` は registry の `false` 既定値を明示的に上書きします。照合は大文字小文字を区別せず、public virtual id を優先し、最終 wire-model id をフォールバックに使います。この correctness-first fallback では incremental delta がなくなり、bounded JSON のサイズと timeout 制限が適用されます。 |
| `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>` |ラベルを推論するためのプロバイダー全体のワイヤ エイリアス。 |
| `modelReasoningEffortMap?` | `Record<string, Record<string, string>>` |推論ラベルのモデルごとのワイヤ エイリアス。 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ managed map을 활성화하면 privacy-safe selector를 만들고, 이후 계정
| `modelSupportsReasoningSummaries?` | `Record<string, boolean>` | 모델을 `false`로 두면 summary 광고를 멈추고 summary 전달 필드를 제거합니다. |
| `modelReasoningSummaryDelivery?` | `Record<string, "sequential" \| "sequential_cutoff" \| "concurrent" \| "concurrent_cutoff">` | 모델별 Responses 전달 enum입니다. 기존 delivery 필드를 다시 씁니다. |
| `modelAdapters?` | `Record<string, string>` | 혼합 와이어 게이트웨이를 위한 모델별 `openai-chat` 또는 `openai-responses` 와이어 재정의입니다. 명시적 항목이 레지스트리 기본값보다 우선합니다. DeepSeek 프리셋은 `deepseek-v4-flash`에 네이티브 Responses를 선택할 수 있고, GitHub Copilot은 GPT-5 계열(`gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`)을 Responses 전용 기본값으로 선언합니다. 이 모델들은 에이전트 트래픽에서 `/chat/completions`를 거부하기 때문입니다. `gpt-5.4-nano`처럼 기본값이 없는 모델은 여기서 직접 옵트인할 수 있습니다. 단일 와이어 상위 항목과 정식 ChatGPT forward는 재정의를 거부합니다. |
| `modelResponsesUpstreamStreaming?` | `Record<string, boolean>` | Forward가 아닌 `openai-responses` provider의 모델별 upstream Responses 정책입니다. `false`는 upstream에 bounded JSON을 요청하고 검증된 terminal 객체를 streaming client용 Responses event로 다시 구성합니다. `true`는 registry의 `false` 기본값을 명시적으로 해제합니다. 대소문자를 구분하지 않으며 public virtual id가 먼저, 최종 wire-model id가 fallback으로 일치합니다. 이 correctness-first fallback에서는 incremental delta가 사라지고 bounded JSON 크기·시간 제한이 적용됩니다. |
| `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 동작을 유지합니다. |
| `reasoningEffortMap?` | `Record<string, string>` | reasoning 레이블의 공급자 전반 와이어 별칭입니다. |
| `modelReasoningEffortMap?` | `Record<string, Record<string, string>>` | reasoning 레이블의 모델별 와이어 별칭입니다. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ differing backup and rewrites known legacy namespaced selected ids to bare ids.
| `modelSupportsReasoningSummaries?` | `Record<string, boolean>` | Set a model to `false` to stop advertising summaries and strip summary-delivery fields. |
| `modelReasoningSummaryDelivery?` | `Record<string, "sequential" \| "sequential_cutoff" \| "concurrent" \| "concurrent_cutoff">` | Per-model Responses delivery enum; rewrites an existing delivery field. |
| `modelAdapters?` | `Record<string, string>` | Per-model `openai-chat` or `openai-responses` wire override for mixed-wire gateways. Explicit entries beat registry defaults; DeepSeek's preset can select native Responses for `deepseek-v4-flash`, and GitHub Copilot declares Responses-only defaults for its GPT-5 family (`gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`) because those models reject `/chat/completions` for agent traffic. Models without a built-in default (for example `gpt-5.4-nano`) can be opted in here. Single-wire upstream pins and canonical ChatGPT forward reject overrides. |
| `modelResponsesUpstreamStreaming?` | `Record<string, boolean>` | Per-model Responses upstream policy for non-forward `openai-responses` providers. `false` asks upstream for bounded JSON and reframes the validated terminal object as Responses events for a streaming client; `true` explicitly overrides a registry `false` default. Matching is case-insensitive and supports public virtual ids with their final wire-model id as fallback. This correctness-first fallback removes incremental deltas and remains subject to the bounded JSON size and timeout limits. |
| `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. |
| `modelReasoningEffortMap?` | `Record<string, Record<string, string>>` | Per-model wire aliases for reasoning labels. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ cross-route credential fallback не существует. Строки API GPT-
| `modelSupportsReasoningSummaries?` | `Record<string, boolean>` | Установите `false` для модели, чтобы перестать рекламировать summary и вырезать поля доставки summary. |
| `modelReasoningSummaryDelivery?` | `Record<string, "sequential" \| "sequential_cutoff" \| "concurrent" \| "concurrent_cutoff">` | Responses delivery enum по моделям; переписывает уже существующее поле delivery. |
| `modelAdapters?` | `Record<string, string>` | Wire-override по модели для `openai-chat` или `openai-responses` в gateway с несколькими wire-форматами. Явные записи имеют приоритет над default'ами registry; preset DeepSeek может выбирать native Responses для `deepseek-v4-flash`, а GitHub Copilot объявляет Responses-only default'ы для семейства GPT-5 (`gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`), потому что эти модели отклоняют `/chat/completions` для агентного трафика. Модели без встроенного default'а (например, `gpt-5.4-nano`) можно включить здесь. Single-wire upstream pin'ы и canonical ChatGPT forward override не принимают. |
| `modelResponsesUpstreamStreaming?` | `Record<string, boolean>` | Модельная политика upstream Responses для non-forward провайдеров `openai-responses`. `false` запрашивает bounded JSON и преобразует проверенный terminal-объект в события Responses для streaming client; `true` явно перекрывает registry default `false`. Сопоставление не зависит от регистра: сначала используется public virtual id, затем итоговый wire-model id. Этот correctness-first fallback убирает incremental delta и подчиняется лимитам размера и timeout bounded JSON. |
| `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>` | Provider-wide wire-alias'ы для reasoning-label'ов. |
| `modelReasoningEffortMap?` | `Record<string, Record<string, string>>` | Wire-alias'ы для reasoning-label'ов по отдельным моделям. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ selector,而不是分配一个新名称。
| `modelSupportsReasoningSummaries?` | `Record<string, boolean>` | 将某个模型设为 `false`,即可停止暴露摘要并移除摘要交付字段。 |
| `modelReasoningSummaryDelivery?` | `Record<string, "sequential" \| "sequential_cutoff" \| "concurrent" \| "concurrent_cutoff">` | 按模型设置的 Responses 交付枚举;会重写现有的 delivery 字段。 |
| `modelAdapters?` | `Record<string, string>` | 按模型设置的 `openai-chat` 或 `openai-responses` 线协议覆盖项,用于混合线协议网关。显式条目优先于注册表默认值;DeepSeek 预设可以为 `deepseek-v4-flash` 选择原生 Responses,GitHub Copilot 则为 GPT-5 系列(`gpt-5.3-codex`、`gpt-5.4`、`gpt-5.4-mini`、`gpt-5.5`、`gpt-5.6-luna`、`gpt-5.6-sol`、`gpt-5.6-terra`)声明了 Responses 专用默认值,因为这些模型在代理流量下会拒绝 `/chat/completions`。没有内置默认值的模型(例如 `gpt-5.4-nano`)可以在此手动启用。单一线协议上游固定项和规范 ChatGPT forward 会拒绝覆盖。 |
| `modelResponsesUpstreamStreaming?` | `Record<string, boolean>` | 非 forward `openai-responses` provider 的逐模型 upstream Responses 策略。`false` 请求 bounded JSON,并将验证后的 terminal 对象重构为 streaming client 所需的 Responses 事件;`true` 显式覆盖 registry 的 `false` 默认值。匹配不区分大小写,优先使用 public virtual id,并以最终 wire-model id 作为回退。此 correctness-first fallback 不提供 incremental delta,并受 bounded JSON 大小与 timeout 限制。 |
| `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>` | 提供者级、用于推理标签的线协议别名。 |
| `modelReasoningEffortMap?` | `Record<string, Record<string, string>>` | 按模型设置的推理标签线协议别名。 |
Expand Down
91 changes: 91 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,7 @@ const providerConfigSchema = z.object({
requiresAdjacentResponsesToolResults: z.boolean().optional(),
supportsServiceTier: z.boolean().optional(),
preserveResponsesReasoningContent: z.boolean().optional(),
modelResponsesUpstreamStreaming: z.record(z.string(), z.boolean()).optional(),
allowPrivateNetwork: z.boolean().optional(),
retryOn429: retryOn429PolicySchema.optional(),
codexAccountMode: z.enum(["pool", "direct"]).optional(),
Expand Down Expand Up @@ -908,6 +909,83 @@ export function modelAdapterRecordConfigError(
return null;
}

/** Validate the opt-in bounded-JSON policy against the model's effective Responses wire. */
export function modelResponsesUpstreamStreamingConfigError(
value: unknown,
field: string,
providerName: string,
provider: { adapter?: unknown; authMode?: unknown; baseUrl?: unknown; modelAdapters?: unknown },
): string | null {
const shapeError = booleanRecordConfigError(value, field);
if (shapeError) return shapeError;
const entries = Object.entries((value ?? {}) as Record<string, boolean>);
if (entries.length === 0) return null;

const registry = getProviderRegistryEntry(providerName);
const registryTransportMatches = typeof provider.baseUrl === "string"
&& providerMatchesRegistryTransport(providerName, {
baseUrl: provider.baseUrl,
adapter: provider.adapter as OcxProviderConfig["adapter"],
...(typeof provider.authMode === "string"
? { authMode: provider.authMode as OcxProviderConfig["authMode"] }
: {}),
});
const effectiveForwardAuth = registryTransportMatches
? registry?.authKind === "forward"
: provider.authMode === "forward";
if (effectiveForwardAuth) {
return `${field} is not supported on forward-auth Responses providers`;
}

const resolveEffectiveWire = (modelId: string, currentWire: unknown): unknown => {
const pinned = pinnedWireAdapter(providerName, modelId);
if (pinned) return pinned;
const configured = provider.modelAdapters && typeof provider.modelAdapters === "object"
&& !Array.isArray(provider.modelAdapters)
? (provider.modelAdapters as Record<string, unknown>)[modelId]
: undefined;
if (typeof configured === "string" && MODEL_ADAPTER_OVERRIDE_ALLOWED.has(configured)) {
return configured;
}
const registryDefault = typeof currentWire === "string" && typeof provider.baseUrl === "string"
? providerModelWireDefault(
providerName,
{
baseUrl: provider.baseUrl,
adapter: currentWire,
...(typeof provider.authMode === "string"
? { authMode: provider.authMode as OcxProviderConfig["authMode"] }
: {}),
},
modelId,
MODEL_ADAPTER_OVERRIDE_ALLOWED,
"responses",
)
: undefined;
return registryDefault ?? currentWire;
};

for (const [modelId] of entries) {
const baseWire = registryTransportMatches ? registry?.adapter ?? provider.adapter : provider.adapter;
const virtualSelectedModelId = Object.keys(registry?.virtualModels ?? {}).find(
candidate => candidate.toLowerCase() === modelId.trim().toLowerCase(),
);
const effectiveSelectedModelId = virtualSelectedModelId ?? modelId;
let effectiveWire = resolveEffectiveWire(effectiveSelectedModelId, baseWire);
const virtualWireModel = resolveOpenAiVirtualModel(
providerName,
effectiveSelectedModelId,
)?.wireModelId;
if (virtualWireModel && virtualWireModel !== effectiveSelectedModelId) {
effectiveWire = resolveEffectiveWire(virtualWireModel, effectiveWire);
}
if (effectiveWire !== "openai-responses") {
return `${field}.${modelId} requires the openai-responses wire`;
}
}
return null;
}

const CODEX_ACCOUNT_NAMESPACES_RECORD_ERROR =
"codexAccountNamespaces must be a plain object mapping account selectors to Codex account ids";
const CODEX_ACCOUNT_NAMESPACE_KEY_ERROR =
Expand Down Expand Up @@ -1249,6 +1327,19 @@ const configSchema = z.object({
message: modelAdaptersError,
});
}
const responsesUpstreamStreamingError = modelResponsesUpstreamStreamingConfigError(
(provider as { modelResponsesUpstreamStreaming?: unknown }).modelResponsesUpstreamStreaming,
"modelResponsesUpstreamStreaming",
name,
provider,
);
if (responsesUpstreamStreamingError) {
ctx.addIssue({
code: "custom",
path: ["providers", name, "modelResponsesUpstreamStreaming"],
message: responsesUpstreamStreamingError,
});
}
const preferHostedToolsError = modelPreferHostedToolsConfigError(
(provider as { modelPreferHostedTools?: unknown }).modelPreferHostedTools,
"modelPreferHostedTools",
Expand Down
Loading
Loading