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 2b8545629c..99d188e5cf 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. 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 9b5acfa274..37e46224eb 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` を検証します。 設定ファイルの読み込みにも同じ経路の規則が適用されます。 + 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 b6657ba58d..0eb8e36f84 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`를 검증한 뒤 메모리와 파일의 설정을 변경합니다. 설정 파일을 읽을 때도 같은 경로 규칙을 적용합니다. + 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 9a0b822f52..d646ea80cb 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. 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 ca389d9ffd..bca8117c4c 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` до изменения конфигурации в памяти или на диске. Те же правила путей применяются при загрузке файла конфигурации. + Провайдеры с 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 3126b9046c..747dc78ca6 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. 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 b28a5a2af4..3ef62189a7 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`,再修改内存或磁盘中的配置。 加载配置文件时也适用同样的路径规则。 + 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 0680fa35aa..7ab577ee1b 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`,再修改記憶體或磁碟中的設定。 載入設定檔時也適用相同的路徑規則。 + 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 54c118ab30..f200ae8f90 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 0000000000..e167dd8d30 --- /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 f9deab442b..e6607bf814 100644 --- a/src/config/schema/leaf-validators.ts +++ b/src/config/schema/leaf-validators.ts @@ -299,21 +299,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 be6bfd3fca..3e23b0dc48 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/adapters/registry.md b/structure/adapters/registry.md index 1eaa48e016..70c28308a5 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -1,5 +1,7 @@ # Adapter Registry Authority +Management provider-validation calls use the [initialization-independent relative send-path validation](../config.md#provider-relative-send-paths) before persistence. + Request-local adapter bindings are separate from registry authority in the Responses [core module ownership](../transports/responses.md#core-module-ownership). This surface retains its existing behavior. diff --git a/structure/catalog.md b/structure/catalog.md index 03636900b0..5af242d6d3 100644 --- a/structure/catalog.md +++ b/structure/catalog.md @@ -1,5 +1,7 @@ # Model Catalog +Management provider-validation calls use the [initialization-independent relative send-path validation](config.md#provider-relative-send-paths) before persistence. + Catalog discovery remains separate from the Responses final-route [core module ownership](transports/responses.md#core-module-ownership). This surface retains its existing behavior. diff --git a/structure/clients/claude-desktop.md b/structure/clients/claude-desktop.md index 3c7ec11ea9..bbe0ac1b8c 100644 --- a/structure/clients/claude-desktop.md +++ b/structure/clients/claude-desktop.md @@ -1,5 +1,7 @@ # Claude Desktop Integration +Management provider-validation calls use the [initialization-independent relative send-path validation](../config.md#provider-relative-send-paths) before persistence. + Desktop callers retain their existing ingress through the Responses [core module ownership](../transports/responses.md#core-module-ownership). This surface retains its existing behavior. diff --git a/structure/config.md b/structure/config.md index e756575a91..82ca7fdeae 100644 --- a/structure/config.md +++ b/structure/config.md @@ -242,6 +242,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/data-planes/images.md b/structure/data-planes/images.md index 99d4a68584..e0171c41db 100644 --- a/structure/data-planes/images.md +++ b/structure/data-planes/images.md @@ -1,5 +1,7 @@ # Images Data Plane +Management provider-validation calls use the [initialization-independent relative send-path validation](../config.md#provider-relative-send-paths) before persistence. + Vision preprocessing and image/video/search execution use the Responses [core module ownership](../transports/responses.md#core-module-ownership). This surface retains its existing behavior. diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index c4acea5037..84cfeef15e 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -1,5 +1,7 @@ # Inbound Compatibility Surfaces +Management provider-validation calls use the [initialization-independent relative send-path validation](../config.md#provider-relative-send-paths) before persistence. + Compatibility callers retain the public Responses ingress described by the [core module ownership](../transports/responses.md#core-module-ownership). This surface retains its existing behavior. diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index b4d9490772..13e7dcff10 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -4,7 +4,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/ops/service-and-sidecars.md b/structure/ops/service-and-sidecars.md index 2095aa257f..1402d6271a 100644 --- a/structure/ops/service-and-sidecars.md +++ b/structure/ops/service-and-sidecars.md @@ -1,5 +1,7 @@ # Background Service And Sidecars +Management provider-validation calls use the [initialization-independent relative send-path validation](../config.md#provider-relative-send-paths) before persistence. + Service endpoints are unchanged by the Responses [core module ownership](../transports/responses.md#core-module-ownership). This surface retains its existing behavior. diff --git a/structure/overview.md b/structure/overview.md index d8f01d4bbc..2309e736d2 100644 --- a/structure/overview.md +++ b/structure/overview.md @@ -1,5 +1,7 @@ # Overview +Management provider-validation calls use the [shared relative send-path validation](config.md#provider-relative-send-paths) before persistence. + 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. diff --git a/structure/providers/openai-tiers.md b/structure/providers/openai-tiers.md index 38f72878ab..3da4964cd9 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/providers/xai-grok.md b/structure/providers/xai-grok.md index e2b5ee6f0f..21a8626c5d 100644 --- a/structure/providers/xai-grok.md +++ b/structure/providers/xai-grok.md @@ -1,5 +1,7 @@ # xAI Grok Provider +Management provider-validation calls use the [initialization-independent relative send-path validation](../config.md#provider-relative-send-paths) before persistence. + xAI uses the same shared credential and delivery policies through the Responses [core module ownership](../transports/responses.md#core-module-ownership). This surface retains its existing behavior. diff --git a/structure/runtime.md b/structure/runtime.md index 09ce8a1f11..355471891e 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -1,5 +1,7 @@ # Runtime +Management provider-validation calls use the [initialization-independent relative send-path validation](config.md#provider-relative-send-paths) before persistence. + Responses admission and finalization are composed through the [core module ownership](transports/responses.md#core-module-ownership). This surface retains its existing behavior. diff --git a/structure/subagents.md b/structure/subagents.md index bebd8fe79b..0937a45181 100644 --- a/structure/subagents.md +++ b/structure/subagents.md @@ -1,5 +1,7 @@ # Subagents And Multi-Agent Surface +Management provider-validation calls use the [initialization-independent relative send-path validation](config.md#provider-relative-send-paths) before persistence. + Encrypted-task and fallback request handling follow the Responses [core module ownership](transports/responses.md#core-module-ownership). This surface retains its existing behavior. diff --git a/structure/transports/byte-accounting.md b/structure/transports/byte-accounting.md index 72ed6119ce..f6930127e4 100644 --- a/structure/transports/byte-accounting.md +++ b/structure/transports/byte-accounting.md @@ -1,5 +1,7 @@ # Byte Accounting +Management provider-validation calls use the [initialization-independent relative send-path validation](../config.md#provider-relative-send-paths) before persistence. + Responses body-reader limits and lifetime handling follow the [core module ownership](responses.md#core-module-ownership). This surface retains its existing behavior. diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index 6b55bbaf0d..d0d7d3925a 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -1,5 +1,7 @@ # Transport Inventory +Management provider-validation calls use the [initialization-independent relative send-path validation](../config.md#provider-relative-send-paths) before persistence. + The existing Responses transport is divided by responsibility in the [core module ownership](responses.md#core-module-ownership). This surface retains its existing behavior. diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 94dc919bbe..a0ae75b96d 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -1,7 +1,7 @@ # Responses Transport 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 6b901c9db4..086e178a19 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 8448b5fabe..c990358f20 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 0000000000..e59e0628fe --- /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 e4ecb0625d..c474870126 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 0000000000..ad97dcd61a --- /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);