diff --git a/docs-site/public/pr-screenshots/t4-log-polling-81a8.png b/docs-site/public/pr-screenshots/t4-log-polling-81a8.png new file mode 100644 index 0000000000..4dd53042e9 Binary files /dev/null and b/docs-site/public/pr-screenshots/t4-log-polling-81a8.png differ diff --git a/docs-site/public/pr-screenshots/t4-model-picker-81a8.png b/docs-site/public/pr-screenshots/t4-model-picker-81a8.png new file mode 100644 index 0000000000..0754aefc3c Binary files /dev/null and b/docs-site/public/pr-screenshots/t4-model-picker-81a8.png differ diff --git a/docs-site/src/content/docs/fr/guides/model-ordering.md b/docs-site/src/content/docs/fr/guides/model-ordering.md index ada196c8fc..b22bf9b827 100644 --- a/docs-site/src/content/docs/fr/guides/model-ordering.md +++ b/docs-site/src/content/docs/fr/guides/model-ordering.md @@ -113,7 +113,7 @@ Utilisez `subagentModels` pour choisir et ordonner les premiers modèles que Cod `spawn_agent`. La page **Sous-agents** du tableau de bord peut réorganiser les identifiants natifs non qualifiés et les identifiants routés. Utilisez `ocx agent subagents set` ou modifiez la configuration OpenCodex pour définir des choix exacts de la forme `/` ; le tableau de bord -ne les répertorie pas et les omet s’il enregistre la liste. Configurez au maximum cinq identifiants. Avec +conserve ces identifiants déjà enregistrés, même indisponibles. Configurez au maximum cinq identifiants. Avec des sélecteurs de compte, un choix natif non qualifié peut se décliner en plusieurs lignes de catalogue qualifiées par sélecteur ; les choix configurés et les lignes annoncées ne correspondent donc pas nécessairement un à un. @@ -178,3 +178,11 @@ l’outil natif. `disabledModels` et `selectedModels` de chaque fournisseur restent des champs de visibilité, pas des contrôles d’ordre. Il n’existe aucun paramètre distinct `modelOrder`, `providerOrder` ou de carte de priorité. + +## Préréglages du sélecteur + +Dans **Models**, choisissez Par défaut, A–Z par modèle, Par fournisseur ou Instantané des usages, puis appliquez l’ordre. Les identifiants routés actuellement disponibles et `modelPickerOrderMode` (`alphabetical`, `provider`, `most-used`) sont enregistrés. Les usages conservés sont lus une seule fois lors de l’application ; un rechargement ou un changement de modèles ne recalcule rien. Un ordre personnalisé ou natif complet reste intact jusqu’à une application explicite. Par défaut efface les deux champs même sans modèle disponible. + +`GET/PUT /api/subagent-models` conserve les choix enregistrés désactivés ou absents dans `chosen` et `available` ; `pickerAvailable` contient les identifiants routés admissibles. Models envoie `pickerOrder` et `pickerOrderMode`, jamais `models`. Une sauvegarde du roster seul conserve l’ordre. Entrée invalide ou échec de sauvegarde préserve l’état précédent. + +Les plages prioritaires et natives restent en place. Les préréglages affectent le catalogue Codex et les groupes routés de la découverte Claude, sans modifier le préfixe natif Claude ni les profils Desktop explicites ou la propriété des alias. Les rangs de guidage OpenCodex et les réglages de repli sont conservés ; les cinq choix annoncés nativement par Codex et le défaut recommandé peuvent changer. Aucun client n’est redémarré ; une actualisation peut rester en attente et nécessiter de rouvrir le client. diff --git a/docs-site/src/content/docs/guides/model-ordering.md b/docs-site/src/content/docs/guides/model-ordering.md index 79e74e8e2d..2d33b409d8 100644 --- a/docs-site/src/content/docs/guides/model-ordering.md +++ b/docs-site/src/content/docs/guides/model-ordering.md @@ -112,8 +112,7 @@ have expanded into selector-qualified groups. Use `subagentModels` to choose and order the leading models that Codex also advertises to `spawn_agent`. The dashboard's **Sub-agents** page can reorder bare native and routed ids. Use `ocx agent subagents set` or edit the opencodex configuration for exact -`/` choices; the dashboard does not list those choices and omits them -if it saves the roster. Use at most five configured ids. With account selectors, one bare native +`/` choices; the dashboard retains those ids once saved, including choices currently unavailable. Use at most five configured ids. With account selectors, one bare native choice can expand into multiple selector-qualified catalog rows, so configured choices and advertised rows are not necessarily one-to-one. @@ -171,3 +170,11 @@ not reorder the native tool's advertised list. `disabledModels` and each provider's `selectedModels` remain visibility fields, not ordering controls. There is no separate `modelOrder`, `providerOrder`, or priority-map setting. + +## Dashboard picker presets + +On **Models**, choose **Default**, **A–Z by model**, **Group by provider**, or **Most used snapshot**, then **Apply order**. This saves the currently visible routed ids and `modelPickerOrderMode` (`alphabetical`, `provider`, or `most-used`). Most used reads all retained usage once when applied; reloads restore the snapshot without fetching usage again. New or removed models do not automatically recompute it. A manually saved order, including a complete/native order, remains untouched until you explicitly apply a replacement. Default clears both picker fields, even when no routed models are available. + +The controls use `GET/PUT /api/subagent-models`: `chosen` and `available` retain saved roster choices, including disabled or missing models; `pickerAvailable` contains only eligible routed catalog ids. The Models page sends `pickerOrder` and `pickerOrderMode`, never `models`. Roster-only saves preserve picker settings. Invalid combined updates and failed persistence leave the previous picker/roster state intact. + +Routed-only presets keep the existing featured/native priority bands. They affect the Codex catalog and Claude discovery's routed groups; Claude's native prefix and explicit Desktop profile/alias ownership remain unchanged. OpenCodex guidance ranks and configured fallback settings are preserved, but native Codex's advertised five and recommended default can change with display priority. Saving does not restart clients; a catalog refresh may remain pending, and clients holding an old catalog may need reopening. diff --git a/docs-site/src/content/docs/ja/guides/model-ordering.md b/docs-site/src/content/docs/ja/guides/model-ordering.md index d9c782b3fb..74febb1e78 100644 --- a/docs-site/src/content/docs/ja/guides/model-ordering.md +++ b/docs-site/src/content/docs/ja/guides/model-ordering.md @@ -109,8 +109,7 @@ account selector がある場合、5 項目の制限は bare native の選択が 先頭モデルの順序を変更するサポート手段は、`subagentModels` を並べ替えることです。 ダッシュボードの **Sub-agents** ページでは bare native id と routed id を並べ替えられます。 設定と `ocx agent subagents set` は exact account-qualified -`/` id も受け付けますが、ダッシュボードはこれらの id を表示せず、 -リストの保存時にも保持しません。設定する id は最大 5 つにしてください。account selector がある +`/` id も受け付けます。ダッシュボードは保存済みの id を現在利用できなくても保持します。設定する id は最大 5 つにしてください。account selector がある 場合は 1 つの bare native が複数の selector-qualified 行に展開されるため、設定した選択肢と公開 される行は必ずしも一対一ではありません。 @@ -155,3 +154,11 @@ V1 には OpenCodex の推奨候補リストを注入しません。V2 にはク `disabledModels` と各プロバイダーの `selectedModels` は 表示の有無を制御するフィールドです。別の `modelOrder`、`providerOrder`、priority map 設定はありません。 + +## ダッシュボードの並び順プリセット + +**Models**でデフォルト、モデル名A–Z、プロバイダー別、使用量スナップショットを選び、順序を適用します。現在選択可能なルーティングIDと `modelPickerOrderMode`(`alphabetical`、`provider`、`most-used`)を保存します。使用量は適用時に保持された全履歴を一度だけ読み、再読込やモデルの増減では再計算しません。既存のカスタム・ネイティブ全体順は明示的な適用まで保持されます。デフォルトは候補が空でも両フィールドを削除できます。 + +`GET/PUT /api/subagent-models` の `chosen` と `available` は無効・欠落した保存済みrosterも保持します。`pickerAvailable` は選択可能なルーティングIDのみです。Modelsは `pickerOrder` と `pickerOrderMode` を送り、`models` は送りません。Rosterだけの保存は順序設定を維持し、不正入力・保存失敗は以前の状態を保ちます。 + +featured・ネイティブの優先順位帯を保ち、CodexカタログとClaude検出のルーティンググループに適用します。Claudeのネイティブ先頭グループと明示的なDesktopプロファイル・alias所有権は維持されます。OpenCodexのガイド順位とfallback設定は変わりませんが、ネイティブCodexの上位5候補や推奨デフォルトは変わる場合があります。保存による再起動は行いません。更新が保留中、または古いカタログを保持するクライアントでは開き直しが必要な場合があります。 diff --git a/docs-site/src/content/docs/ko/guides/model-ordering.md b/docs-site/src/content/docs/ko/guides/model-ordering.md index 365ea476fc..e921bfdfbc 100644 --- a/docs-site/src/content/docs/ko/guides/model-ordering.md +++ b/docs-site/src/content/docs/ko/guides/model-ordering.md @@ -109,7 +109,7 @@ account selector가 있으면 bare native 선택이 selector-qualified 그룹으 선두 모델 순서를 바꾸는 지원 수단은 `subagentModels`를 재정렬하는 것입니다. 대시보드의 **Sub-agents** 페이지에서는 bare native와 routed id의 순서를 바꿀 수 있습니다. 설정과 `ocx agent subagents set`은 exact account-qualified `/` id도 -지원하지만, 대시보드는 이러한 id를 제공하지 않으며 목록을 저장할 때도 보존하지 않습니다. 설정 id는 +지원합니다. 대시보드는 이미 저장된 id를 현재 사용할 수 없어도 보존합니다. 설정 id는 최대 5개만 사용하세요. account selector가 있으면 bare native 하나가 여러 selector-qualified 행으로 확장될 수 있으므로 설정 항목과 노출 행이 항상 일대일로 대응하지는 않습니다. @@ -153,3 +153,11 @@ V1에는 OpenCodex의 선호 후보 목록을 주입하지 않습니다. V2는 `disabledModels`와 각 공급자의 `selectedModels`는 노출 여부를 정하는 필드입니다. 별도의 `modelOrder`, `providerOrder`, priority map 설정은 없습니다. + +## 대시보드 정렬 프리셋 + +**Models**에서 기본값·모델 이름순·프로바이더별·사용량순 스냅샷을 선택한 뒤 **순서 적용**을 누르세요. 현재 표시 가능한 라우팅 ID와 `modelPickerOrderMode`(`alphabetical`, `provider`, `most-used`)를 저장합니다. 사용량순은 적용 시 보관된 전체 사용량을 한 번 읽습니다. 다시 열거나 모델이 추가·삭제되어도 자동 재계산하지 않습니다. 기존 사용자 지정·네이티브 전체 순서는 명시적으로 적용하기 전까지 유지됩니다. 기본값은 사용 가능한 모델이 없어도 두 피커 필드를 지웁니다. + +`GET/PUT /api/subagent-models`의 `chosen`·`available`은 비활성·누락된 저장 roster도 보존하고, `pickerAvailable`은 선택 가능한 라우팅 ID만 제공합니다. Models는 `pickerOrder`·`pickerOrderMode`만 보내며 `models`를 보내지 않습니다. Roster만 저장하면 피커 설정은 유지됩니다. 잘못된 요청이나 저장 실패는 이전 상태를 보존합니다. + +라우팅 전용 프리셋은 featured·네이티브 우선순위 구간을 유지하며 Codex 카탈로그와 Claude 검색 목록의 라우팅 그룹에 적용됩니다. Claude 네이티브 선두 그룹과 명시적 Desktop 프로필·alias 소유권은 유지됩니다. OpenCodex 가이드 순위와 fallback 설정은 유지되지만 네이티브 Codex에 표시되는 상위 5개·권장 기본 모델은 달라질 수 있습니다. 저장은 클라이언트를 재시작하지 않으며 카탈로그 갱신이 미완료이면 나중에 다시 열어야 할 수 있습니다. diff --git a/docs-site/src/content/docs/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md index a12303cfd7..cef695b4ad 100644 --- a/docs-site/src/content/docs/reference/management-api.md +++ b/docs-site/src/content/docs/reference/management-api.md @@ -166,6 +166,14 @@ See [Combos](/guides/combos/) for target strategies, cooldowns, aliases, and rou ### Logs, usage, and storage +`GET /api/logs` accepts an optional opaque `cursor` from its previous response. The envelope preserves +`logs`, `total`, `generatedAt` and `timeZone`, and adds `cursor` and `reset`. Without a cursor it returns +the full filtered window. A valid unchanged prefix returns only appended rows; `reset: true` replaces +the client window after edits, eviction, query changes or restart. Invalid cursors return HTTP 400 with +`error.code: "invalid_cursor"`. Authentication is unchanged. The dashboard falls back to full snapshots +for older servers. This reduces response bytes for stable windows; server projection remains bounded +by the current window size. + | Method and path | Purpose | Notable errors | | --- | --- | --- | | `GET /api/logs` | Query filtered in-memory request logs | — | diff --git a/docs-site/src/content/docs/ru/guides/model-ordering.md b/docs-site/src/content/docs/ru/guides/model-ordering.md index 290194fcd2..5ff04e4892 100644 --- a/docs-site/src/content/docs/ru/guides/model-ordering.md +++ b/docs-site/src/content/docs/ru/guides/model-ordering.md @@ -118,7 +118,7 @@ native-выбора в selector-qualified группы. Поддерживаемый способ настроить порядок ведущих моделей — переставить элементы `subagentModels`. Страница **Sub-agents** в дашборде позволяет менять порядок bare native- и routed-id. Конфигурация и `ocx agent subagents set` также принимают точные account-qualified id -`/`, но дашборд не предлагает и не сохраняет их при записи списка. +`/`, а дашборд сохраняет уже записанные ID, даже если они недоступны. Используйте не более пяти настроенных id. При активных селекторах одна bare native-модель может развернуться в несколько selector-qualified строк, поэтому число настроенных вариантов и объявляемых строк не обязательно совпадает. @@ -170,3 +170,11 @@ native-выбора в selector-qualified группы. `disabledModels` и `selectedModels` каждого провайдера по-прежнему управляют видимостью. Отдельных настроек `modelOrder`, `providerOrder` или карты приоритетов нет. + +## Пресеты в дашборде + +На странице **Models** выберите порядок по умолчанию, по имени A–Z, по провайдеру или снимок использования и примените его. Сохраняются доступные маршрутизируемые ID и `modelPickerOrderMode` (`alphabetical`, `provider`, `most-used`). Сохранённая статистика читается один раз при применении; перезагрузка и изменения каталога не пересчитывают снимок. Пользовательский порядок, включая полный нативный, сохраняется до явного применения. Сброс удаляет оба поля даже при пустом списке моделей. + +`GET/PUT /api/subagent-models` сохраняет отключённые и отсутствующие выбранные ID в `chosen` и `available`; `pickerAvailable` содержит допустимые маршрутизируемые ID. Models отправляет `pickerOrder` и `pickerOrderMode`, но не `models`. Сохранение только roster не меняет порядок; неверный ввод и ошибка записи сохраняют предыдущее состояние. + +Диапазоны приоритетных и нативных моделей сохраняются. Порядок применяется к каталогу Codex и маршрутизируемым группам обнаружения Claude, сохраняя нативный префикс Claude, явные профили Desktop и владельцев alias. Ранги подсказок OpenCodex и настройки fallback не меняются, но пять вариантов, объявляемых нативным Codex, и рекомендуемая модель могут измениться. Сохранение не перезапускает клиенты; обновление может ожидать завершения, а старый каталог потребовать повторного открытия клиента. diff --git a/docs-site/src/content/docs/tr/guides/model-ordering.md b/docs-site/src/content/docs/tr/guides/model-ordering.md index 336513225a..ea42630154 100644 --- a/docs-site/src/content/docs/tr/guides/model-ordering.md +++ b/docs-site/src/content/docs/tr/guides/model-ordering.md @@ -131,7 +131,7 @@ yeniden sıralamaktır. Kontrol panelinin **Alt Ajanlar** sayfası yalın yerel yönlendirilen kimlikleri yeniden sıralayabilir. Tam `/` seçimleri için `ocx agent subagents set` kullanın veya opencodex yapılandırmasını düzenleyin; kontrol paneli bu seçimleri -listelemez ve kadroyu kaydederse bunları atlar. En fazla beş yapılandırılmış +önceden kaydedilen kimlikleri, kullanılamasalar bile korur. En fazla beş yapılandırılmış kimlik kullanın. Hesap seçicileriyle tek bir yalın yerel seçenek birden çok seçici nitelikli katalog satırına genişleyebilir, bu nedenle yapılandırılmış seçimler ve tanıtılan satırlar birebir olmak zorunda değildir. @@ -181,3 +181,11 @@ tanıttığı listeyi yeniden sıralamaz. `disabledModels` ve her sağlayıcının `selectedModels` alanı görünürlüğü denetler. Ayrı bir `modelOrder`, `providerOrder` veya öncelik haritası ayarı yoktur. + +## Kontrol paneli sıra önayarları + +**Models** sayfasında Varsayılan, Model adına göre A–Z, Sağlayıcıya göre veya Kullanım anlık görüntüsünü seçip sırayı uygulayın. Kullanılabilir yönlendirilmiş kimlikler ve `modelPickerOrderMode` (`alphabetical`, `provider`, `most-used`) kaydedilir. Saklanan tüm kullanım yalnızca uygulamada bir kez okunur; yeniden yükleme veya model değişiklikleri yeniden hesaplamaz. Özel ve tam yerel sıra açıkça değiştirilene kadar korunur. Varsayılan, kullanılabilir model yokken bile iki alanı temizler. + +`GET/PUT /api/subagent-models`, devre dışı veya eksik kayıtlı seçimleri `chosen` ve `available` içinde korur; `pickerAvailable` uygun yönlendirilmiş kimliklerdir. Models yalnızca `pickerOrder` ve `pickerOrderMode` gönderir, `models` göndermez. Yalnız kadroyu kaydetmek sırayı değiştirmez. Geçersiz giriş veya kayıt hatası önceki durumu korur. + +Öne çıkan ve yerel öncelik aralıkları korunur. Sıra Codex kataloğu ve Claude keşfinin yönlendirilmiş gruplarına uygulanır; Claude yerel öneki, açık Desktop profilleri ve alias sahipliği korunur. OpenCodex rehberlik sıraları ve fallback ayarları değişmez; yerel Codex aracının sunduğu ilk beş seçenek ve önerilen varsayılan değişebilir. Kayıt istemcileri yeniden başlatmaz; katalog yenilemesi bekleyebilir ve eski kataloğu gösteren istemcinin yeniden açılması gerekebilir. diff --git a/docs-site/src/content/docs/zh-cn/guides/model-ordering.md b/docs-site/src/content/docs/zh-cn/guides/model-ordering.md index a07d4cf01f..ffe4ffda87 100644 --- a/docs-site/src/content/docs/zh-cn/guides/model-ordering.md +++ b/docs-site/src/content/docs/zh-cn/guides/model-ordering.md @@ -104,7 +104,7 @@ subagentModels = [ 自定义开头模型顺序的受支持方式是重新排列 `subagentModels`。仪表盘的 **Sub-agents** 页面可以调整 裸原生和路由 id 的顺序。配置和 `ocx agent subagents set` 也接受精确的账户限定 -`/` id,但仪表盘不会提供这些 id,保存列表时也不会保留它们。配置的 +`/` id,仪表盘会保留已保存的 id,即使当前不可用。配置的 id 请勿超过五个。存在账户 selector 时,一个裸原生选项可能展开为多个 selector-qualified 行,因此 已配置的选项与公布的行不一定一一对应。 @@ -145,3 +145,11 @@ V2 在客户端目录状态允许时,可以额外接收基于原有优先级 `disabledModels` 和各提供商的 `selectedModels` 仍是可见性字段。没有独立的 `modelOrder`、 `providerOrder` 或优先级映射设置。 + +## 仪表盘排序预设 + +在 **Models** 中选择默认、按模型名 A–Z、按提供商或使用量快照,再应用顺序。保存当前可用的路由 ID 和 `modelPickerOrderMode`(`alphabetical`、`provider`、`most-used`)。使用量排序仅在应用时读取一次保留的全部历史;重新打开或模型增减不会重新计算。已有自定义、原生完整顺序会保留,直到明确应用替换。即使没有可用模型,默认也能清除两个字段。 + +`GET/PUT /api/subagent-models` 的 `chosen`、`available` 保留禁用或缺失的已存 roster;`pickerAvailable` 只包含可选路由 ID。Models 只发送 `pickerOrder`、`pickerOrderMode`,不发送 `models`。只保存 roster 不影响排序,非法输入或保存失败会保留原状态。 + +预设保留精选、原生优先级区间,应用于 Codex 目录与 Claude 发现列表的路由分组。Claude 原生前缀、明确的 Desktop 配置及 alias 归属不变。OpenCodex 指导排序与 fallback 设置不变,但原生 Codex 工具显示的前五候选及推荐默认模型可能改变。保存不会重启客户端;目录刷新可能尚未完成,旧列表可能需要重新打开客户端。 diff --git a/docs-site/src/content/docs/zh-tw/guides/model-ordering.md b/docs-site/src/content/docs/zh-tw/guides/model-ordering.md index e0946db620..d944aef199 100644 --- a/docs-site/src/content/docs/zh-tw/guides/model-ordering.md +++ b/docs-site/src/content/docs/zh-tw/guides/model-ordering.md @@ -134,3 +134,11 @@ V2 在用戶端目錄狀態允許時,可以額外接收基於原有優先級 `disabledModels` 和各供應商的 `selectedModels` 仍是可見性欄位。沒有獨立的 `modelOrder`、 `providerOrder` 或優先級對應表設定。 + +## 儀表板排序預設 + +在 **Models** 選擇預設、依模型名稱 A–Z、依供應商或使用量快照,再套用順序。儲存目前可用的路由 ID 和 `modelPickerOrderMode`(`alphabetical`、`provider`、`most-used`)。使用量排序僅在套用時讀取一次保留的全部歷史;重新開啟或模型增減不會重新計算。現有自訂與原生完整順序會保留,直到明確套用替換。即使沒有可用模型,預設也能清除兩個欄位。 + +`GET/PUT /api/subagent-models` 的 `chosen`、`available` 保留停用或缺少的已存 roster;`pickerAvailable` 僅包含可選路由 ID。Models 只傳送 `pickerOrder`、`pickerOrderMode`,不傳送 `models`。只儲存 roster 不影響排序;無效輸入或儲存失敗會保留原狀態。 + +預設保留精選與原生優先級區間,套用於 Codex 目錄和 Claude 探索清單的路由群組。Claude 原生前綴、明確的 Desktop 設定及 alias 歸屬不變。OpenCodex 指引排序和 fallback 設定不變,但原生 Codex 工具顯示的前五個候選與建議預設模型可能改變。儲存不會重新啟動用戶端;目錄更新可能尚未完成,舊清單可能需要重新開啟用戶端。 diff --git a/gui/.eslint/i18n-allowlist.ts b/gui/.eslint/i18n-allowlist.ts index acab975933..805d7dcfbc 100644 --- a/gui/.eslint/i18n-allowlist.ts +++ b/gui/.eslint/i18n-allowlist.ts @@ -59,6 +59,9 @@ export function isTechnicalLiteral(value: string): boolean { // Version prefix / unit tokens if (TECHNICAL_UNITS.has(trimmed)) return true; + // Internal Models session-cache suffix, never rendered as user-facing copy. + if (trimmed === ":picker-order") return true; + // Absolute/relative URLs and localhost endpoints if (/^https?:\/\//i.test(trimmed)) return true; if (/^https?:\/\/[^\s]+$/i.test(trimmed)) return true; diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 429379396f..9086c9bf42 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -5,6 +5,20 @@ import type { TKey } from "./en"; * German i18n catalog, generated from en.ts. Must match the `TKey` set (compile-checked). */ export const de: Record = { + "models.pickerOrder.label": "Modellreihenfolge", + "models.pickerOrder.default": "Standard", + "models.pickerOrder.alphabetical": "A–Z nach Modell", + "models.pickerOrder.provider": "Nach Anbieter", + "models.pickerOrder.mostUsed": "Nutzungsschnappschuss", + "models.pickerOrder.custom": "Eigene Reihenfolge", + "models.pickerOrder.apply": "Reihenfolge anwenden", + "models.pickerOrder.applying": "Wird angewendet…", + "models.pickerOrder.saved": "Modellreihenfolge gespeichert. Clients mit altem Katalog erneut öffnen.", + "models.pickerOrder.pending": "Reihenfolge gespeichert; Katalogaktualisierung ausstehend.", + "models.pickerOrder.usageFailed": "Modellnutzung konnte nicht geladen werden.", + "models.pickerOrder.loadFailed": "Auswahleinstellungen konnten nicht geladen werden.", + "models.pickerOrder.retry": "Erneut versuchen", + "models.pickerOrder.hint": "Speichert geroutete Modelle für Codex- und Claude-Listen. Prioritätsbereiche bevorzugter/nativer Modelle bleiben erhalten. Nutzung ist eine Momentaufnahme; nativ angebotene Optionen können sich ändern.", "codexAuth.quotaAutoRefreshAllHint": "Schaltet die unterstützten 5-Stunden- und Wochenfenster aller aktuellen Konten gemeinsam um. Im Pool-Modus wird nach jedem Reset eine kleine Anfrage gesendet, die Kontingent verbraucht.", "codexAuth.quotaAutoRefreshMixed": "Einige Fenster sind aktiviert.", "codexAuth.quotaAutoRefreshEmpty": "Keine unterstützten Kontingentfenster. Aktualisieren Sie die Kontingente der Konten.", @@ -560,7 +574,7 @@ export const de: Record = { "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.", + "models.orderHint": "Die Standardreihenfolge folgt bevorzugten Modellen und der Katalogpriorität; geroutete Modelle mit gleichem Rang folgen Anbieter und Modell. Eine gespeicherte Reihenfolge kann die Anzeige ändern. Sichtbarkeitsschalter filtern die Zeilen. Clients können bis zum erneuten Öffnen einen älteren Katalog anzeigen.", "models.custom": "Benutzerdefiniert…", "models.customApply": "Anwenden", "models.customPlaceholder": "Tokens (z. B. 420000)", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 9cbf8699fd..0d2f1d05d4 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -6,6 +6,20 @@ * `{var}` are plain interpolations. */ export const en = { + "models.pickerOrder.label": "Picker order", + "models.pickerOrder.default": "Default", + "models.pickerOrder.alphabetical": "A–Z by model", + "models.pickerOrder.provider": "Group by provider", + "models.pickerOrder.mostUsed": "Most used snapshot", + "models.pickerOrder.custom": "Custom order", + "models.pickerOrder.apply": "Apply order", + "models.pickerOrder.applying": "Applying…", + "models.pickerOrder.saved": "Picker order saved. Reopen clients that still show the old catalog.", + "models.pickerOrder.pending": "Order saved; catalog refresh is pending.", + "models.pickerOrder.usageFailed": "Could not load model usage.", + "models.pickerOrder.loadFailed": "Could not load picker settings.", + "models.pickerOrder.retry": "Retry", + "models.pickerOrder.hint": "Saves routed order for Codex and Claude discovery. Featured/native bands stay in place; Most used is a snapshot. Native advertised choices may change.", "codexAuth.quotaAutoRefreshAllHint": "Controls the supported 5-hour and weekly windows for all current accounts together. In Pool mode, a small request is sent after each reset and uses quota.", "codexAuth.quotaAutoRefreshMixed": "Some windows are enabled.", "codexAuth.quotaAutoRefreshEmpty": "No supported quota windows. Refresh account quotas to check again.", @@ -585,7 +599,7 @@ export const en = { "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.", + "models.orderHint": "Default order follows featured selections and catalog priority, with provider/model ordering for tied routed rows. A saved picker order can override display order; visibility switches filter which rows appear. Clients may keep an older catalog until reopened.", "models.custom": "Custom…", "models.customApply": "Apply", "models.customPlaceholder": "Tokens (e.g. 420000)", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index ec171e627c..b6eb03f0b0 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -4,6 +4,20 @@ import type { TKey } from "./en"; * French i18n catalog. Must match the `TKey` set. */ export const fr: Record = { + "models.pickerOrder.label": "Ordre des modèles", + "models.pickerOrder.default": "Par défaut", + "models.pickerOrder.alphabetical": "A–Z par modèle", + "models.pickerOrder.provider": "Par fournisseur", + "models.pickerOrder.mostUsed": "Instantané des usages", + "models.pickerOrder.custom": "Ordre personnalisé", + "models.pickerOrder.apply": "Appliquer l’ordre", + "models.pickerOrder.applying": "Application…", + "models.pickerOrder.saved": "Ordre enregistré. Rouvrez les clients affichant encore l’ancien catalogue.", + "models.pickerOrder.pending": "Ordre enregistré ; actualisation du catalogue en attente.", + "models.pickerOrder.usageFailed": "Impossible de charger les usages.", + "models.pickerOrder.loadFailed": "Impossible de charger les réglages du sélecteur.", + "models.pickerOrder.retry": "Réessayer", + "models.pickerOrder.hint": "Enregistre l’ordre des modèles routés pour Codex et la découverte Claude. Les plages prioritaires et natives sont conservées. Les usages sont un instantané ; les choix annoncés nativement peuvent changer.", "codexAuth.quotaAutoRefreshAllHint": "Active ou désactive ensemble, pour tous les comptes actuels, les fenêtres de quota prises en charge par chaque compte : 5 heures et hebdomadaire. En mode Groupe, une petite requête consommant du quota est envoyée après chaque réinitialisation.", "codexAuth.quotaAutoRefreshMixed": "Certaines fenêtres sont activées.", "codexAuth.quotaAutoRefreshEmpty": "Aucune fenêtre de quota prise en charge. Actualisez les quotas des comptes.", @@ -570,7 +584,7 @@ export const fr: Record = { "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.", + "models.orderHint": "L’ordre par défaut suit les modèles prioritaires et la priorité du catalogue ; les modèles routés à égalité suivent le fournisseur puis le modèle. Un ordre enregistré peut modifier l’affichage. Les options de visibilité filtrent les lignes. Les clients peuvent conserver un ancien catalogue jusqu’à leur réouverture.", "models.custom": "Personnalisé…", "models.customApply": "Appliquer", "models.customPlaceholder": "Jetons (p. ex. 420000)", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index c71bd7a045..4b16912324 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -4,6 +4,20 @@ import type { TKey } from "./en"; * Japanese i18n catalog; must match the `TKey` set (compile-checked). */ export const ja: Record = { + "models.pickerOrder.label": "モデル選択順", + "models.pickerOrder.default": "デフォルト", + "models.pickerOrder.alphabetical": "モデル名のA–Z順", + "models.pickerOrder.provider": "プロバイダー別", + "models.pickerOrder.mostUsed": "使用量のスナップショット", + "models.pickerOrder.custom": "カスタム順", + "models.pickerOrder.apply": "順序を適用", + "models.pickerOrder.applying": "適用中…", + "models.pickerOrder.saved": "選択順を保存しました。古い一覧が表示される場合はクライアントを開き直してください。", + "models.pickerOrder.pending": "順序を保存しました。カタログの更新は保留中です。", + "models.pickerOrder.usageFailed": "モデル使用量を読み込めませんでした。", + "models.pickerOrder.loadFailed": "モデル選択設定を読み込めませんでした。", + "models.pickerOrder.retry": "再試行", + "models.pickerOrder.hint": "CodexとClaudeの検出一覧のルーティングモデル順を保存します。優先・ネイティブの順位帯は維持されます。使用量順はスナップショットで、ネイティブツールの候補表示は変わる場合があります。", "codexAuth.quotaAutoRefreshAllHint": "現在の全アカウントで、対応する5時間・週間枠をまとめて切り替えます。プールモードではリセット後に少量の利用枠を消費するリクエストを送信します。", "codexAuth.quotaAutoRefreshMixed": "一部の枠が有効です。", "codexAuth.quotaAutoRefreshEmpty": "対応する利用枠がありません。アカウントの利用枠を更新してください。", @@ -568,7 +582,7 @@ export const ja: Record = { "models.setAllHint": "すべてのルーティング済みプロバイダーに {value} のデフォルトウィンドウをオンにします。中継が context_window / context_length を返さない場合、この値が実際の Codex ウィンドウになります。1 モデルだけ手で書くときは同じ行の「カスタムウィンドウ」を使います。ネイティブプロバイダーには影響しません。", "models.collapseAll": "すべて折りたたむ", "models.expandAll": "すべて展開", - "models.orderHint": "ピッカーの順序: サブエージェントの選択(選択順) → 残りのルーティングモデルはプロバイダー別、次にモデル ID 別のアルファベット順 → ネイティブモデル。表示切り替えはモデルをフィルタするだけで、この順序は変更しません。", + "models.orderHint": "デフォルト順は優先モデルとカタログの優先順位に従い、同順位のルーティングモデルはプロバイダー・モデル順になります。保存した順序で表示順を変更でき、表示スイッチは表示する行を絞り込みます。クライアントを開き直すまで古いカタログが表示される場合があります。", "models.custom": "カスタム…", "models.customApply": "適用", "models.customPlaceholder": "トークン (例: 420000)", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 63ac304426..a87bf608e5 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -4,6 +4,20 @@ import type { TKey } from "./en"; * Korean i18n catalog; must match the `TKey` set (compile-checked). */ export const ko: Record = { + "models.pickerOrder.label": "모델 선택 순서", + "models.pickerOrder.default": "기본값", + "models.pickerOrder.alphabetical": "모델 이름순", + "models.pickerOrder.provider": "프로바이더별", + "models.pickerOrder.mostUsed": "사용량순 스냅샷", + "models.pickerOrder.custom": "사용자 지정 순서", + "models.pickerOrder.apply": "순서 적용", + "models.pickerOrder.applying": "적용 중…", + "models.pickerOrder.saved": "모델 선택 순서를 저장했습니다. 이전 목록이 보이면 클라이언트를 다시 열어 주세요.", + "models.pickerOrder.pending": "순서를 저장했습니다. 카탈로그 갱신은 아직 완료되지 않았습니다.", + "models.pickerOrder.usageFailed": "모델 사용량을 불러오지 못했습니다.", + "models.pickerOrder.loadFailed": "모델 선택 설정을 불러오지 못했습니다.", + "models.pickerOrder.retry": "다시 시도", + "models.pickerOrder.hint": "Codex·Claude 검색 목록의 라우팅 모델 순서를 저장합니다. 지정 모델·네이티브 모델의 우선순위 구간은 유지됩니다. 사용량순은 스냅샷이며, 네이티브 도구에 표시되는 후보는 달라질 수 있습니다.", "codexAuth.quotaAutoRefreshAllHint": "현재 등록된 모든 계정의 5시간·주간 할당량을 한 번에 켜거나 끕니다. 지원하는 창에만 적용하며, 풀 모드에서 리셋 후 소량의 할당량을 쓰는 요청을 보냅니다.", "codexAuth.quotaAutoRefreshMixed": "일부만 켜져 있습니다.", "codexAuth.quotaAutoRefreshEmpty": "지원하는 할당량 창이 없습니다. 계정 할당량을 새로고침해 주세요.", @@ -571,7 +585,7 @@ export const ko: Record = { "models.setAllHint": "라우팅된 모든 프로바이더에 {value} 기본 창을 켭니다. 중계가 context_window / context_length 를 주지 않으면 이 값이 실제 Codex 창이 됩니다. 모델 하나만 손으로 쓰려면 같은 줄의 「사용자 지정 창」을 쓰세요. 네이티브 프로바이더는 영향을 받지 않습니다.", "models.collapseAll": "모두 접기", "models.expandAll": "모두 펼치기", - "models.orderHint": "피커 순서: Subagents에서 지정한 순서 → 나머지 라우팅 모델(프로바이더, 모델 ID 순 알파벳 정렬) → 네이티브 모델. 노출 토글은 모델을 필터링할 뿐 이 순서를 바꾸지 않습니다.", + "models.orderHint": "기본 순서는 지정 모델과 카탈로그 우선순위를 따르며, 우선순위가 같은 라우팅 모델은 프로바이더·모델 순으로 정렬됩니다. 저장된 피커 순서는 표시 순서를 바꿀 수 있고, 노출 토글은 표시할 행을 필터링합니다. 클라이언트를 다시 열기 전까지 이전 카탈로그가 보일 수 있습니다.", "models.custom": "직접 입력…", "models.customApply": "적용", "models.customPlaceholder": "토큰 (예: 420000)", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 9f220ba2b1..70eb364002 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -4,6 +4,20 @@ import type { TKey } from "./en"; * Russian i18n catalog; must match the `TKey` set (compile-checked). */ export const ru: Record = { + "models.pickerOrder.label": "Порядок моделей", + "models.pickerOrder.default": "По умолчанию", + "models.pickerOrder.alphabetical": "По имени A–Z", + "models.pickerOrder.provider": "По провайдеру", + "models.pickerOrder.mostUsed": "Снимок использования", + "models.pickerOrder.custom": "Свой порядок", + "models.pickerOrder.apply": "Применить порядок", + "models.pickerOrder.applying": "Применение…", + "models.pickerOrder.saved": "Порядок сохранён. Перезапустите клиенты, показывающие старый каталог.", + "models.pickerOrder.pending": "Порядок сохранён; обновление каталога ожидается.", + "models.pickerOrder.usageFailed": "Не удалось загрузить статистику моделей.", + "models.pickerOrder.loadFailed": "Не удалось загрузить настройки выбора.", + "models.pickerOrder.retry": "Повторить", + "models.pickerOrder.hint": "Сохраняет порядок маршрутизируемых моделей для Codex и обнаружения Claude. Диапазоны приоритетных и нативных моделей сохраняются. Использование — снимок; нативно объявляемые варианты могут измениться.", "codexAuth.quotaAutoRefreshAllHint": "Общее переключение поддерживаемых 5-часовых и недельных окон всех текущих аккаунтов. В режиме пула после сброса отправляется небольшой запрос, расходующий квоту.", "codexAuth.quotaAutoRefreshMixed": "Включены некоторые окна.", "codexAuth.quotaAutoRefreshEmpty": "Нет поддерживаемых окон квоты. Обновите квоты аккаунтов.", @@ -573,7 +587,7 @@ export const ru: Record = { "models.setAllHint": "Включает окно по умолчанию {value} для всех маршрутизируемых провайдеров. Если релей не отдаёт context_window / context_length, это значение становится реальным окном Codex. Чтобы задать одну модель вручную, используйте «Пользовательские окна» в той же строке. Нативные провайдеры не затрагиваются.", "models.collapseAll": "Свернуть все", "models.expandAll": "Развернуть все", - "models.orderHint": "Порядок в селекторе: модели, выбранные на странице «Подагенты» (в заданном порядке) → остальные маршрутизируемые модели по алфавиту — сначала по провайдеру, затем по ID модели → нативные модели. Переключатели видимости лишь фильтруют модели и не меняют этот порядок.", + "models.orderHint": "Порядок по умолчанию следует избранным моделям и приоритету каталога; равные по рангу маршрутизируемые строки сортируются по провайдеру и модели. Сохранённый порядок может менять отображение, а переключатели видимости фильтруют строки. До повторного открытия клиент может показывать старый каталог.", "models.custom": "Другое…", "models.customApply": "Применить", "models.customPlaceholder": "Токены (напр. 420000)", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index aee152cd39..ca233f452e 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -5,6 +5,20 @@ import type { TKey } from "./en"; * Turkish i18n catalog. Must match the `TKey` set (compile-checked). */ export const tr: Record = { + "models.pickerOrder.label": "Model sırası", + "models.pickerOrder.default": "Varsayılan", + "models.pickerOrder.alphabetical": "Model adına göre A–Z", + "models.pickerOrder.provider": "Sağlayıcıya göre", + "models.pickerOrder.mostUsed": "Kullanım anlık görüntüsü", + "models.pickerOrder.custom": "Özel sıra", + "models.pickerOrder.apply": "Sırayı uygula", + "models.pickerOrder.applying": "Uygulanıyor…", + "models.pickerOrder.saved": "Model sırası kaydedildi. Eski kataloğu gösteren istemcileri yeniden açın.", + "models.pickerOrder.pending": "Sıra kaydedildi; katalog yenilemesi bekleniyor.", + "models.pickerOrder.usageFailed": "Model kullanımı yüklenemedi.", + "models.pickerOrder.loadFailed": "Seçici ayarları yüklenemedi.", + "models.pickerOrder.retry": "Yeniden dene", + "models.pickerOrder.hint": "Codex ve Claude keşfi için yönlendirilen model sırasını kaydeder. Öne çıkan/yerel öncelik aralıkları korunur. Kullanım bir anlık görüntüdür; yerel araçta sunulan seçenekler değişebilir.", "codexAuth.quotaAutoRefreshAllHint": "Mevcut tüm hesapların desteklenen 5 saatlik ve haftalık pencerelerini birlikte açıp kapatır. Havuz modunda her sıfırlamadan sonra az miktarda kota kullanan bir istek gönderilir.", "codexAuth.quotaAutoRefreshMixed": "Bazı pencereler etkin.", "codexAuth.quotaAutoRefreshEmpty": "Desteklenen kota penceresi yok. Hesap kotalarını yenileyin.", @@ -576,7 +590,7 @@ export const tr: Record = { "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.", + "models.orderHint": "Varsayılan sıra öne çıkan seçimleri ve katalog önceliğini izler; eşit öncelikli yönlendirilmiş satırlar sağlayıcı ve modele göre sıralanır. Kaydedilmiş sıra görünümü değiştirebilir; görünürlük anahtarları satırları filtreler. İstemci yeniden açılana kadar eski kataloğu gösterebilir.", "models.custom": "Özel…", "models.customApply": "Uygula", "models.customPlaceholder": "Jetonlar (örn. 420000)", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 39c9e2f0b3..2b7e6ac6ba 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -2,6 +2,20 @@ import type { TKey } from "./en"; /** Traditional Chinese (Taiwan) UI strings — keys must match `en.ts` 1:1. */ export const zhTW: Record = { + "models.pickerOrder.label": "模型選擇順序", + "models.pickerOrder.default": "預設", + "models.pickerOrder.alphabetical": "依模型名稱 A–Z", + "models.pickerOrder.provider": "依供應商分組", + "models.pickerOrder.mostUsed": "使用量快照", + "models.pickerOrder.custom": "自訂順序", + "models.pickerOrder.apply": "套用順序", + "models.pickerOrder.applying": "正在套用…", + "models.pickerOrder.saved": "選擇順序已儲存。若仍顯示舊目錄,請重新開啟用戶端。", + "models.pickerOrder.pending": "順序已儲存,目錄更新尚未完成。", + "models.pickerOrder.usageFailed": "無法載入模型使用量。", + "models.pickerOrder.loadFailed": "無法載入模型選擇設定。", + "models.pickerOrder.retry": "重試", + "models.pickerOrder.hint": "儲存 Codex 與 Claude 探索清單中的路由模型順序。保留精選與原生模型的優先級區間;使用量排序是快照,原生工具顯示的候選可能改變。", "codexAuth.quotaAutoRefreshAllHint": "統一切換目前所有帳戶各自支援的 5 小時與每週額度視窗。在帳戶池模式下,重設後會傳送消耗少量額度的請求。", "codexAuth.quotaAutoRefreshMixed": "部分視窗已啟用。", "codexAuth.quotaAutoRefreshEmpty": "沒有支援的額度視窗。請重新整理帳戶額度。", @@ -439,7 +453,7 @@ export const zhTW: Record = { "models.setAllHint": "為所有已路由供應商打開 {value} 預設視窗。中繼站沒回報 context_window / context_length 時,這個值就是 Codex 實際視窗。要幫單一模型手寫,用同一列上的「自訂視窗」。原生供應商不受影響。", "models.collapseAll": "全部摺疊", "models.expandAll": "全部展開", - "models.orderHint": "選擇器順序:Subagents 中的選擇(按所選順序)→ 其餘已路由模型(依次按供應商、模型 ID 字母排序)→ 原生模型。可見性開關僅用於篩選,不會改變此順序。", + "models.orderHint": "預設順序遵循精選項目與目錄優先級;同優先級的路由列依供應商、模型排序。儲存的選擇順序可改變顯示順序,可見性開關用於篩選顯示的列。用戶端重新開啟前可能仍顯示舊目錄。", "models.custom": "自訂…", "models.customApply": "套用", "models.customPlaceholder": "tokens (例如 420000)", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 1ba4cabfa8..42ac3941d4 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -4,6 +4,20 @@ import type { TKey } from "./en"; * Chinese i18n catalog; must match the `TKey` set (compile-checked). */ export const zh: Record = { + "models.pickerOrder.label": "模型选择顺序", + "models.pickerOrder.default": "默认", + "models.pickerOrder.alphabetical": "按模型名 A–Z", + "models.pickerOrder.provider": "按提供商分组", + "models.pickerOrder.mostUsed": "使用量快照", + "models.pickerOrder.custom": "自定义顺序", + "models.pickerOrder.apply": "应用顺序", + "models.pickerOrder.applying": "正在应用…", + "models.pickerOrder.saved": "选择顺序已保存。若仍显示旧目录,请重新打开客户端。", + "models.pickerOrder.pending": "顺序已保存,目录刷新尚未完成。", + "models.pickerOrder.usageFailed": "无法加载模型使用量。", + "models.pickerOrder.loadFailed": "无法加载模型选择设置。", + "models.pickerOrder.retry": "重试", + "models.pickerOrder.hint": "保存 Codex 和 Claude 发现列表中的路由模型顺序。保留精选与原生模型的优先级区间;使用量排序是快照,原生工具显示的候选可能变化。", "codexAuth.quotaAutoRefreshAllHint": "统一开关当前所有账户各自支持的 5 小时和每周额度窗口。在账户池模式下,重置后会发送消耗少量额度的请求。", "codexAuth.quotaAutoRefreshMixed": "部分窗口已启用。", "codexAuth.quotaAutoRefreshEmpty": "没有支持的额度窗口。请刷新账户额度。", @@ -568,7 +582,7 @@ export const zh: Record = { "models.setAllHint": "给所有已路由提供方打开 {value} 默认窗口。中转站没报 context_window / context_length 时,这个值就是 Codex 实际窗口。要给单个模型手写,用同一行上的「自定义窗口」。原生提供方不受影响。", "models.collapseAll": "全部折叠", "models.expandAll": "全部展开", - "models.orderHint": "选择器顺序:Subagents 中的选择(按所选顺序)→ 其余已路由模型(依次按提供方、模型 ID 字母排序)→ 原生模型。可见性开关仅用于筛选,不会改变此顺序。", + "models.orderHint": "默认顺序遵循精选项和目录优先级;同优先级的路由行按提供商、模型排序。保存的选择顺序可改变显示顺序,可见性开关用于筛选显示的行。客户端重新打开前可能仍显示旧目录。", "models.custom": "自定义…", "models.customApply": "应用", "models.customPlaceholder": "令牌 (例如 420000)", diff --git a/gui/src/model-picker-order.ts b/gui/src/model-picker-order.ts new file mode 100644 index 0000000000..dfa5073b08 --- /dev/null +++ b/gui/src/model-picker-order.ts @@ -0,0 +1,106 @@ +export type ModelPickerOrderMode = "default" | "alphabetical" | "provider" | "most-used" | "custom"; +export type SavedModelPickerOrderMode = Exclude; +export interface ModelPickerUsage { + provider: string; + model: string; + resolvedModel?: string; + requests: number; +} +export interface PickerModelIdentity { provider: string; id: string; namespaced: string } +export interface PickerOrderSaved { + pickerOrder: string[]; + pickerOrderMode: SavedModelPickerOrderMode | null; +} +export interface PickerOrderSettings extends PickerOrderSaved { pickerAvailable: string[] } + +function stringList(value: unknown): value is string[] { + return Array.isArray(value) && value.every(id => typeof id === "string" && id.trim().length > 0); +} +function savedMode(value: unknown): value is SavedModelPickerOrderMode | null { + return value === null || value === "alphabetical" || value === "provider" || value === "most-used"; +} +export function isPickerOrderSaved(value: unknown): value is PickerOrderSaved { + if (value === null || typeof value !== "object") return false; + const row = value as Record; + return stringList(row.pickerOrder) && savedMode(row.pickerOrderMode); +} +export function isPickerOrderSettings(value: unknown): value is PickerOrderSettings { + return isPickerOrderSaved(value) && stringList((value as PickerOrderSettings).pickerAvailable); +} +export function isModelPickerUsage(value: unknown): value is ModelPickerUsage[] { + return Array.isArray(value) && value.every(row => row !== null && typeof row === "object" + && typeof row.provider === "string" && typeof row.model === "string" + && (row.resolvedModel === undefined || typeof row.resolvedModel === "string") + && typeof row.requests === "number" && Number.isFinite(row.requests) && row.requests >= 0); +} +function parts(slug: string): [string, string] { + const slash = slug.indexOf("/"); + return slash < 0 ? ["", slug] : [slug.slice(0, slash), slug.slice(slash + 1)]; +} +// Fixed locale makes snapshots independent of the user's display language/OS locale. +const compare = (a: string, b: string) => a.localeCompare(b, "en"); +function byProvider(a: string, b: string): number { + const [ap, am] = parts(a), [bp, bm] = parts(b); + return compare(ap, bp) || compare(am, bm); +} + +export function modelPickerOrder( + mode: Exclude, + models: readonly string[], + usage: readonly ModelPickerUsage[] = [], + identities: readonly PickerModelIdentity[] = [], +): string[] | null { + if (mode === "default") return null; + const unique = [...new Set(models)]; + if (mode === "alphabetical") return unique.sort((a, b) => compare(parts(a)[1], parts(b)[1]) || byProvider(a, b)); + if (mode === "provider") return unique.sort(byProvider); + const candidates = new Set(unique); + const raw = new Map>(); + const owners = new Map>(); + for (const row of identities) { + if (!candidates.has(row.namespaced)) continue; + const key = JSON.stringify([row.provider, row.id]); + const values = raw.get(key) ?? new Set(); + values.add(row.namespaced); + raw.set(key, values); + const sources = owners.get(row.namespaced) ?? new Set(); + sources.add(key); + owners.set(row.namespaced, sources); + } + const unambiguous = (slug: string): string | null => (owners.get(slug)?.size ?? 0) > 1 ? null : slug; + const resolve = (provider: string, id: string): string | null | undefined => { + const exact = raw.get(JSON.stringify([provider, id])); + if (exact) return exact.size === 1 ? unambiguous([...exact][0]!) : null; + // A raw upstream slash is not a namespace. Use the observed identity table above; + // only fall back to an exact same-provider catalog id or an ordinary bare model id. + if (id.startsWith(`${provider}/`) && candidates.has(id)) return unambiguous(id); + const slug = `${provider}/${id}`; + return !id.includes("/") && candidates.has(slug) ? unambiguous(slug) : undefined; + }; + const counts = new Map(); + for (const row of usage) { + // Summary buckets are keyed by requested model. resolvedModel is only a + // representative observation, not proof that every request used that target. + const target = resolve(row.provider, row.model); + if (target) counts.set(target, (counts.get(target) ?? 0) + row.requests); + } + return unique.sort((a, b) => (counts.get(b) ?? 0) - (counts.get(a) ?? 0) || byProvider(a, b)); +} + +export function modelPickerOrderMode( + models: readonly string[], saved: readonly string[], mode?: SavedModelPickerOrderMode | null, +): ModelPickerOrderMode { + if (saved.length === 0) return "default"; + // Existing complete/native orders are never silently replaced by a routed preset. + if (saved.some(id => !id.includes("/"))) return "custom"; + if (mode === "alphabetical" || mode === "provider" || mode === "most-used") return mode; + const candidates = new Set(models); + if (new Set(saved).size !== saved.length || saved.length !== candidates.size + || saved.some(id => !candidates.has(id))) return "custom"; + for (const preset of ["alphabetical", "provider"] as const) { + const expected = modelPickerOrder(preset, models); + if (expected !== null && expected.length === saved.length + && expected.every((id, index) => id === saved[index])) return preset; + } + return "custom"; +} diff --git a/gui/src/pages/Logs.tsx b/gui/src/pages/Logs.tsx index 2632ae8788..03a0a45005 100644 --- a/gui/src/pages/Logs.tsx +++ b/gui/src/pages/Logs.tsx @@ -27,6 +27,7 @@ import { sanitizeLogEntryRouteDecision, validCachedRouteDecision, } from "./log-route-decision"; +import { mergeLogDelta, parseLogPollResponse } from "./log-poll"; function logsCacheKey(apiBase: string): string { return `ocx.logs.list.v1:${apiBase}`; @@ -385,11 +386,16 @@ export default function Logs({ apiBase }: { apiBase: string }) { const filterClockRef = useRef<{ key: string; anchor?: LogsClockAnchor; active: boolean; request: number; }>({ key: resourceKey, active: false, request: 0 }); + const logPollRef = useRef<{ key: string; cursor: string | null; rows: LogEntry[] }>( + { key: resourceKey, cursor: null, rows: [] }, + ); // Invalidate the old resource at commit, before passive resource-loader effects. // A late body read must not mutate this page's clock, cache or retry state. useLayoutEffect(() => { const clock = { key: resourceKey, active: true, request: 0 }; filterClockRef.current = clock; + // Cached display rows never establish a cursor, including A -> B -> A. + logPollRef.current = { key: resourceKey, cursor: null, rows: [] }; setFilterClockNow(Date.now()); return () => { clock.active = false; }; }, [resourceKey]); @@ -462,16 +468,22 @@ export default function Logs({ apiBase }: { apiBase: string }) { logRetryRef.current = retry; } if (retry.failures > 0 && Date.now() < retry.nextAttemptAt) throw retry.error; + const poll = logPollRef.current; + const cursor = poll.key === resourceKey ? poll.cursor : null; + const url = `${apiBase}/api/logs?limit=2000${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`; try { - const res = await fetch(`${apiBase}/api/logs?limit=2000`, { signal }); + const res = await fetch(url, { signal }); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`.trim()); - const body = await res.json() as LogEntry[] | { logs?: LogEntry[]; generatedAt?: unknown }; + const body: unknown = await res.json(); const receivedAt = performance.now(); - const raw = Array.isArray(body) ? body : (body.logs ?? []); - const next = raw.map(sanitizeLogEntryRouteDecision); + const parsed = parseLogPollResponse(body); + const incoming = parsed.rows.map(sanitizeLogEntryRouteDecision); + const next = cursor && parsed.cursor && !parsed.reset + ? mergeLogDelta(poll.rows, incoming) : incoming; // The resource-store generation guard runs only after this loader returns. // Guard these local side effects here as fetch/body readers may ignore abort. if (!isCurrent()) throw signal.reason ?? new DOMException("Obsolete log request", "AbortError"); + logPollRef.current = { key: resourceKey, cursor: parsed.cursor, rows: next }; // Reconcile when the accepted snapshot changes, using the latest user state // rather than filters captured when the request started. Persist disappearance // as All so a later ring cannot resurrect a cleared selection. @@ -488,7 +500,7 @@ export default function Logs({ apiBase }: { apiBase: string }) { if (previous.model === nextModel && previous.provider === nextProvider) return previous; return { ...previous, model: nextModel, provider: nextProvider }; }); - const sample = logsClockAnchor(Array.isArray(body) ? undefined : body.generatedAt, receivedAt); + const sample = logsClockAnchor(parsed.generatedAt, receivedAt); if (sample) clock.anchor = sample; setFilterClockNow(logsClockNow(clock.anchor, receivedAt, Date.now())); logRetryRef.current = { key: resourceKey, failures: 0, nextAttemptAt: 0, error: null }; @@ -525,6 +537,7 @@ export default function Logs({ apiBase }: { apiBase: string }) { const fetchLogs = logsResource.refresh; const retryLogs = useCallback(() => { logRetryRef.current = { key: resourceKey, failures: 0, nextAttemptAt: 0, error: null }; + logPollRef.current = { key: resourceKey, cursor: null, rows: [] }; fetchLogs({ forceLoading: true }); }, [fetchLogs, resourceKey]); diff --git a/gui/src/pages/Models.tsx b/gui/src/pages/Models.tsx index a78a57e6a8..22ea51bc20 100644 --- a/gui/src/pages/Models.tsx +++ b/gui/src/pages/Models.tsx @@ -13,7 +13,11 @@ import { readJsonIfOk, readJsonOrThrow } from "../fetch-json"; import { describeIntegrationRefusalParts } from "./integrations/refusal-copy"; import { readSessionListCache, writeSessionListCache } from "../session-list-cache"; import { setClientResourceData } from "../client-resource"; -import { createBoundedFetch } from "../bounded-fetch"; +import { createBoundedFetch, type BoundedFetch } from "../bounded-fetch"; +import { + isModelPickerUsage, isPickerOrderSaved, isPickerOrderSettings, modelPickerOrder, modelPickerOrderMode, + type ModelPickerOrderMode, type PickerOrderSettings, type ModelPickerUsage, +} from "../model-picker-order"; import { startVisibilityPoll } from "../visibility-poll"; import { useDataSurface } from "../data-surface"; import { DataSurfaceSkeleton } from "../components/data-surface"; @@ -142,26 +146,48 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; return () => { appServerMounted.current = false; }; }, []); - const reloadAppServerState = useCallback((signal?: AbortSignal) => { - void fetchCodexAppServerState(apiBase, { signal }).then(outcome => { - if (signal?.aborted || !appServerMounted.current) return; + const appServerRead = useRef(null); + const appServerReadGeneration = useRef(0); + const appServerReadBase = useRef(apiBase); + const cancelAppServerRead = useCallback(() => { + appServerReadGeneration.current++; + appServerRead.current?.controller.abort(); + appServerRead.current?.clear(); + appServerRead.current = null; + }, []); + const reloadAppServerState = useCallback(async () => { + // An old restart callback must not start an A read after the page moved to B. + if (!appServerMounted.current || appServerReadBase.current !== apiBase) return; + cancelAppServerRead(); + const generation = appServerReadGeneration.current; + const bounded = createBoundedFetch(15_000); + appServerRead.current = bounded; + try { + const outcome = await fetchCodexAppServerState(apiBase, { signal: bounded.signal }); + if (bounded.signal.aborted || !appServerMounted.current + || appServerReadBase.current !== apiBase || generation !== appServerReadGeneration.current + || appServerRead.current !== bounded) return; setAppServerState(outcome.state); - }); - }, [apiBase]); + } finally { + // The observation owns its deadline until settlement, independently of PUT. + bounded.clear(); + if (appServerRead.current === bounded) appServerRead.current = null; + } + }, [apiBase, cancelAppServerRead]); // onSettled, not a per-button callback: the sidebar control knows nothing about // this page, and a restart succeeding there must still clear the banner here. const { restarting: codexRestarting, restart: handleCodexRestart } = useCodexRestart(apiBase, { - onSettled: () => reloadAppServerState(), + onSettled: () => { void reloadAppServerState(); }, }); useEffect(() => { - // Once on mount, on apiBase change, and when a restart settles anywhere in the - // app (restartEpoch) — never a timer. - const controller = new AbortController(); - reloadAppServerState(controller.signal); - return () => controller.abort(); - }, [reloadAppServerState, restartEpoch]); + // Once on mount/base change or restart completion, never on a timer. + appServerReadBase.current = apiBase; + setAppServerState(null); + void reloadAppServerState(); + return cancelAppServerRead; + }, [apiBase, cancelAppServerRead, reloadAppServerState, restartEpoch]); @@ -217,6 +243,40 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; const [contextCaps, setContextCaps] = useState>(() => cached?.contextCaps ?? {}); const [contextCapValues, setContextCapValues] = useState>(() => cached?.contextCapValues ?? {}); const [contextCapValue, setContextCapValue] = useState(() => cached?.contextCapValue ?? 350_000); + const pickerCacheKey = `${cacheKey}:picker-order`; + const cachedPicker = useMemo(() => { + const value = readSessionListCache(pickerCacheKey); + return isPickerOrderSettings(value) ? value : undefined; + }, [pickerCacheKey]); + const [pickerDraft, setPickerDraft] = useState(null); + const [pickerBusy, setPickerBusy] = useState(false); + const pickerFlight = useRef(null); + const pickerResource = useDataSurface( + pickerCacheKey, [apiBase], + useCallback(async (signal: AbortSignal) => { + const response = await fetch(`${apiBase}/api/subagent-models`, { signal }); + const data = await readJsonOrThrow(response); + if (!isPickerOrderSettings(data)) throw new Error("picker settings payload missing"); + if (signal.aborted) throw new Error("picker settings request aborted"); + writeSessionListCache(pickerCacheKey, data); + return data; + }, [apiBase, pickerCacheKey]), + { isEmpty: () => false, enabled: catalogActive, deadlineMs: 15_000, initialData: cachedPicker }, + ); + const pickerSettings = pickerResource.state.data; + const pickerMode = pickerDraft ?? modelPickerOrderMode( + pickerSettings?.pickerAvailable ?? [], pickerSettings?.pickerOrder ?? [], pickerSettings?.pickerOrderMode, + ); + useEffect(() => { + setPickerDraft(null); + setPickerBusy(false); + return () => { + pickerFlight.current?.controller.abort(); + pickerFlight.current?.clear(); + pickerFlight.current = null; + cancelAppServerRead(); + }; + }, [apiBase, catalogActive, cancelAppServerRead]); const [customCap, setCustomCap] = useState(""); const [showCustom, setShowCustom] = useState(false); const [providerCapCustomOpen, setProviderCapCustomOpen] = useState>({}); @@ -486,6 +546,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; // Follow-up mutation refreshes retain their existing awaitable contract while publishing // the result through the same shared store used by the initial catalog subscription. setClientResourceData(cacheKey, next); + pickerResource.refresh(); return true; } catch { return false; @@ -494,7 +555,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; loadPendingRef.current = false; } } - }, [applyCatalog, cacheKey, fetchCatalog]); + }, [applyCatalog, cacheKey, fetchCatalog, pickerResource.refresh]); // Shadow/v2 controls must not wait on the models catalog (live discovery can be slow). useEffect(() => { @@ -1593,6 +1654,52 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; ? groups.filter(group => group.provider === selectedProvider) : groups; + const savePickerOrder = async () => { + if (pickerFlight.current || !pickerSettings || pickerResource.state.showError || pickerMode === "custom") return; + const mode = pickerMode; + const available = pickerSettings.pickerAvailable; + const bounded = createBoundedFetch(15_000); + pickerFlight.current = bounded; + setPickerBusy(true); + try { + let usage: ModelPickerUsage[] = []; + if (mode === "most-used") { + const response = await fetch(`${apiBase}/api/usage?range=all&surface=all`, { signal: bounded.signal }); + const payload = await readJsonOrThrow<{ models?: unknown }>(response, t("models.pickerOrder.usageFailed")); + if (!isModelPickerUsage(payload?.models)) throw new Error(t("models.pickerOrder.usageFailed")); + usage = payload.models; + } + const order = modelPickerOrder(mode, available, usage, models); + const response = await fetch(`${apiBase}/api/subagent-models`, { + method: "PUT", headers: { "Content-Type": "application/json" }, signal: bounded.signal, + body: JSON.stringify({ pickerOrder: order, pickerOrderMode: mode === "default" ? null : mode }), + }); + const data = await readJsonOrThrow(response, t("models.saveFailed")); + if (!isPickerOrderSaved(data) || !("ok" in data) || data.ok !== true) throw new Error(t("models.saveFailed")); + if (bounded.signal.aborted || pickerFlight.current !== bounded) return; + const next = { ...pickerSettings, pickerOrder: data.pickerOrder, pickerOrderMode: data.pickerOrderMode }; + // This aborts an older GET and advances the shared resource generation. + setClientResourceData(pickerCacheKey, next); + writeSessionListCache(pickerCacheKey, next); + setPickerDraft(null); + + const refresh = "catalogRefresh" in data ? data.catalogRefresh : undefined; + const converged = refresh !== null && typeof refresh === "object" + && "status" in refresh && refresh.status === "committed" + && "degraded" in refresh && refresh.degraded === false; + publishFeedback(converged, t(converged ? "models.pickerOrder.saved" : "models.pickerOrder.pending")); + // Durable save is already accepted. Observational failure must not undo it. + void reloadAppServerState(); + } catch (error) { + if (pickerFlight.current === bounded) { + publishFeedback(false, error instanceof Error ? error.message : t("models.networkError")); + } + } finally { + bounded.clear(); + if (pickerFlight.current === bounded) { pickerFlight.current = null; setPickerBusy(false); } + } + }; + const controlsBlock = ( <>
@@ -1752,6 +1859,35 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; {t("models.setAllHint", { value: fmtK(contextCapValue) })}
+
+ {t("models.pickerOrder.label")} +