diff --git a/docs-site/public/pr-screenshots/1991-models-custom-windows.png b/docs-site/public/pr-screenshots/1991-models-custom-windows.png new file mode 100644 index 0000000000..d771785175 Binary files /dev/null and b/docs-site/public/pr-screenshots/1991-models-custom-windows.png differ diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index f06b217022..231b46dfec 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -493,7 +493,7 @@ export const de: Record = { "models.capApplied": "Kontext-Limit angewendet — greift bei der nächsten Codex-Runde.", "models.capSaveFailed": "Kontext-Limit konnte nicht gespeichert werden", "models.contextCapped": "350k-Limit", - "models.contextCapLabel": "Kontext-Limit", + "models.contextCapLabel": "Standardfenster / Limit", "models.v2Label": "Sub-Agent", "models.shadowCallOriginal": "⚠ {models} →", "models.v2DocsLink": "Was ist v1 / v2?", @@ -514,13 +514,13 @@ export const de: Record = { "models.v2ThreadsApplied": "Thread-Limit aktualisiert — gilt für neue Sitzungen", "models.v2ThreadsInvalid": "Thread-Limit muss eine ganze Zahl >= 1 sein", "models.v2ThreadsApply": "Anwenden", - "models.capValue": "Limit {value}", - "models.contextSettings": "Kontextfenster", - "models.contextSettingsTitle": "Kontextfenster — {provider}", + "models.capValue": "Standard {value}", + "models.contextSettings": "Eigene Fenster", + "models.contextSettingsTitle": "Eigene Fenster — {provider}", "models.contextDefault": "Anbieterstandard", "models.contextModel": "Modell", "models.contextModelOverride": "Modellüberschreibung", - "models.contextHint": "Wird verwendet, wenn Upstream-Metadaten fehlen; andernfalls begrenzt der Wert ein größeres gemeldetes Fenster. Leer lassen für automatische Erkennung.", + "models.contextHint": "Wenn das Fenster bekannt ist, tragen Sie hier das tatsächliche Codex-Fenster ein. Fehlt ein Upstream-Wert, gilt dieser Eintrag; ein größeres gemeldetes Fenster wird nur nach unten begrenzt, ein kleineres bleibt erhalten. Leer bedeutet das Anbieter-«Standardfenster / Limit», oder 128k wenn das Limit aus ist.", "models.contextAutomatic": "Automatische Erkennung", "models.contextSaved": "Kontextfenster aktualisiert — gilt ab der nächsten Codex-Runde.", "models.contextUnchanged": "Keine Änderungen am Kontextfenster zu speichern.", @@ -528,7 +528,7 @@ export const de: Record = { "models.contextInvalid": "Kontextfenster müssen positive ganze Zahlen sein", "models.contextCappedValue": "{value}-Limit", "models.setAll": "Alle setzen", - "models.setAllHint": "Wendet das {value}-Kontext-Limit auf alle gerouteten Anbieter an. Native Anbieter bleiben unberührt.", + "models.setAllHint": "Schaltet das Standardfenster {value} für alle gerouteten Anbieter ein. Fehlen context_window / context_length, wird dieser Wert das tatsächliche Codex-Fenster. Für ein einzelnes Modell nutzen Sie «Eigene Fenster» in derselben Zeile. Native Anbieter bleiben unberührt.", "models.collapseAll": "Alle einklappen", "models.expandAll": "Alle ausklappen", "models.orderHint": "Reihenfolge in der Modellauswahl: Subagents-Auswahl (in der festgelegten Reihenfolge) → übrige geroutete Modelle alphabetisch nach Anbieter, dann Modell-ID → native Modelle. Sichtbarkeitsschalter filtern nur; sie ändern diese Reihenfolge nicht.", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index d1e5389165..4bb60733fd 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -518,7 +518,7 @@ export const en = { "models.capApplied": "Context cap applied — takes effect on the next Codex turn.", "models.capSaveFailed": "Failed to save context cap", "models.contextCapped": "350k cap", - "models.contextCapLabel": "Context cap", + "models.contextCapLabel": "Default window / cap", "models.v2Label": "Sub-agent", "models.shadowCallOriginal": "⚠ {models} →", "models.v2DocsLink": "What is v1 / v2?", @@ -539,13 +539,13 @@ export const en = { "models.v2ThreadsApplied": "Thread limit updated — applies to new sessions", "models.v2ThreadsInvalid": "Thread limit must be an integer >= 1", "models.v2ThreadsApply": "Apply", - "models.capValue": "Cap {value}", - "models.contextSettings": "Context windows", - "models.contextSettingsTitle": "Context windows — {provider}", + "models.capValue": "Default {value}", + "models.contextSettings": "Custom windows", + "models.contextSettingsTitle": "Custom windows — {provider}", "models.contextDefault": "Provider default", "models.contextModel": "Model", "models.contextModelOverride": "Model override", - "models.contextHint": "Used when upstream metadata is missing; otherwise limits a larger reported window. Leave blank for automatic discovery.", + "models.contextHint": "Write the actual Codex window here when you already know it. This fills a missing upstream window and only lowers a larger reported one. Leave blank to use the provider Default window / cap, or 128k if that cap is off.", "models.contextAutomatic": "Automatic discovery", "models.contextSaved": "Context windows updated — takes effect on the next Codex turn.", "models.contextUnchanged": "No context window changes to save.", @@ -553,7 +553,7 @@ export const en = { "models.contextInvalid": "Context windows must be positive whole numbers", "models.contextCappedValue": "{value} cap", "models.setAll": "Set all", - "models.setAllHint": "Apply the {value} context cap to every routed provider. Native providers are unaffected.", + "models.setAllHint": "Turn on the {value} default window for every routed provider. Relays that omit context_window / context_length get this as the actual Codex window. Use Custom windows on a provider row to set one model by hand. Native providers are unaffected.", "models.collapseAll": "Collapse all", "models.expandAll": "Expand all", "models.orderHint": "Picker order: Subagents picks (in the selected order) → remaining routed models alphabetically by provider, then model ID → native models. Visibility switches only filter models; they do not change this order.", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index a6a34e5954..36aa8a0a01 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -503,7 +503,7 @@ export const fr: Record = { "models.capApplied": "Plafond de contexte appliqué — il prendra effet au prochain tour Codex.", "models.capSaveFailed": "Échec de l’enregistrement du plafond de contexte", "models.contextCapped": "Plafond de 350k", - "models.contextCapLabel": "Plafond de contexte", + "models.contextCapLabel": "Fenêtre par défaut / plafond", "models.v2Label": "Sous-agent", "models.shadowCallOriginal": "⚠ {models} →", "models.v2DocsLink": "Que sont v1 et v2 ?", @@ -524,13 +524,13 @@ export const fr: Record = { "models.v2ThreadsApplied": "Limite de fils mise à jour — s’applique aux nouvelles sessions", "models.v2ThreadsInvalid": "La limite de fils doit être un entier >= 1", "models.v2ThreadsApply": "Appliquer", - "models.capValue": "Plafond {value}", - "models.contextSettings": "Fenêtres de contexte", - "models.contextSettingsTitle": "Fenêtres de contexte — {provider}", + "models.capValue": "Défaut {value}", + "models.contextSettings": "Fenêtres perso", + "models.contextSettingsTitle": "Fenêtres perso — {provider}", "models.contextDefault": "Valeur par défaut du fournisseur", "models.contextModel": "Modèle", "models.contextModelOverride": "Remplacement pour le modèle", - "models.contextHint": "Utilisé lorsque les métadonnées en amont sont absentes ; sinon, limite une fenêtre déclarée plus grande. Laisser vide pour une détection automatique.", + "models.contextHint": "Si vous connaissez déjà la fenêtre, écrivez ici la fenêtre Codex réelle. Sans métadonnées amont, cette valeur est utilisée ; une fenêtre plus grande est seulement abaissée, une plus petite est conservée. Laisser vide utilise la « Fenêtre par défaut / plafond » du fournisseur, ou 128k si ce plafond est désactivé.", "models.contextAutomatic": "Détection automatique", "models.contextSaved": "Fenêtres de contexte mises à jour — prend effet au prochain tour Codex.", "models.contextUnchanged": "Aucune modification des fenêtres de contexte à enregistrer.", @@ -538,7 +538,7 @@ export const fr: Record = { "models.contextInvalid": "Les fenêtres de contexte doivent être des nombres entiers positifs", "models.contextCappedValue": "Plafond de {value}", "models.setAll": "Tout définir", - "models.setAllHint": "Appliquer le plafond de contexte de {value} à chaque fournisseur routé. Les fournisseurs natifs ne sont pas affectés.", + "models.setAllHint": "Active la fenêtre par défaut {value} pour chaque fournisseur routé. Si un relais omet context_window / context_length, cette valeur devient la fenêtre Codex réelle. Pour un seul modèle, utilisez « Fenêtres perso » sur la même ligne. Les fournisseurs natifs ne sont pas affectés.", "models.collapseAll": "Tout réduire", "models.expandAll": "Tout développer", "models.orderHint": "Ordre du sélecteur : choix des sous-agents (dans l’ordre sélectionné) → autres modèles routés, classés par ordre alphabétique du fournisseur puis par ID de modèle → modèles natifs. Les options de visibilité ne font que filtrer les modèles ; elles ne modifient pas cet ordre.", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 23889a0742..9847db888d 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -501,7 +501,7 @@ export const ja: Record = { "models.capApplied": "コンテキスト上限を適用しました — 次回の Codex ターンで有効になります。", "models.capSaveFailed": "コンテキスト上限の保存に失敗しました", "models.contextCapped": "350k 上限", - "models.contextCapLabel": "コンテキスト上限", + "models.contextCapLabel": "デフォルトウィンドウ / 上限", "models.v2Label": "サブエージェント", "models.shadowCallOriginal": "⚠ {models} →", "models.v2DocsLink": "v1 / v2 とは?", @@ -522,13 +522,13 @@ export const ja: Record = { "models.v2ThreadsApplied": "スレッド上限を更新しました — 新規セッションに適用", "models.v2ThreadsInvalid": "スレッド上限は 1 以上の整数にしてください", "models.v2ThreadsApply": "適用", - "models.capValue": "上限 {value}", - "models.contextSettings": "コンテキストウィンドウ", - "models.contextSettingsTitle": "コンテキストウィンドウ — {provider}", + "models.capValue": "デフォルト {value}", + "models.contextSettings": "カスタムウィンドウ", + "models.contextSettingsTitle": "カスタムウィンドウ — {provider}", "models.contextDefault": "プロバイダーのデフォルト", "models.contextModel": "モデル", "models.contextModelOverride": "モデル別の上書き", - "models.contextHint": "上流メタデータがない場合に使われ、メタデータがある場合は報告値の上限になります。自動検出に戻すには空欄にします。", + "models.contextHint": "すでに分かっている場合は、ここに Codex の実際のコンテキストウィンドウを書きます。上流の値がないときはこの値を使い、より大きな報告値だけ下げ、より小さな上流のコンテキストウィンドウはそのまま残します。空欄ならプロバイダーの「デフォルトウィンドウ / 上限」を使い、その上限がオフなら 128k です。", "models.contextAutomatic": "自動検出", "models.contextSaved": "コンテキストウィンドウを更新しました — 次回の Codex ターンから有効です。", "models.contextUnchanged": "保存するコンテキストウィンドウの変更はありません。", @@ -536,7 +536,7 @@ export const ja: Record = { "models.contextInvalid": "コンテキストウィンドウは正の整数で指定してください", "models.contextCappedValue": "{value} 上限", "models.setAll": "すべて設定", - "models.setAllHint": "{value} のコンテキスト上限をすべてのルーティング済みプロバイダーに適用します。ネイティブプロバイダーには影響しません。", + "models.setAllHint": "すべてのルーティング済みプロバイダーに {value} のデフォルトウィンドウをオンにします。中継が context_window / context_length を返さない場合、この値が実際の Codex ウィンドウになります。1 モデルだけ手で書くときは同じ行の「カスタムウィンドウ」を使います。ネイティブプロバイダーには影響しません。", "models.collapseAll": "すべて折りたたむ", "models.expandAll": "すべて展開", "models.orderHint": "ピッカーの順序: サブエージェントの選択(選択順) → 残りのルーティングモデルはプロバイダー別、次にモデル ID 別のアルファベット順 → ネイティブモデル。表示切り替えはモデルをフィルタするだけで、この順序は変更しません。", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index dc7fb87462..7ca9059243 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -504,7 +504,7 @@ export const ko: Record = { "models.capApplied": "컨텍스트 제한 적용됨 — 다음 Codex 턴부터 반영됩니다.", "models.capSaveFailed": "컨텍스트 제한 저장 실패", "models.contextCapped": "350k 제한", - "models.contextCapLabel": "컨텍스트 제한", + "models.contextCapLabel": "기본 창 / 상한", "models.v2Label": "서브에이전트", "models.shadowCallOriginal": "⚠ {models} →", "models.v2Mode_v1": "v1", @@ -525,13 +525,13 @@ export const ko: Record = { "models.v2ThreadsApplied": "스레드 한도 변경됨 — 새 세션부터 적용", "models.v2ThreadsInvalid": "스레드 한도는 1 이상 정수여야 합니다", "models.v2ThreadsApply": "적용", - "models.capValue": "{value} 제한", - "models.contextSettings": "컨텍스트 윈도우", - "models.contextSettingsTitle": "컨텍스트 윈도우 — {provider}", + "models.capValue": "기본 {value}", + "models.contextSettings": "사용자 지정 창", + "models.contextSettingsTitle": "사용자 지정 창 — {provider}", "models.contextDefault": "프로바이더 기본값", "models.contextModel": "모델", "models.contextModelOverride": "모델별 재정의", - "models.contextHint": "업스트림 메타데이터가 없을 때 사용하며, 메타데이터가 있으면 더 큰 보고값의 상한으로 적용합니다. 자동 검색을 사용하려면 비워 두세요.", + "models.contextHint": "이미 아는 경우 여기에 실제 Codex 컨텍스트 윈도우를 적습니다. 업스트림 값이 없으면 이 값을 쓰고, 더 큰 보고값만 낮추며, 더 작은 업스트림 컨텍스트 윈도우는 그대로 둡니다. 비우면 프로바이더의 「기본 창 / 상한」을 쓰고, 그 상한이 꺼져 있으면 128k입니다.", "models.contextAutomatic": "자동 검색", "models.contextSaved": "컨텍스트 윈도우가 업데이트되었습니다 — 다음 Codex 턴부터 적용됩니다.", "models.contextUnchanged": "저장할 컨텍스트 윈도우 변경이 없습니다.", @@ -539,7 +539,7 @@ export const ko: Record = { "models.contextInvalid": "컨텍스트 윈도우는 양의 정수여야 합니다", "models.contextCappedValue": "{value} 제한", "models.setAll": "전체 적용", - "models.setAllHint": "{value} 컨텍스트 상한을 라우팅된 모든 프로바이더에 적용합니다. 네이티브 프로바이더는 영향을 받지 않습니다.", + "models.setAllHint": "라우팅된 모든 프로바이더에 {value} 기본 창을 켭니다. 중계가 context_window / context_length 를 주지 않으면 이 값이 실제 Codex 창이 됩니다. 모델 하나만 손으로 쓰려면 같은 줄의 「사용자 지정 창」을 쓰세요. 네이티브 프로바이더는 영향을 받지 않습니다.", "models.collapseAll": "모두 접기", "models.expandAll": "모두 펼치기", "models.orderHint": "피커 순서: Subagents에서 지정한 순서 → 나머지 라우팅 모델(프로바이더, 모델 ID 순 알파벳 정렬) → 네이티브 모델. 노출 토글은 모델을 필터링할 뿐 이 순서를 바꾸지 않습니다.", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 2368ee0257..21c33c3559 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -506,7 +506,7 @@ export const ru: Record = { "models.capApplied": "Лимит контекста применён — вступит в силу на следующем ходе Codex.", "models.capSaveFailed": "Не удалось сохранить лимит контекста", "models.contextCapped": "Лимит 350k", - "models.contextCapLabel": "Лимит контекста", + "models.contextCapLabel": "Окно по умолчанию / лимит", "models.v2Label": "Подагент", "models.shadowCallOriginal": "⚠ {models} →", "models.v2DocsLink": "Что такое v1 / v2?", @@ -527,13 +527,13 @@ export const ru: Record = { "models.v2ThreadsApplied": "Лимит потоков обновлён — применяется к новым сессиям", "models.v2ThreadsInvalid": "Лимит потоков должен быть целым числом >= 1", "models.v2ThreadsApply": "Применить", - "models.capValue": "Лимит {value}", - "models.contextSettings": "Контекстные окна", - "models.contextSettingsTitle": "Контекстные окна — {provider}", + "models.capValue": "По умолч. {value}", + "models.contextSettings": "Пользовательские окна", + "models.contextSettingsTitle": "Пользовательские окна — {provider}", "models.contextDefault": "Значение провайдера", "models.contextModel": "Модель", "models.contextModelOverride": "Переопределение модели", - "models.contextHint": "Используется, если вышестоящие метаданные отсутствуют; иначе ограничивает большее заявленное окно. Оставьте поле пустым для автоматического определения.", + "models.contextHint": "Если окно уже известно, запишите здесь реальное окно Codex. При отсутствии метаданных используется это значение; большее заявленное окно только ограничивается, меньшее сохраняется. Пустое поле берёт «Окно по умолчанию / лимит» провайдера или 128k, если лимит выключен.", "models.contextAutomatic": "Автоматическое определение", "models.contextSaved": "Контекстные окна обновлены — изменения вступят в силу на следующем ходе Codex.", "models.contextUnchanged": "Нет изменений контекстных окон для сохранения.", @@ -541,7 +541,7 @@ export const ru: Record = { "models.contextInvalid": "Контекстные окна должны быть положительными целыми числами", "models.contextCappedValue": "Лимит {value}", "models.setAll": "Применить ко всем", - "models.setAllHint": "Применяет лимит контекста {value} ко всем маршрутизируемым провайдерам. Нативные провайдеры не затрагиваются.", + "models.setAllHint": "Включает окно по умолчанию {value} для всех маршрутизируемых провайдеров. Если релей не отдаёт context_window / context_length, это значение становится реальным окном Codex. Чтобы задать одну модель вручную, используйте «Пользовательские окна» в той же строке. Нативные провайдеры не затрагиваются.", "models.collapseAll": "Свернуть все", "models.expandAll": "Развернуть все", "models.orderHint": "Порядок в селекторе: модели, выбранные на странице «Подагенты» (в заданном порядке) → остальные маршрутизируемые модели по алфавиту — сначала по провайдеру, затем по ID модели → нативные модели. Переключатели видимости лишь фильтруют модели и не меняют этот порядок.", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 7644e50c89..abc47076d3 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -509,7 +509,7 @@ export const tr: Record = { "models.capApplied": "Bağlam sınırı uygulandı.", "models.capSaveFailed": "Bağlam sınırı kaydedilemedi", "models.contextCapped": "350k sınırı", - "models.contextCapLabel": "Bağlam sınırı", + "models.contextCapLabel": "Varsayılan pencere / sınır", "models.v2Label": "Alt Ajan", "models.shadowCallOriginal": "⚠ {models} →", "models.v2DocsLink": "v1 / v2 nedir?", @@ -530,13 +530,13 @@ export const tr: Record = { "models.v2ThreadsApplied": "İş parçacığı limiti güncellendi", "models.v2ThreadsInvalid": "İş parçacığı limiti >= 1 tamsayı olmalıdır", "models.v2ThreadsApply": "Uygula", - "models.capValue": "Sınır {value}", - "models.contextSettings": "Bağlam pencereleri", - "models.contextSettingsTitle": "Bağlam pencereleri — {provider}", + "models.capValue": "Varsayılan {value}", + "models.contextSettings": "Özel pencereler", + "models.contextSettingsTitle": "Özel pencereler — {provider}", "models.contextDefault": "Sağlayıcı varsayılanı", "models.contextModel": "Model", "models.contextModelOverride": "Model geçersiz kılma", - "models.contextHint": "Yukarı akış meta verileri eksik olduğunda kullanılır.", + "models.contextHint": "Pencereyi biliyorsanız gerçek Codex penceresini buraya yazın. Üst akış değer yoksa bu kullanılır; daha büyük bildirilen pencere düşürülür, daha küçük olan korunur. Boş bırakırsanız sağlayıcının «Varsayılan pencere / sınır» değeri kullanılır; o sınır kapalıysa 128k olur.", "models.contextAutomatic": "Otomatik keşif", "models.contextSaved": "Bağlam pencereleri güncellendi.", "models.contextUnchanged": "Kaydedilecek bağlam penceresi değişikliği yok.", @@ -544,7 +544,7 @@ export const tr: Record = { "models.contextInvalid": "Bağlam pencereleri pozitif tam sayılar olmalıdır", "models.contextCappedValue": "{value} sınırı", "models.setAll": "Tümünü ayarla", - "models.setAllHint": "{value} bağlam sınırını her yönlendirilen sağlayıcıya uygulayın.", + "models.setAllHint": "Her yönlendirilen sağlayıcıda {value} varsayılan pencereyi açar. Röle context_window / context_length vermezse bu değer gerçek Codex penceresi olur. Tek bir modeli elle yazmak için aynı satırdaki «Özel pencereler»i kullanın.", "models.collapseAll": "Tümünü daralt", "models.expandAll": "Tümünü genişlet", "models.orderHint": "Seçici sırası: Alt ajan seçimleri → kalan modeller.", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 4f5cb8331d..9a07183f59 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -385,7 +385,7 @@ export const zhTW: Record = { "models.capApplied": "上下文限制已套用 — 將在下一個 Codex 回合生效。", "models.capSaveFailed": "儲存上下文限制失敗", "models.contextCapped": "350k 限制", - "models.contextCapLabel": "上下文限制", + "models.contextCapLabel": "預設視窗 / 上限", "models.v2Label": "子代理", "models.shadowCallOriginal": "⚠ {models} →", "models.v2DocsLink": "v1 / v2 是什麼?", @@ -406,10 +406,10 @@ export const zhTW: Record = { "models.v2ThreadsApplied": "執行緒上限已更新 — 新會話生效", "models.v2ThreadsInvalid": "執行緒上限必須為 >= 1 的整數", "models.v2ThreadsApply": "套用", - "models.capValue": "限制 {value}", + "models.capValue": "預設 {value}", "models.contextCappedValue": "{value} 限制", "models.setAll": "全部設定", - "models.setAllHint": "將 {value} 上下文上限套用到所有已路由的供應商。原生供應商不受影響。", + "models.setAllHint": "為所有已路由供應商打開 {value} 預設視窗。中繼站沒回報 context_window / context_length 時,這個值就是 Codex 實際視窗。要幫單一模型手寫,用同一列上的「自訂視窗」。原生供應商不受影響。", "models.collapseAll": "全部摺疊", "models.expandAll": "全部展開", "models.orderHint": "選擇器順序:Subagents 中的選擇(按所選順序)→ 其餘已路由模型(依次按供應商、模型 ID 字母排序)→ 原生模型。可見性開關僅用於篩選,不會改變此順序。", @@ -1771,12 +1771,12 @@ export const zhTW: Record = { "models.subtitle.combos": "將多個模型合成一個 id 來回答。容錯移轉會依序嘗試目標;輪詢則分攤負載。", "models.subtitle.compatibility": "來自實驗室投影證據的唯讀相容性判定矩陣。", "models.subtitle.routing": "原則設定檔、dry-run 評估,以及有來源依據的路由分析。", - "models.contextSettings": "上下文視窗", - "models.contextSettingsTitle": "上下文視窗 — {provider}", + "models.contextSettings": "自訂視窗", + "models.contextSettingsTitle": "自訂視窗 — {provider}", "models.contextDefault": "供應商預設", "models.contextModel": "模型", "models.contextModelOverride": "模型覆寫", - "models.contextHint": "上游缺少中繼資料時使用此值;否則會限制較大的回報視窗。留空以自動偵測。", + "models.contextHint": "已經知道視窗時,在這裡手寫 Codex 實際視窗。上游沒回報視窗就用這個值;上游回報更大視窗才壓低。留空則使用供應商的「預設視窗 / 上限」;那個開關沒開時才回退 128k。", "models.contextAutomatic": "自動偵測", "models.contextSaved": "上下文視窗已更新 — 將在下一個 Codex 回合生效。", "models.contextUnchanged": "沒有需要儲存的上下文視窗變更。", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 3589644f4a..f8f147ef8d 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -501,7 +501,7 @@ export const zh: Record = { "models.capApplied": "上下文限制已应用 — 将在下一个 Codex 回合生效。", "models.capSaveFailed": "保存上下文限制失败", "models.contextCapped": "350k 限制", - "models.contextCapLabel": "上下文限制", + "models.contextCapLabel": "默认窗口 / 上限", "models.v2Label": "子代理", "models.shadowCallOriginal": "⚠ {models} →", "models.v2Mode_v1": "v1", @@ -522,13 +522,13 @@ export const zh: Record = { "models.v2ThreadsApplied": "线程上限已更新 — 新会话生效", "models.v2ThreadsInvalid": "线程上限必须为 >= 1 的整数", "models.v2ThreadsApply": "应用", - "models.capValue": "限制 {value}", - "models.contextSettings": "上下文窗口", - "models.contextSettingsTitle": "上下文窗口 — {provider}", + "models.capValue": "默认 {value}", + "models.contextSettings": "自定义窗口", + "models.contextSettingsTitle": "自定义窗口 — {provider}", "models.contextDefault": "提供方默认值", "models.contextModel": "模型", "models.contextModelOverride": "模型覆盖值", - "models.contextHint": "上游缺少元数据时使用该值;上游已有元数据时,它只限制更大的报告值。留空则恢复自动发现。", + "models.contextHint": "已经知道窗口时,在这里手写 Codex 实际窗口。上游没报窗口就用这个值;上游报了更大窗口才压低。留空则使用提供方的「默认窗口 / 上限」;那个开关没开时才回退 128k。", "models.contextAutomatic": "自动发现", "models.contextSaved": "上下文窗口已更新 — 将在下一个 Codex 回合生效。", "models.contextUnchanged": "没有需要保存的上下文窗口更改。", @@ -536,7 +536,7 @@ export const zh: Record = { "models.contextInvalid": "上下文窗口必须为正整数", "models.contextCappedValue": "{value} 限制", "models.setAll": "全部设置", - "models.setAllHint": "将 {value} 上下文上限应用到所有已路由的提供方。原生提供方不受影响。", + "models.setAllHint": "给所有已路由提供方打开 {value} 默认窗口。中转站没报 context_window / context_length 时,这个值就是 Codex 实际窗口。要给单个模型手写,用同一行上的「自定义窗口」。原生提供方不受影响。", "models.collapseAll": "全部折叠", "models.expandAll": "全部展开", "models.orderHint": "选择器顺序:Subagents 中的选择(按所选顺序)→ 其余已路由模型(依次按提供方、模型 ID 字母排序)→ 原生模型。可见性开关仅用于筛选,不会改变此顺序。", diff --git a/src/codex/catalog/effort.ts b/src/codex/catalog/effort.ts index a2c462d046..3bf5daa086 100644 --- a/src/codex/catalog/effort.ts +++ b/src/codex/catalog/effort.ts @@ -12,7 +12,7 @@ import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, mo import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata"; import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive"; import { getProviderRegistryEntry } from "../../providers/registry"; -import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap"; +import { applyProviderContextCap, providerContextCap, resolveUnknownRoutedContextWindow } from "../../providers/context-cap"; import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec"; import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity"; import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery"; @@ -122,11 +122,14 @@ export function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel) // so genuine upstream marketing names are preserved untouched. const displayName = typeof model.displayName === "string" ? model.displayName.trim() : ""; if (displayName) entry.display_name = displayName; - if (typeof model.contextWindow === "number" && model.contextWindow > 0) { - entry.context_window = model.contextWindow; - entry.max_context_window = model.contextWindow; + const resolvedContext = typeof model.contextWindow === "number" && model.contextWindow > 0 + ? model.contextWindow + : (model.contextCap !== undefined ? resolveUnknownRoutedContextWindow(model.contextCap) : undefined); + if (typeof resolvedContext === "number" && resolvedContext > 0) { + entry.context_window = resolvedContext; + entry.max_context_window = resolvedContext; entry.auto_compact_token_limit = Math.min( - Math.floor(model.contextWindow * 0.9), + Math.floor(resolvedContext * 0.9), model.maxInputTokens ?? Number.POSITIVE_INFINITY, ); } diff --git a/src/codex/catalog/provider-fetch.ts b/src/codex/catalog/provider-fetch.ts index 2675278d07..f1acb1f699 100644 --- a/src/codex/catalog/provider-fetch.ts +++ b/src/codex/catalog/provider-fetch.ts @@ -39,7 +39,7 @@ import { } from "../../providers/service-tier"; import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport } from "../../providers/registry"; import { parseAntigravityAvailableModels, registerAntigravityDiscoveredWireModels } from "../../providers/antigravity-models"; -import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap"; +import { applyProviderContextCap, providerContextCap, resolveUnknownRoutedContextWindow } from "../../providers/context-cap"; import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec"; import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity"; import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery"; @@ -648,15 +648,16 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig, const supportsReasoningSummaries = configuredReasoningSummarySupport(prov, model.id); const supportsServiceTier = serviceTierSupportForModel(prov, model.id, name); const { supportsServiceTier: _staleServiceTier, ...modelWithoutServiceTier } = model; + // 已发现窗口只允许被配置值压低;缺窗口时,已开的 Context cap 就是实际窗口。 + const discoveredWindow = typeof model.contextWindow === "number" && model.contextWindow > 0 + ? model.contextWindow + : undefined; + const hintedWindow = discoveredWindow !== undefined + ? (configuredCap !== undefined ? Math.min(discoveredWindow, configuredCap) : discoveredWindow) + : (configuredCap ?? (providerCap !== undefined ? resolveUnknownRoutedContextWindow(providerCap) : undefined)); const hinted = { ...modelWithoutServiceTier, - ...(configuredCap !== undefined - ? { - contextWindow: typeof model.contextWindow === "number" && model.contextWindow > 0 - ? Math.min(model.contextWindow, configuredCap) - : configuredCap, - } - : {}), + ...(hintedWindow !== undefined ? { contextWindow: hintedWindow } : {}), ...(inputModalities ? { inputModalities } : {}), ...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}), ...(configuredMaxInput !== undefined @@ -695,9 +696,11 @@ export function applyConfigHintsToCachedModels(name: string, prov: OcxProviderCo /** - * Last-resort context window for combo member synthesis when discovery and - * provider config both omit one. Matches the catalog entry default in - * `normalizeRoutedCatalogEntry` so incomplete live rows still catalog. + * Last-resort context window for combo member synthesis when discovery, + * provider config, and an enabled Context cap all omit one. Matches the + * catalog entry default in `normalizeRoutedCatalogEntry` so incomplete live + * rows still catalog. An enabled Context cap is the operator-facing window, + * not a clamp on this placeholder. */ const COMBO_MEMBER_CONTEXT_FALLBACK = 128_000; @@ -715,9 +718,9 @@ interface ComboCatalogMemberFallback { * lacks a positive contextWindow, synthesize from the (registry-enriched) * provider config so combos remain catalogued when targets are configured but * discovery metadata is incomplete. Disabled providers stay unresolved. - * When hints still omit contextWindow, prefer known maxInputTokens, else - * COMBO_MEMBER_CONTEXT_FALLBACK so a live row without ctx does not drop the - * whole combo from the public catalog. + * When hints still omit contextWindow, prefer known maxInputTokens, else the + * enabled Context cap, else COMBO_MEMBER_CONTEXT_FALLBACK so a live row + * without ctx does not drop the whole combo from the public catalog. */ export function resolveComboCatalogMember( target: { provider: string; model: string }, @@ -806,12 +809,15 @@ export function resolveComboCatalogMember( const uncappedContext = hintedContext ?? knownMaxInput ?? fallbackContext - ?? (existing || prov ? COMBO_MEMBER_CONTEXT_FALLBACK : undefined); + ?? (existing || prov ? resolveUnknownRoutedContextWindow(contextCap) : undefined); if (uncappedContext === undefined) return undefined; - const usedFallback = hintedContext === undefined; - const cappedContext = applyProviderContextCap(uncappedContext, contextCap); + // 真发现值才压低。resolveUnknownRoutedContextWindow 已经把 cap 当成窗口填进去了,不能再 min 一次。 + const usedDiscoveredWindow = hintedContext !== undefined || knownMaxInput !== undefined || fallbackContext !== undefined; + const cappedContext = usedDiscoveredWindow + ? applyProviderContextCap(uncappedContext, contextCap) + : uncappedContext; const contextWindow = cappedContext ?? uncappedContext; - const fallbackCapped = usedFallback + const fallbackCapped = usedDiscoveredWindow && contextCap !== undefined && cappedContext !== undefined && cappedContext !== uncappedContext; diff --git a/src/codex/catalog/sync.ts b/src/codex/catalog/sync.ts index 9ee2886028..6c4b859060 100644 --- a/src/codex/catalog/sync.ts +++ b/src/codex/catalog/sync.ts @@ -307,8 +307,8 @@ export function deriveEntry( if (isRouted) { // A routed model is NOT the native template: never inherit its context // window when /models omits context metadata (#992). Known metadata - // restores exact values below; otherwise the strict-fields fallback - // supplies the conservative 128k triple. + // restores exact values below; an enabled Context cap fills the gap; + // otherwise the strict-fields fallback supplies the 128k triple. if (!codexForwardNativeCapabilityAlias) { delete e.context_window; delete e.max_context_window; diff --git a/src/providers/context-cap.ts b/src/providers/context-cap.ts index 4a4ac3f376..24834bb522 100644 --- a/src/providers/context-cap.ts +++ b/src/providers/context-cap.ts @@ -27,6 +27,15 @@ export function applyProviderContextCap(contextWindow: number | undefined, cap: return contextWindow > cap ? cap : contextWindow; } +/** + * 上游没报窗口时,已开启的 Context cap 就是实际窗口。 + * 128k 只是 Codex 解析器的兼容底线,不能当成“已发现窗口”再拿去和 cap 做 min。 + */ +export function resolveUnknownRoutedContextWindow(cap: number | undefined): number { + const window = isValidContextCap(cap) ? Math.floor(cap) : 0; + return window > 0 ? window : 128_000; +} + /** Effective global cap value: explicit config value, else the built-in default. */ export function globalContextCapValue(config: Pick): number { const value = config.contextCapValue; diff --git a/tests/codex-catalog.test.ts b/tests/codex-catalog.test.ts index 7086b6850a..d41daa58ad 100644 --- a/tests/codex-catalog.test.ts +++ b/tests/codex-catalog.test.ts @@ -1441,7 +1441,7 @@ describe("combo catalog capability intersection", () => { maxInputTokens: 128_000, inputModalities: ["text"], }); - // Provider contextCap below the 128k fallback clamps the synthesized window. + // Provider contextCap below the 128k fallback fills the unknown window. expect(resolveComboCatalogMember( { provider: "a", model: "ghost" }, new Map(), @@ -1453,9 +1453,8 @@ describe("combo catalog capability intersection", () => { contextWindow: 64_000, maxInputTokens: 64_000, contextCap: 64_000, - contextCapped: true, }); - // Cap above the fallback leaves 128k (no artificial raise, no capped flag). + // Cap above the empty discovery window fills that window. This is not a clamp. const aboveCap = resolveComboCatalogMember( { provider: "a", model: "ghost" }, new Map(), @@ -1463,10 +1462,10 @@ describe("combo catalog capability intersection", () => { 200_000, ); expect(aboveCap).toMatchObject({ - contextWindow: 128_000, - maxInputTokens: 128_000, + contextWindow: 200_000, + maxInputTokens: 200_000, + contextCap: 200_000, }); - // Cap may be recorded for bookkeeping (contextCapped: false) but must not claim a clamp. expect(aboveCap?.contextCapped).toBeFalsy(); // No provider entry and no discovery row — cannot invent a member. expect(resolveComboCatalogMember( @@ -4299,12 +4298,14 @@ describe("Codex catalog routed normalization", () => { expect(routed?.auto_compact_token_limit).toBe(115_200); }); - test("a provider context cap never invents routed capacity (#992)", () => { + test("a provider context cap fills an unknown routed window (#992)", () => { const entries = buildCatalogEntries({ context_window: 372_000 }, [], [ - { provider: "relay", id: "relay-model", contextCap: 950_000 }, + { provider: "relay", id: "relay-model", contextCap: 350_000 }, ]); const routed = entries.find(e => e.slug === "relay/relay-model"); - expect(routed?.context_window).toBe(128_000); + expect(routed?.context_window).toBe(350_000); + expect(routed?.max_context_window).toBe(350_000); + expect(routed?.auto_compact_token_limit).toBe(315_000); }); test("known routed metadata still restores the exact context window (#992)", () => { @@ -4752,6 +4753,37 @@ describe("Codex catalog routed normalization", () => { expect(routed?.auto_compact_token_limit).toBe(115_200); }); + test("an id-only model with an enabled context cap uses that cap as the window", async () => { + globalThis.fetch = (async () => new Response( + JSON.stringify({ data: [{ id: "gpt-5.6-terra" }] }), + { status: 200, headers: { "content-type": "application/json" } }, + )) as typeof fetch; + + const models = await gatherRoutedModels({ + port: 10100, + defaultProvider: "sub2api", + providerContextCaps: { sub2api: 350_000 }, + providers: { + sub2api: { + adapter: "openai-chat", + baseUrl: "https://sub2api.test/v1", + apiKey: "sk-test", + }, + }, + }); + const routed = buildCatalogEntries(nativeTemplate(), [], models) + .find(e => e.slug === "sub2api/gpt-5.6-terra"); + + expect(models.find(m => m.id === "gpt-5.6-terra")).toMatchObject({ + contextWindow: 350_000, + contextCap: 350_000, + contextCapped: false, + }); + expect(routed?.context_window).toBe(350_000); + expect(routed?.max_context_window).toBe(350_000); + expect(routed?.auto_compact_token_limit).toBe(315_000); + }); + test("upstream metadata smaller than the configured window wins (#1073)", async () => { globalThis.fetch = (async () => new Response( JSON.stringify({ data: [{ id: "gpt-5.6-luna", context_length: 64_000 }] }), @@ -4880,7 +4912,7 @@ describe("Codex catalog routed normalization", () => { }); }); - test("provider context-cap toggle lowers only known windows above 350k", async () => { + test("provider context-cap toggle lowers known windows above 350k and fills unknown ones", async () => { globalThis.fetch = (async () => new Response(JSON.stringify({ data: [ { id: "wide-model", metadata: { limits: { max_context_length: 500_000 } } }, @@ -4913,13 +4945,13 @@ describe("Codex catalog routed normalization", () => { contextCapped: false, }); expect(models.find(m => m.id === "unknown-model")).toMatchObject({ + contextWindow: 350_000, contextCap: 350_000, contextCapped: false, }); - expect(models.find(m => m.id === "unknown-model")?.contextWindow).toBeUndefined(); }); - test("provider context-cap toggle does not invent context for static no-metadata models", async () => { + test("provider context-cap toggle fills static no-metadata models", async () => { let fetchCalls = 0; globalThis.fetch = (() => { fetchCalls += 1; @@ -4943,10 +4975,10 @@ describe("Codex catalog routed normalization", () => { expect(fetchCalls).toBe(0); expect(models.find(m => m.id === "static-no-context")).toMatchObject({ + contextWindow: 350_000, contextCap: 350_000, contextCapped: false, }); - expect(models.find(m => m.id === "static-no-context")?.contextWindow).toBeUndefined(); }); test("provider context-window caps apply to stale cached metadata", async () => { diff --git a/tests/context-cap-unknown-window.test.ts b/tests/context-cap-unknown-window.test.ts new file mode 100644 index 0000000000..79e46ff410 --- /dev/null +++ b/tests/context-cap-unknown-window.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, test } from "bun:test"; +import { + applyProviderContextCap, + resolveUnknownRoutedContextWindow, +} from "../src/providers/context-cap"; + +describe("unknown routed context windows", () => { + test("an enabled cap fills a missing window instead of inventing 128k", () => { + expect(resolveUnknownRoutedContextWindow(350_000)).toBe(350_000); + expect(applyProviderContextCap(undefined, 350_000)).toBeUndefined(); + }); + + test("a discovered window is only lowered", () => { + expect(applyProviderContextCap(128_000, 350_000)).toBe(128_000); + expect(applyProviderContextCap(500_000, 350_000)).toBe(350_000); + }); + + test("no cap keeps the conservative 128k fallback", () => { + expect(resolveUnknownRoutedContextWindow(undefined)).toBe(128_000); + }); + + test("a fractional cap that floors to zero does not invent a zero window", () => { + expect(resolveUnknownRoutedContextWindow(0.5)).toBe(128_000); + }); +});