diff --git a/docs-site/src/content/docs/fr/reference/configuration/providers.md b/docs-site/src/content/docs/fr/reference/configuration/providers.md index 3ca5776d2a2..99ed2fa876b 100644 --- a/docs-site/src/content/docs/fr/reference/configuration/providers.md +++ b/docs-site/src/content/docs/fr/reference/configuration/providers.md @@ -85,6 +85,7 @@ sauvegarde dont le contenu diffère, puis réécrit en identifiants sans préfix | `baseUrl` | `string` | URL de base de l'API en amont. La plupart des points de terminaison fixes intégrés ignorent une valeur incompatible ; les préréglages de clés protégés contre les collisions préservent une ancienne destination personnalisée portant le même nom. | | `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, models? }` | Cadencement facultatif du démarrage des requêtes sortantes côté client, distinct de l’utilisation, de la facturation et des indicateurs de limitation en amont. Le nombre de requêtes par minute est converti en intervalle régulier ; `minIntervalMs` peut imposer un intervalle plus long. Les limites du fournisseur s’appliquent à tous ses modèles, tandis que les entrées `models` ciblent les identifiants exacts des modèles en amont, par exemple `nvidia/llama-3.1-nemotron-ultra-253b-v1`, et ne peuvent qu’ajouter du délai. L’attente dans la file ne consomme pas le délai d’expiration des en-têtes de réponse en amont. Les requêtes HTTP, Responses WebSocket et les distributions explicites `fetchResponse`/`runTurn` des adaptateurs sont couvertes. | | `responsesPath?` | `string` | Chemin de ressource relatif pour les requêtes d'authentification par clé `openai-responses`. Il doit commencer par `/` et ne contenir aucun schéma, requête ou fragment. | +| `chatCompletionsPath?` | `string` | Chemin de ressource relatif pour les requêtes `openai-chat`, miroir de `responsesPath` et soumis aux mêmes règles de forme. Nécessaire lorsqu'un même service en amont sert Chat Completions et Responses sous des préfixes différents : un override wire par modèle change l'adaptateur sans toucher `baseUrl`, donc sans ce réglage une requête Chat activée serait envoyée vers la base Responses. L'exemple fourni est Z.AI. | | `upstreamWebsocket?` | `boolean` | Active le transport Responses WebSocket en amont pour les requêtes `openai-responses` (désactivé par défaut). Lorsque le service en amont prend en charge ce protocole, les requêtes POST en streaming utilisent le chemin Responses configuré (par défaut `/v1/responses`) via WSS avec une base HTTPS, puis sont reconverties en SSE. Les fournisseurs en mode forward utilisent `{baseUrl}/responses` ; les fournisseurs avec clé utilisent `responsesPath`, ou le repli historique `/v1/responses`. Une base HTTP reste en SSE ; les chemins qui ne sont pas Responses et les requêtes `openai-chat` restent en HTTP. | | `supportsServiceTier?` | `boolean` | Repli à trois états pour la capacité `service_tier`. `true` : le mode rapide peut injecter le champ et les valeurs de l’appelant sont conservées. `false` : le champ est retiré et jamais injecté, et aucune déclaration précise de modèle ne peut le réactiver. Absent : le fournisseur n’est pas classé ; les valeurs de l’appelant sont conservées intactes et le mode rapide n’injecte rien, sauf pour un modèle exact activé. Le registre classe OpenAI canonique comme `true`, et DeepSeek ainsi que Volcengine Ark comme `false`. Ne le définissez explicitement que pour les passerelles personnalisées qui prennent réellement en charge les niveaux. Les routes Chat exigent en plus une autorisation globale ou propre au modèle. | | `modelSupportsServiceTier?` | `Record` | Remplacements de capacité par identifiant exact de modèle en amont. La valeur exacte `true` autorise ce modèle Chat même sans `chatServiceTier` ; `false` restreint les valeurs globales et l’autorisation Chat. Une valeur globale explicite `supportsServiceTier: false` reste fermée et ne peut pas être réactivée. Les modèles non déclarés suivent le comportement global. La requête de gestion `PATCH /api/providers` fusionne les entrées et accepte `null` pour en supprimer une. | @@ -154,6 +155,8 @@ sauvegarde dont le contenu diffère, puis réécrit en identifiants sans préfix | `unsafeAllowNativeLocalExec?` | `boolean` | Ancien booléen de Cursor, équivalent à `nativeLocalExec: "on"` uniquement lorsque le champ plus récent n'est pas défini. | | `nativeLocalExec?` | `"off" \| "codex-sandbox" \| "on"` | Politique d'exécution locale de Cursor. `off` est la valeur par défaut ; actuellement, `codex-sandbox` échoue de manière sûre comme `off`. | +La création et le remplacement d’un fournisseur (`POST /api/providers`) valident `responsesPath` et `chatCompletionsPath` avant de modifier la configuration en mémoire ou sur disque. `PATCH /api/providers?name=` fusionne le corps de la requête avec le fournisseur enregistré ; les mises à jour qui touchent des champs autres que `disabled` — à l’exception des mises à jour portant uniquement sur `requestPacing` — valident de la même manière les chemins du fournisseur fusionné avant l’enregistrement, et un chemin conservé invalide renvoie `400` sans modifier la configuration. Les mêmes règles de chemin s’appliquent au chargement d’un fichier de configuration. + Les fournisseurs à clé API peuvent détenir une clé littérale ou une référence à une variable d'environnement. Les fournisseurs OAuth utilisent le magasin d'identifiants alimenté par `ocx login` ; le comportement de lancement de Claude Code avec abonnement est configuré sous [`claudeCode.authMode`](/fr/reference/configuration/server/#claude-code-claudecode). diff --git a/docs-site/src/content/docs/ja/reference/configuration/providers.md b/docs-site/src/content/docs/ja/reference/configuration/providers.md index 33cbe63b1e3..a40de15f7c2 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ja/reference/configuration/providers.md @@ -79,6 +79,7 @@ account を削除しても mapping は保持され、同じ id を再追加す | `baseUrl` | `string` |アップストリーム API のベース URL。ほとんどの組み込み固定エンドポイントは不一致を無視します。衝突安全キー プリセットは、古い同じ名前のカスタム宛先を保持します。 | | `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, models? }` | 上流の使用量、請求、レート制限表示とは別の、クライアント側の送信開始間隔調整です。プロバイダー制限は全モデルに適用され、`models` は上流の正確なモデル ID に一致し、遅延を増やす場合のみ有効です。キュー待機は応答ヘッダーのタイムアウトを消費しません。HTTP、Responses WebSocket、明示的なアダプターの `fetchResponse`/`runTurn` 送信を対象にします。 | | `responsesPath?` | `string` |キー認証 `openai-responses` リクエストの相対リソース パス。 `/` で始まり、スキーム、クエリ、またはフラグメントが含まれていない必要があります。 | +| `chatCompletionsPath?` | `string` | `openai-chat` リクエストの相対リソース パス。 `responsesPath` の対となる設定で、同じ形式ルールが適用されます。1つのアップストリームが Chat Completions と Responses を異なるプレフィックスで提供する場合に必要です。モデルごとの wire override はアダプターのみを変更し `baseUrl` は変更しないため、この設定がないと有効化された Chat リクエストが Responses ベースへ送信されます。同梱例は Z.AI です。 | | `upstreamWebsocket?` | `boolean` | `openai-responses` リクエストで使用するアップストリーム Responses WebSocket トランスポート(既定値は無効)。アップストリームがこのプロトコルに対応している場合、ストリーミング POST は設定済みの Responses パス(既定値 `/v1/responses`)へ HTTPS の WSS で接続し、通常の処理向けに SSE へ再エンコードされます。forward プロバイダーは `{baseUrl}/responses`、キー認証プロバイダーは `responsesPath`(未設定時は従来の `/v1/responses`)を使用します。HTTP のベース URL は SSE のままとなり、Responses 以外のパスと `openai-chat` リクエストは HTTP を使用します。 | | `supportsServiceTier?` | `boolean` | `service_tier` ケイパビリティの 3 状態です。`true`: fast モードが注入でき、呼び出し元の値も保持されます。`false`: フィールドは削除され、注入もされません (非対応と文書化されたアップストリームには送りません)。未設定: 未分類 — 呼び出し元の値はそのまま保持され、fast モードは注入しません。レジストリは正規 OpenAI (`true`)、DeepSeek、Volcengine Ark (`false`) を分類します。実際にティアをサポートするカスタム ゲートウェイにのみ明示的に設定してください。 | | `preserveResponsesReasoningContent?` | `boolean` | リプレイされる Responses reasoning アイテムの平文 reasoning コンテンツを消去せずに保持します (消去は ChatGPT バックエンドのルールです)。DeepSeek のように reasoning リプレイを受け入れるアップストリームで有効にしてください。プロキシ生成の `ocxr1` エンベロープは常に削除されます。 | @@ -146,6 +147,8 @@ account を削除しても mapping は保持され、同じ id を再追加す | `unsafeAllowNativeLocalExec?` | `boolean` |カーソルのレガシー ブール値。新しいフィールドが設定されていない場合のみ、`nativeLocalExec: "on"` と同等です。 | | `nativeLocalExec?` | `"off" \| "codex-sandbox" \| "on"` |カーソルのローカル実行ポリシー。 `off` がデフォルトです。 `codex-sandbox` は現在、`off` と同様にフェールクローズされます。 | +プロバイダーの登録・置換(`POST /api/providers`)では、メモリやファイルの設定を変更する前に `responsesPath` と `chatCompletionsPath` を検証します。`PATCH /api/providers?name=` はリクエスト本文を保存済みのプロバイダーにマージします。`disabled` 以外のフィールドを変更する更新(`requestPacing` のみの更新を除く)では、保存前にマージ後のプロバイダーのパスを同じ方法で検証し、保持されているパスが無効な場合は `400` を返して設定を変更しません。設定ファイルの読み込みにも同じ経路の規則が適用されます。 + API キープロバイダーは、リテラルキーまたは環境参照を保持する場合があります。 OAuth プロバイダーは、`ocx login` によって設定された資格情報ストアを使用します。サブスクリプションに基づくクロード コードの起動動作は、[`claudeCode.authMode`](/reference/configuration/server/#claude-code) で構成されます。 ## プロバイダーによるアウトバウンドの安全性診断 diff --git a/docs-site/src/content/docs/ko/reference/configuration/providers.md b/docs-site/src/content/docs/ko/reference/configuration/providers.md index d8e38e9411d..a616c771fbb 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ko/reference/configuration/providers.md @@ -79,6 +79,7 @@ managed map을 활성화하면 privacy-safe selector를 만들고, 이후 계정 | `baseUrl` | `string` | 상위 API 기본 URL입니다. 대부분의 내장 고정 엔드포인트는 불일치를 무시합니다. 충돌 안전 키 프리셋은 같은 이름의 이전 사용자 지정 목적지를 보존합니다. | | `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, models? }` | 업스트림 사용량, 과금, rate-limit 지표와 별개인 선택적 클라이언트 측 아웃바운드 요청 시작 속도 조절입니다. Provider 제한은 모든 모델에 적용되고 `models` 항목은 정확한 업스트림 모델 ID와 일치하며 지연을 더 늘릴 때만 적용됩니다. 큐 대기는 응답 헤더 타임아웃을 소모하지 않습니다. HTTP, Responses WebSocket, 명시적 어댑터 `fetchResponse`/`runTurn` 전송을 포함합니다. | | `responsesPath?` | `string` | 키 인증 `openai-responses` 요청의 상대 리소스 경로입니다. 반드시 `/`로 시작해야 하며 스킴, query, fragment를 포함하면 안 됩니다. | +| `chatCompletionsPath?` | `string` | `openai-chat` 요청의 상대 리소스 경로로, `responsesPath`와 동일한 형식 규칙이 적용되는 대응 항목입니다. 하나의 업스트림이 Chat Completions와 Responses를 서로 다른 접두사로 제공할 때 필요합니다. 모델별 wire override는 어댑터만 바꾸고 `baseUrl`은 그대로 두므로, 이 설정이 없으면 옵트인된 Chat 요청이 Responses base로 전송됩니다. Z.AI가 제공되는 예시입니다. | | `upstreamWebsocket?` | `boolean` | `openai-responses` 요청에 대한 업스트림 Responses WebSocket 전송을 선택적으로 활성화합니다(기본값 `false`). 업스트림이 이 프로토콜을 지원하면 스트리밍 POST가 설정된 Responses 경로(기본값 `/v1/responses`)로 HTTPS 기반 WSS를 사용하고, 일반 파이프라인을 위해 SSE로 다시 인코딩됩니다. forward 공급자는 `{baseUrl}/responses`를 사용하고, key-auth 공급자는 `responsesPath`를 사용하며 미설정 시 기존 `/v1/responses`로 대체됩니다. HTTP 기본 URL은 SSE를 유지하고, Responses가 아닌 경로와 `openai-chat` 요청은 HTTP를 사용합니다. | | `supportsServiceTier?` | `boolean` | `service_tier` 케이퍼빌리티 3상태입니다. `true`: fast 모드가 주입할 수 있고 호출자 값도 보존합니다. `false`: 필드를 제거하고 절대 주입하지 않습니다(미지원으로 문서화된 업스트림에는 볼 수 없습니다). 미설정: 미분류 — 호출자가 준 값은 그대로 보존하고 fast 모드는 주입하지 않습니다. 레지스트리는 정식 OpenAI(`true`), DeepSeek, Volcengine Ark(`false`)를 분류하며, 실제로 티어를 지원하는 커스텀 게이트웨이에만 명시적으로 설정하세요. | | `preserveResponsesReasoningContent?` | `boolean` | 리플레이되는 Responses reasoning 항목의 평문 reasoning 내용을 지우지 않고 유지합니다(지우는 것은 ChatGPT 백엔드 규칙입니다). DeepSeek처럼 reasoning 리플레이를 허용하는 업스트림에 켜세요. 프록시가 만든 `ocxr1` 봉투는 항상 제거됩니다. | @@ -146,6 +147,8 @@ managed map을 활성화하면 privacy-safe selector를 만들고, 이후 계정 | `unsafeAllowNativeLocalExec?` | `boolean` | Cursor 레거시 불리언입니다. 더 새로운 필드가 설정되지 않았을 때만 `nativeLocalExec: "on"`과 같습니다. | | `nativeLocalExec?` | `"off" \| "codex-sandbox" \| "on"` | Cursor 로컬 실행 정책입니다. 기본값은 `off`입니다. `codex-sandbox`는 현재 `off`처럼 실패를 닫습니다. | +공급자 등록·교체(`POST /api/providers`)는 `responsesPath`와 `chatCompletionsPath`를 검증한 뒤 메모리와 파일의 설정을 변경합니다. `PATCH /api/providers?name=`는 요청 본문을 저장된 공급자에 병합합니다. `disabled` 외의 필드를 변경하는 업데이트(`requestPacing`만 변경하는 업데이트 제외)는 저장 전에 병합된 공급자의 경로를 같은 방식으로 검증하며, 유지된 경로가 유효하지 않으면 `400`을 반환하고 설정을 변경하지 않습니다. 설정 파일을 읽을 때도 같은 경로 규칙을 적용합니다. + API 키 공급자는 리터럴 키나 환경 참조를 둘 수 있습니다. OAuth 공급자는 `ocx login`으로 채워지는 자격 증명 저장소를 사용합니다. 구독 기반 Claude Code 실행 동작은 [`claudeCode.authMode`](/reference/configuration/server/#claude-code)에서 설정합니다. ## 공급자 진단용 외부 요청 안전성 diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 36aa57eef10..d7db89945ae 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -225,6 +225,8 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity | `unsafeAllowNativeLocalExec?` | `boolean` | Cursor legacy boolean, equivalent to `nativeLocalExec: "on"` only when the newer field is unset. | | `nativeLocalExec?` | `"off" \| "codex-sandbox" \| "on"` | Cursor local-exec policy. `off` is default; `codex-sandbox` currently fails closed like `off`. | +Provider registration and replacement (`POST /api/providers`) validate `responsesPath` and `chatCompletionsPath` before changing live configuration or disk state. `PATCH /api/providers?name=` merges the request body with the stored provider; updates touching fields beyond `disabled` — except `requestPacing`-only updates — validate the merged provider's paths the same way before saving, and an invalid retained path returns `400` with the configuration unchanged. The same path rules apply when loading a configuration file. + With `webSearchBridge` enabled, a search continuation stays bound to the API-key selection that served the first request. Changing the selected key, its reference or resolved value, authentication mode, or base URL during search or provider pacing ends the turn with a bridge error before another diff --git a/docs-site/src/content/docs/ru/reference/configuration/providers.md b/docs-site/src/content/docs/ru/reference/configuration/providers.md index e8e5ea01098..dbcd2024ba0 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ru/reference/configuration/providers.md @@ -92,6 +92,7 @@ cross-route credential fallback не существует. Строки API GPT- | `baseUrl` | `string` | Базовый URL API upstream'а. Большинство built-in fixed-endpoint'ов игнорируют несовпадение; collision-safe key-preset'ы сохраняют старый custom destination с тем же именем. | | `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, models? }` | Опциональное клиентское выравнивание начала исходящих запросов, отдельное от учёта использования, биллинга и индикаторов rate limit апстрима. Лимит провайдера действует на все модели, а `models` сопоставляется с точными ID моделей апстрима и может только увеличить задержку. Ожидание очереди не расходует таймаут заголовков ответа. Поддерживаются HTTP, Responses WebSocket и явные вызовы адаптеров `fetchResponse`/`runTurn`. | | `responsesPath?` | `string` | Relative resource path для key-auth запросов `openai-responses`. Должен начинаться с `/` и не может содержать scheme, query или fragment. | +| `chatCompletionsPath?` | `string` | Relative resource path для запросов `openai-chat`, зеркало `responsesPath` с теми же правилами формы. Нужен, когда один upstream обслуживает Chat Completions и Responses под разными префиксами: per-model wire override меняет адаптер и не трогает `baseUrl`, поэтому без него включённый Chat-запрос ушёл бы в Responses base. Поставляемый пример — Z.AI. | | `upstreamWebsocket?` | `boolean` | Необязательный upstream Responses WebSocket для запросов `openai-responses` (по умолчанию `false`). Если upstream поддерживает этот протокол, потоковые POST-запросы используют настроенный путь Responses (по умолчанию `/v1/responses`), подключаются по WSS через HTTPS и перекодируются обратно в SSE для обычного конвейера. Провайдеры в режиме forward используют `{baseUrl}/responses`; провайдеры с ключом используют `responsesPath` или исторический fallback `/v1/responses`. Для HTTP остаётся SSE; пути, не относящиеся к Responses, и запросы `openai-chat` остаются на HTTP. | | `supportsServiceTier?` | `boolean` | Три состояния поддержки `service_tier`. `true`: fast mode может подставлять поле, значения вызывающего сохраняются. `false`: поле удаляется и никогда не подставляется (апстрим, для которого задокументировано отсутствие поддержки, не должен его получать). Не задано: провайдер не классифицирован — значения вызывающего сохраняются без изменений, fast mode не подставляет. Registry классифицирует canonical OpenAI (`true`), DeepSeek и Volcengine Ark (`false`); задавайте явно только для custom gateway'ев, реально поддерживающих tier'ы. | | `preserveResponsesReasoningContent?` | `boolean` | Сохранять plaintext reasoning content в replay'нутых Responses reasoning item'ах вместо очистки (очистка — правило ChatGPT backend'а). Включайте для upstream'ов, чей контракт принимает reasoning replay, например DeepSeek. Proxy-minted `ocxr1` envelope'ы удаляются всегда. | @@ -159,6 +160,8 @@ cross-route credential fallback не существует. Строки API GPT- | `unsafeAllowNativeLocalExec?` | `boolean` | Legacy boolean Cursor, эквивалентен `nativeLocalExec: "on"` только если новое поле не задано. | | `nativeLocalExec?` | `"off" \| "codex-sandbox" \| "on"` | Политика local-exec для Cursor. `off` — дефолт; `codex-sandbox` сейчас ведёт себя fail-closed как `off`. | +Регистрация и замена провайдера (`POST /api/providers`) проверяют `responsesPath` и `chatCompletionsPath` до изменения конфигурации в памяти или на диске. `PATCH /api/providers?name=` объединяет тело запроса с сохранённым провайдером; обновления, затрагивающие поля помимо `disabled`, — кроме обновлений только `requestPacing` — так же проверяют пути объединённого провайдера перед сохранением, а недопустимый сохранённый путь возвращает `400` без изменения конфигурации. Те же правила путей применяются при загрузке файла конфигурации. + Провайдеры с API-key могут хранить literal key или environment-reference. OAuth-провайдеры используют credential store, заполняемый через `ocx login`; поведение subscription-backed launcher'а Claude Code настраивается через diff --git a/docs-site/src/content/docs/tr/reference/configuration/providers.md b/docs-site/src/content/docs/tr/reference/configuration/providers.md index a119c8e88e8..ee6414e6d34 100644 --- a/docs-site/src/content/docs/tr/reference/configuration/providers.md +++ b/docs-site/src/content/docs/tr/reference/configuration/providers.md @@ -94,6 +94,7 @@ alanlı seçilmiş kimlikleri yalın kimliklere yeniden yazar. | `adapter` | `string` | `openai-chat`, `openai-responses`, `anthropic`, `google`, `kiro`, `cursor`, `ollama-native`, `azure-openai` (veya takma ad `azure`) seçeneklerinden biri. | | `baseUrl` | `string` | Yukarı akış API temel URL'si. Çoğu yerleşik sabit uç nokta uyumsuzluğu yok sayar; çakışma güvenli anahtar önayarları aynı adlı daha eski özel bir hedefi korur. | | `responsesPath?` | `string` | Anahtar kimlik doğrulamalı `openai-responses` istekleri için göreli kaynak yolu. `/` ile başlamalı ve şema, sorgu veya parça içermemelidir. | +| `chatCompletionsPath?` | `string` | `openai-chat` istekleri için göreli kaynak yolu; `responsesPath`'in aynasıdır ve aynı şekil kurallarına tabidir. Bir upstream Chat Completions ve Responses'u farklı öneklerde sunduğunda gereklidir: model başına wire override adaptörü değiştirir ve `baseUrl`'e dokunmaz, bu yüzden bu ayar olmadan etkin bir Chat isteği Responses base'e gönderilir. Gönderilen örnek Z.AI'dir. | | `upstreamWebsocket?` | `boolean` | `openai-responses` istekleri için isteğe bağlı upstream Responses WebSocket aktarımıdır (varsayılan `false`). Upstream bu protokolü desteklediğinde, akışlı POST istekleri yapılandırılmış Responses yolunu (varsayılan `/v1/responses`) HTTPS tabanında WSS ile kullanır ve normal işlem hattı için SSE'ye yeniden kodlanır. Forward sağlayıcılar `{baseUrl}/responses`, anahtar kimlik doğrulamalı sağlayıcılar `responsesPath` veya eski `/v1/responses` geri dönüşünü kullanır. Düz HTTP SSE olarak kalır; Responses dışı yollar ve `openai-chat` istekleri HTTP'de kalır. | | `supportsServiceTier?` | `boolean` | Üç durumlu `service_tier` yeteneği. `true`: hızlı mod enjekte edebilir ve arayan değerleri korunur. `false`: alan kaldırılır ve asla enjekte edilmez (desteklemediği belgelenen yukarı akış bunu almamalıdır). Yok: sağlayıcı sınıflandırılmamıştır — arayan tarafından sağlanan değerler dokunulmadan korunur ve hızlı mod asla enjekte etmez. Kayıt defteri kurallı OpenAI'yi (`true`), DeepSeek'i ve Volcengine Ark'ı (`false`) sınıflandırır; bunu yalnızca katmanları gerçekten destekleyen özel ağ geçitleri için açıkça ayarlayın. | | `preserveResponsesReasoningContent?` | `boolean` | Düz metin akıl yürütme içeriğini boşaltmak yerine (boşaltma ChatGPT arka ucunun kuralıdır) tekrarlanan Responses akıl yürütme öğelerinde tutun. DeepSeek gibi sözleşmesi akıl yürütme tekrarını kabul eden yukarı akışlar için etkinleştirin. Proxy tarafından basılan `ocxr1` zarfları her zaman kaldırılır. | @@ -160,6 +161,8 @@ alanlı seçilmiş kimlikleri yalın kimliklere yeniden yazar. | `unsafeAllowNativeLocalExec?` | `boolean` | Cursor eski boolean değeri, yalnızca daha yeni alan ayarlanmadığında `nativeLocalExec: "on"` değerine eşdeğerdir. | | `nativeLocalExec?` | `"off" \| "codex-sandbox" \| "on"` | Cursor yerel yürütme politikası. `off` varsayılandır; `codex-sandbox` şu anda `off` gibi kapalı olarak başarısız olur. | +Sağlayıcı kaydı ve değiştirme (`POST /api/providers`), bellekteki veya diskteki yapılandırmayı değiştirmeden önce `responsesPath` ve `chatCompletionsPath` değerlerini doğrular. `PATCH /api/providers?name=`, istek gövdesini kayıtlı sağlayıcıyla birleştirir; `disabled` dışındaki alanlara dokunan güncellemeler (yalnızca `requestPacing` içeren güncellemeler hariç), kaydetmeden önce birleştirilmiş sağlayıcının yollarını aynı şekilde doğrular ve geçersiz bir korunmuş yol `400` döndürüp yapılandırmayı değiştirmez. Aynı yol kuralları yapılandırma dosyası yüklenirken de uygulanır. + API anahtarı sağlayıcıları değişmez bir anahtar veya bir ortam referansı tutabilir. OAuth sağlayıcıları `ocx login` tarafından doldurulan kimlik bilgisi deposunu kullanır; abonelik destekli Claude Code başlatma davranışı diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md index 11d34b2f722..70afc16f0f6 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md @@ -79,6 +79,7 @@ selector,而不是分配一个新名称。 | `baseUrl` | `string` | 上游 API 基础 URL。大多数内置固定端点会忽略不匹配的值;具备冲突安全键的预设会保留一个更早、同名的自定义目标。 | | `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, models? }` | 可选的客户端出站请求启动节流,与上游用量、计费和限流指标相互独立。提供商限制适用于所有模型,`models` 按上游模型精确 ID 匹配且只能增加延迟。排队等待不计入响应头超时。覆盖 HTTP、Responses WebSocket 以及显式适配器 `fetchResponse`/`runTurn` 调用。 | | `responsesPath?` | `string` | 用于 key-auth `openai-responses` 请求的相对资源路径。必须以 `/` 开头,且不能包含 scheme、query 或 fragment。 | +| `chatCompletionsPath?` | `string` | 用于 `openai-chat` 请求的相对资源路径,是 `responsesPath` 的对应项,适用相同的路径规则。当同一上游以不同前缀提供 Chat Completions 和 Responses 时需要此配置:按模型的 wire override 只更换适配器而不改动 `baseUrl`,否则已启用的 Chat 请求会被发送到 Responses base。随附示例为 Z.AI。 | | `upstreamWebsocket?` | `boolean` | 为 `openai-responses` 请求选择性启用上游 Responses WebSocket 传输(默认 `false`)。当上游支持该协议时,流式 POST 请求会使用配置的 Responses 路径(默认 `/v1/responses`),通过 HTTPS 基础 URL 以 WSS 连接,并重新编码为常规流程使用的 SSE。forward 提供者使用 `{baseUrl}/responses`;key-auth 提供者使用 `responsesPath`,未设置时回退到传统的 `/v1/responses`。普通 HTTP 仍使用 SSE;非 Responses 路径和 `openai-chat` 请求仍使用 HTTP。 | | `supportsServiceTier?` | `boolean` | `service_tier` 能力的三态。`true`:fast 模式可以注入,调用方提供的值也会被保留。`false`:剥离该字段且绝不注入(已明确不支持的上游不会收到它)。未设置:未分类——调用方提供的值原样保留,fast 模式绝不注入。注册表已对官方 OpenAI(`true`)、DeepSeek 和 Volcengine Ark(`false`)分类;仅对真正支持分层的自定义网关显式设置。 | | `preserveResponsesReasoningContent?` | `boolean` | 在重放的 Responses reasoning 项中保留明文 reasoning 内容,而不是清空(清空是 ChatGPT 后端的规则)。对接受 reasoning 重放的上游(如 DeepSeek)启用。代理生成的 `ocxr1` 信封始终会被剥离。 | @@ -146,6 +147,8 @@ selector,而不是分配一个新名称。 | `unsafeAllowNativeLocalExec?` | `boolean` | Cursor 旧布尔值;仅当更新字段未设置时,等同于 `nativeLocalExec: "on"`。 | | `nativeLocalExec?` | `"off" \| "codex-sandbox" \| "on"` | Cursor 本地执行策略。`off` 是默认值;`codex-sandbox` 目前会像 `off` 一样失败关闭。 | +注册或替换提供商(`POST /api/providers`)时,会先验证 `responsesPath` 和 `chatCompletionsPath`,再修改内存或磁盘中的配置。`PATCH /api/providers?name=` 会将请求体与已保存的提供商合并;除仅更新 `requestPacing` 的请求外,凡是修改 `disabled` 以外字段的更新,都会在保存前以同样方式验证合并后提供商的路径,若保留的既有路径无效则返回 `400`,且不更改配置。加载配置文件时也适用同样的路径规则。 + API key 提供者可以持有字面量 key,或环境引用。OAuth 提供者使用由 `ocx login` 填充的凭据存储;基于订阅的 Claude Code 启动行为在 [`claudeCode.authMode`](/reference/configuration/server/#claude-code) 下配置。 ## 提供者诊断出站安全性 diff --git a/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md b/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md index 4b075de3cf8..87e01868821 100644 --- a/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md +++ b/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md @@ -59,6 +59,7 @@ ocx models provider openrouter on | `baseUrl` | `string` | 上游 API base URL。多數內建固定端點忽略不符;碰撞安全的金鑰預設保留較舊的同名自訂目的地。 | | `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, models? }` | 選用的用戶端出站請求啟動節流,與上游用量、計費及限流指標彼此獨立。供應商限制適用於所有模型,`models` 依上游模型精確 ID 比對且只能增加延遲。排隊等待不計入回應標頭逾時。涵蓋 HTTP、Responses WebSocket 及明確的適配器 `fetchResponse`/`runTurn` 呼叫。 | | `responsesPath?` | `string` | Key-auth `openai-responses` 請求的相對資源路徑。必須以 `/` 開頭且不含 scheme、query 或 fragment。 | +| `chatCompletionsPath?` | `string` | `openai-chat` 請求的相對資源路徑,為 `responsesPath` 的對應項,適用相同的路徑規則。當同一上游以不同前綴提供 Chat Completions 與 Responses 時需要此設定:按模型的 wire override 只更換適配器而不改動 `baseUrl`,否則已啟用的 Chat 請求會送往 Responses base。隨附範例為 Z.AI。 | | `upstreamWebsocket?` | `boolean` | 為 `openai-responses` 請求選用上游 Responses WebSocket 傳輸(預設 `false`)。當上游支援此協定時,串流 POST 請求會使用設定的 Responses 路徑(預設 `/v1/responses`),透過 HTTPS 基礎 URL 以 WSS 連線,再重新編碼為一般流程使用的 SSE。forward 供應商使用 `{baseUrl}/responses`;key-auth 供應商使用 `responsesPath`,未設定時回退到傳統的 `/v1/responses`。一般 HTTP 仍使用 SSE;非 Responses 路徑與 `openai-chat` 請求仍使用 HTTP。 | | `disabled?` | `boolean` | 將供應商保留在磁碟上但排除於路由與模型/目錄清單。 | | `apiKey?` | `string` | API 金鑰,或在請求時解析的 `${ENV_VAR}` / `$ENV_VAR` 參考。 | @@ -116,6 +117,8 @@ ocx models provider openrouter on | `unsafeAllowNativeLocalExec?` | `boolean` | Cursor 舊版布林值,僅在較新欄位未設定時等同於 `nativeLocalExec: "on"`。 | | `nativeLocalExec?` | `"off" \| "codex-sandbox" \| "on"` | Cursor 本機執行政策。`off` 為預設;`codex-sandbox` 目前像 `off` 般 fail closed。 | +註冊或替換供應商(`POST /api/providers`)時,會先驗證 `responsesPath` 和 `chatCompletionsPath`,再修改記憶體或磁碟中的設定。`PATCH /api/providers?name=` 會將請求內容與已儲存的供應商合併;除僅更新 `requestPacing` 的請求外,凡是修改 `disabled` 以外欄位的更新,都會在儲存前以同樣方式驗證合併後供應商的路徑,若保留的既有路徑無效則回傳 `400`,且不變更設定。載入設定檔時也適用相同的路徑規則。 + API-key 供應商可持有字面值金鑰或環境參考。OAuth 供應商使用由 `ocx login` 填入的憑證存放;訂閱支援的 Claude Code 啟動行為在 [`claudeCode.authMode`](/zh-tw/reference/configuration/server/#claude-code) 下設定。 ## 供應商診斷對外安全 diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index 2d46186e933..a18e6e6950a 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -168,6 +168,7 @@ }, "explicit": { "key-attribution.test.ts": "usage", + "provider-send-path-import.test.ts": "server", "responses-core-modules.test.ts": "responses", "responses-spend-ledger-wiring.test.ts": "responses", "responses-send-budget-errors.test.ts": "responses", diff --git a/src/config/provider-relative-send-path.ts b/src/config/provider-relative-send-path.ts new file mode 100644 index 00000000000..e167dd8d309 --- /dev/null +++ b/src/config/provider-relative-send-path.ts @@ -0,0 +1,16 @@ +/** Shared, initialization-independent shape check for provider send-path overrides. */ +export function providerRelativeSendPathConfigError( + field: "responsesPath" | "chatCompletionsPath", + value: unknown, +): string | null { + if (value === undefined) return null; + if (typeof value !== "string") return `${field} must be a string`; + if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value) || value.includes("://")) { + return `${field} must be a relative path without a URL scheme`; + } + if (!value.startsWith("/")) return `${field} must start with /`; + if (value.includes("?") || value.includes("#")) { + return `${field} must not include query strings or fragments`; + } + return null; +} diff --git a/src/config/schema/leaf-validators.ts b/src/config/schema/leaf-validators.ts index b0ace501c54..5b4eb3b3c9b 100644 --- a/src/config/schema/leaf-validators.ts +++ b/src/config/schema/leaf-validators.ts @@ -300,21 +300,7 @@ export const providerConfigSchema = z.object({ }).passthrough(); -/** - * Shared shape check for the two relative send-path overrides. `field` names the - * offending key so the message stays specific to what the user actually wrote. - */ -export function providerRelativeSendPathConfigError(field: string, value: string | undefined): string | null { - if (value === undefined) return null; - if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value) || value.includes("://")) { - return `${field} must be a relative path without a URL scheme`; - } - if (!value.startsWith("/")) return `${field} must start with /`; - if (value.includes("?") || value.includes("#")) { - return `${field} must not include query strings or fragments`; - } - return null; -} +export { providerRelativeSendPathConfigError } from "../provider-relative-send-path"; /** * Validate `providers..modelCosts`: a plain object keyed by exact model diff --git a/src/server/auth-cors.ts b/src/server/auth-cors.ts index 32fc9fe8ce1..ddc87f948e4 100644 --- a/src/server/auth-cors.ts +++ b/src/server/auth-cors.ts @@ -1,3 +1,4 @@ +import { providerRelativeSendPathConfigError } from "../config/provider-relative-send-path"; import { modelCapabilitiesConfigError } from "../config/provider-validation"; import { createHmac, randomBytes, timingSafeEqual } from "node:crypto"; import { initialModelSelection } from "../providers/initial-model-selection"; @@ -754,6 +755,10 @@ export function providerManagementConfigError( } const destinationError = providerDestinationConfigError(name, typed); if (destinationError) return `provider ${name} ${destinationError}`; + for (const field of ["responsesPath", "chatCompletionsPath"] as const) { + const sendPathError = providerRelativeSendPathConfigError(field, raw[field]); + if (sendPathError) return `provider ${JSON.stringify(redactSecretString(name))} ${sendPathError}`; + } const headersError = providerHeadersConfigError(typed.headers); if (headersError) return `provider ${name} ${headersError}`; const retryOn429Error = retryOn429PolicyConfigError(raw.retryOn429); diff --git a/structure/config.md b/structure/config.md index 7103bfe73bf..85b5a779169 100644 --- a/structure/config.md +++ b/structure/config.md @@ -299,6 +299,22 @@ hand-edited `config.json` must accept and reject the same provider shapes. > Decision record: [ADR-0020](decisions/ADR-0020-provider-validation-ownership.md) +## Provider relative send paths + +`src/config/provider-relative-send-path.ts` owns the initialization-independent +`providerRelativeSendPathConfigError` check. The schema leaf re-exports it for compatibility; +`src/server/auth-cors.ts` imports the pure module directly, so this validator adds no +runtime dependency on config-schema initialization. +Both `responsesPath` and `chatCompletionsPath` +must be strings beginning with `/`, without a scheme, query or fragment; omission is allowed. +Provider registration/replacement rejects invalid values before DNS, persistence or catalog +refresh. Editor PATCH checks also validate retained paths when they revalidate a merged provider; +pacing-only and other existing validation bypasses are unchanged. No send-path PATCH setter is added. +`tests/server/management-provider-validation.test.ts` covers rejection without live/disk mutation +and valid-path persistence/reload through the actual management handler. +`tests/server/provider-send-path-import.test.ts` loads the management boundary before the +config facade in a fresh process, so an earlier schema import cannot mask an initialization cycle. + ## Restore `ocx stop`, `ocx restore` / `ocx eject`, `ocx service stop`, and `ocx service uninstall` must strip diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index 8d7f5edf98d..921c6cb49dd 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -8,7 +8,7 @@ The shared server request path follows the Responses [core module ownership](transports/responses.md#core-module-ownership). This surface retains its existing behavior. The configuration-only [plaintext V2 contract](subagents.md#plaintext-v2-agent-messages) -is scoped to canonical ChatGPT Responses forwarding; other source-area behavior described here is unchanged. Response-attached WebSocket telemetry follows the [stage record identity contract](transports/responses.md#passthrough-sse-stream-shapes-314). Catalog HTTP acquisition follows the [proxy-routing contract](catalog.md#remote-catalog-http-proxy-routing). CLI installation inspection reason codes, including Windows deferral, follow the [runtime inspection contract](runtime.md#lifecycle). +is scoped to canonical ChatGPT Responses forwarding; other source-area behavior described here is unchanged. Response-attached WebSocket telemetry follows the [stage record identity contract](transports/responses.md#passthrough-sse-stream-shapes-314). Management provider-validation calls use the [initialization-independent relative send-path validation](config.md#provider-relative-send-paths) before persistence. Catalog HTTP acquisition follows the [proxy-routing contract](catalog.md#remote-catalog-http-proxy-routing). CLI installation inspection reason codes, including Windows deferral, follow the [runtime inspection contract](runtime.md#lifecycle). ## Dashboard serving diff --git a/structure/overview.md b/structure/overview.md index 695869ab630..6eef3a999e9 100644 --- a/structure/overview.md +++ b/structure/overview.md @@ -1,5 +1,6 @@ # Overview +Management provider-validation calls use the [shared relative send-path validation](config.md#provider-relative-send-paths) before persistence. Native steering follows [the shared WebSocket contract](transports/streaming-health.md#experimental-native-mid-turn-steering); this surface's defaults remain unchanged. The configuration-only [plaintext V2 contract](subagents.md#plaintext-v2-agent-messages) diff --git a/structure/providers/openai-tiers.md b/structure/providers/openai-tiers.md index f10d28fe1ed..52d2bde3a9c 100644 --- a/structure/providers/openai-tiers.md +++ b/structure/providers/openai-tiers.md @@ -1,6 +1,6 @@ # OpenAI Provider Account Modes -Catalog HTTP acquisition follows the [proxy-routing contract](../catalog.md#remote-catalog-http-proxy-routing). +Management provider-validation calls use the [shared relative send-path validation](../config.md#provider-relative-send-paths) before persistence. Catalog HTTP acquisition follows the [proxy-routing contract](../catalog.md#remote-catalog-http-proxy-routing). The configuration-only [plaintext V2 contract](../subagents.md#plaintext-v2-agent-messages) is scoped to canonical ChatGPT Responses forwarding; other source-area behavior described here is unchanged. CLI installation inspection reason codes, including Windows deferral, follow the [runtime inspection contract](../runtime.md#lifecycle). diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 9963bc058e2..3ab8d79cc57 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -5,7 +5,7 @@ Native result continuations and function-result injection follow [the mode-speci Native steering follows [the shared WebSocket contract](streaming-health.md#experimental-native-mid-turn-steering); this surface's defaults remain unchanged. The configuration-only [plaintext V2 contract](../subagents.md#plaintext-v2-agent-messages) -is scoped to canonical ChatGPT Responses forwarding; other source-area behavior described here is unchanged. Cursor's localized native-shell names follow the [routing-commentary guard contract](../providers/cursor.md#cursor-native-exec). +is scoped to canonical ChatGPT Responses forwarding; other source-area behavior described here is unchanged. Management provider-validation calls use the [initialization-independent relative send-path validation](../config.md#provider-relative-send-paths) before persistence. Cursor's localized native-shell names follow the [routing-commentary guard contract](../providers/cursor.md#cursor-native-exec). Plaintext collaboration restoration treats a null namespace as absent, rejects non-string namespace types, and restores the native namespace/name pair before HTTP/WS delivery and continuation publication. diff --git a/structure/transports/streaming-health.md b/structure/transports/streaming-health.md index c4e6e1f7a7d..1cc15ebb9c4 100644 --- a/structure/transports/streaming-health.md +++ b/structure/transports/streaming-health.md @@ -1,5 +1,7 @@ # Streaming Health And WebSocket +Management provider-validation calls use the [initialization-independent relative send-path validation](../config.md#provider-relative-send-paths) before persistence. + Native and translated delivery now have separate owners in the [core module ownership](responses.md#core-module-ownership). This surface retains its existing behavior. diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index d9950391b49..9e5d009196a 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -1,5 +1,6 @@ { "key-attribution.test.ts": "usage", + "provider-send-path-import.test.ts": "server", "responses-core-modules.test.ts": "responses", "responses-spend-ledger-wiring.test.ts": "responses", "responses-send-budget-errors.test.ts": "responses", diff --git a/tests/helpers/management-relative-send-paths.ts b/tests/helpers/management-relative-send-paths.ts new file mode 100644 index 00000000000..e59e0628fec --- /dev/null +++ b/tests/helpers/management-relative-send-paths.ts @@ -0,0 +1,118 @@ +import { describe, expect, spyOn, test } from "bun:test"; +import { ManagementRequest as Request } from "./management-auth"; +import { mkdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { loadConfig, saveConfig } from "../../src/config"; +import { handleManagementAPI } from "../../src/server/management-api"; +import { providerManagementConfigError } from "../../src/server/auth-cors"; +import type { OcxConfig } from "../../src/types"; +import * as destinationPolicy from "../../src/lib/destination-policy"; +import { catalogConvergenceFactory } from "./catalog-convergence"; + +export function config(hostname?: string): OcxConfig { + return { + port: 10100, + hostname, + defaultProvider: "openai", + providers: { + openai: { + adapter: "openai-chat", + baseUrl: "https://api.example.test/v1", + apiKey: "sk-secret-value", + headers: { "X-Custom": "provider-secret" }, + defaultModel: "gpt-test", + }, + }, + }; +} + +export function registerRelativeSendPathTests(TEST_DIR: string): void { +describe("relative send paths at the management write boundary", () => { + const provider = { adapter: "openai-responses" as const, baseUrl: "https://relay.example.test/v1" }; + const fields = ["responsesPath", "chatCompletionsPath"] as const; + + test.each(fields)("rejects invalid %s values through the shared management validator", field => { + expect(providerManagementConfigError("relay", provider)).toBeNull(); + expect(providerManagementConfigError("relay", { ...provider, [field]: "/custom/send" })).toBeNull(); + for (const value of ["@other.example.test/send", "https://other.example.test/send", "/send?query=1", "/send#fragment", "", 42, null, {}]) { + expect(providerManagementConfigError("relay", { ...provider, [field]: value })).toContain(field); + } + }); + + for (const field of fields) { + for (const mode of ["create", "replace"]) { + test(`${mode} rejects invalid ${field} before changing memory or disk`, async () => { + mkdirSync(TEST_DIR, { recursive: true }); + process.env.OPENCODEX_HOME = TEST_DIR; + const cfg: OcxConfig = { port: 10100, hostname: "127.0.0.1", defaultProvider: "stable", + providers: { stable: { ...provider, responsesPath: "/existing", chatCompletionsPath: "/existing-chat" } } }; + saveConfig(cfg); + const beforeMemory = structuredClone(cfg); + const beforeDisk = readFileSync(join(TEST_DIR, "config.json")); + const resolved = spyOn(destinationPolicy, "providerDestinationResolvedError").mockResolvedValue(null); + let refreshes = 0; + try { + for (const value of ["@other.example.test/send", "https://other.example.test/send", "/send?query=1", "/send#fragment", "", 42]) { + const name = mode === "create" ? "new-provider" : "stable"; + const url = new URL("http://127.0.0.1/api/providers"); + const response = await handleManagementAPI(new Request(url, { + method: "POST", headers: { "content-type": "application/json" }, + body: JSON.stringify({ name, provider: { ...provider, [field]: value } }), + }), url, cfg, { createManagementConvergeCodex: catalogConvergenceFactory(() => { refreshes++; }) }); + expect(response?.status).toBe(400); + expect(await response!.json()).toMatchObject({ error: expect.stringContaining(field) }); + expect(cfg).toEqual(beforeMemory); + expect(readFileSync(join(TEST_DIR, "config.json"))).toEqual(beforeDisk); + } + expect(resolved).not.toHaveBeenCalled(); + expect(refreshes).toBe(0); + } finally { resolved.mockRestore(); } + }); + } + } + + test.each(fields)("PATCH revalidates an existing invalid %s without changing state", async field => { + mkdirSync(TEST_DIR, { recursive: true }); + process.env.OPENCODEX_HOME = TEST_DIR; + const cfg: OcxConfig = { port: 10100, hostname: "127.0.0.1", defaultProvider: "stable", + providers: { stable: { ...provider, [field]: "@other.example.test/send" } } }; + // Model a live row accepted before the write-boundary fix. PATCH edits supported + // transport fields; it does not itself expose a send-path setter. + saveConfig(cfg); + const beforeMemory = structuredClone(cfg); + const beforeDisk = readFileSync(join(TEST_DIR, "config.json")); + const resolved = spyOn(destinationPolicy, "providerDestinationResolvedError").mockResolvedValue(null); + let refreshes = 0; + try { + const url = new URL("http://127.0.0.1/api/providers?name=stable"); + const response = await handleManagementAPI(new Request(url, { method: "PATCH", headers: { "content-type": "application/json" }, + body: JSON.stringify({ baseUrl: "https://replacement.example.test/v1" }), + }), url, cfg, { createManagementConvergeCodex: catalogConvergenceFactory(() => { refreshes++; }) }); + expect(response?.status).toBe(400); + expect(await response!.json()).toMatchObject({ error: expect.stringContaining(field) }); + expect(cfg).toEqual(beforeMemory); + expect(readFileSync(join(TEST_DIR, "config.json"))).toEqual(beforeDisk); + expect(resolved).not.toHaveBeenCalled(); + expect(refreshes).toBe(0); + } finally { resolved.mockRestore(); } + }); + + test("valid relative send paths remain persistable and reloadable", async () => { + mkdirSync(TEST_DIR, { recursive: true }); + process.env.OPENCODEX_HOME = TEST_DIR; + const cfg: OcxConfig = { port: 10100, hostname: "127.0.0.1", defaultProvider: "stable", providers: { stable: { ...provider } } }; + saveConfig(cfg); + const resolved = spyOn(destinationPolicy, "providerDestinationResolvedError").mockResolvedValue(null); + try { + const url = new URL("http://127.0.0.1/api/providers"); + const response = await handleManagementAPI(new Request(url, { method: "POST", headers: { "content-type": "application/json" }, + body: JSON.stringify({ name: "custom-paths", provider: { ...provider, responsesPath: "/custom/responses", chatCompletionsPath: "/custom/chat" } }), + }), url, cfg, { createManagementConvergeCodex: catalogConvergenceFactory() }); + expect(response?.status).toBe(200); + expect(cfg.providers["custom-paths"]?.responsesPath).toBe("/custom/responses"); + expect(loadConfig().providers["custom-paths"]?.chatCompletionsPath).toBe("/custom/chat"); + } finally { resolved.mockRestore(); } + }); +}); + +} diff --git a/tests/server/management-provider-validation.test.ts b/tests/server/management-provider-validation.test.ts index e4ecb0625d1..c474870126c 100644 --- a/tests/server/management-provider-validation.test.ts +++ b/tests/server/management-provider-validation.test.ts @@ -1,3 +1,4 @@ +import { config, registerRelativeSendPathTests } from "../helpers/management-relative-send-paths"; import { afterEach, beforeEach, describe, expect, setDefaultTimeout, spyOn, test } from "bun:test"; import { managementFetch as fetch, ManagementRequest as Request } from "../helpers/management-auth"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; @@ -69,23 +70,6 @@ const originalGlobalFetch = globalThis.fetch; const TEST_DIR = mkdtempSync(join(tmpdir(), "ocx-management-provider-validation-")); let isolatedCodexHome: IsolatedCodexHome | null = null; -function config(hostname?: string): OcxConfig { - return { - port: 10100, - hostname, - defaultProvider: "openai", - providers: { - openai: { - adapter: "openai-chat", - baseUrl: "https://api.example.test/v1", - apiKey: "sk-secret-value", - headers: { "X-Custom": "provider-secret" }, - defaultModel: "gpt-test", - }, - }, - }; -} - const canonicalDirect = { adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex", @@ -142,6 +126,8 @@ afterEach(() => { if (existsSync(TEST_DIR)) removeTreeWithRetry(TEST_DIR); }); +registerRelativeSendPathTests(TEST_DIR); + describe("provider quota routing state", () => { function quotaConfig(name = "openrouter", baseUrl = "https://openrouter.ai/api/v1"): OcxConfig { return { port: 10100, defaultProvider: name, providers: { [name]: { diff --git a/tests/server/provider-send-path-import.test.ts b/tests/server/provider-send-path-import.test.ts new file mode 100644 index 00000000000..ad97dcd61aa --- /dev/null +++ b/tests/server/provider-send-path-import.test.ts @@ -0,0 +1,26 @@ +import { expect, test } from "bun:test"; +import { repoRoot } from "../helpers/repo-root"; + +// A prior config import can hide an initialization cycle. Keep the management +// boundary first in a fresh module cache, just as an isolated CI batch loads it. +test("management send-path validation initializes before the config facade", () => { + const child = Bun.spawnSync([ + process.execPath, + "--eval", + ` + const { providerManagementConfigError } = await import("./src/server/auth-cors.ts"); + const { getDefaultConfig } = await import("./src/config.ts"); + const provider = { + adapter: "openai-chat", baseUrl: "https://example.test/v1", + apiKey: "fixture-key", models: ["fixture-model"], responsesPath: 42, + }; + const error = providerManagementConfigError("fixture", provider); + if (!error?.includes("responsesPath must be a string")) throw new Error(String(error)); + if (getDefaultConfig().port !== 10100) throw new Error("config schema did not initialize"); + console.log("management and config initialized"); + `, + ], { cwd: repoRoot(), env: process.env, timeout: 20_000 }); + expect(new TextDecoder().decode(child.stderr)).toBe(""); + expect(child.exitCode).toBe(0); + expect(new TextDecoder().decode(child.stdout).trim()).toBe("management and config initialized"); +}, 25_000);