diff --git a/docs-site/src/content/docs/fr/guides/grok-build.md b/docs-site/src/content/docs/fr/guides/grok-build.md index 69f4e056c0..4043bf870d 100644 --- a/docs-site/src/content/docs/fr/guides/grok-build.md +++ b/docs-site/src/content/docs/fr/guides/grok-build.md @@ -21,7 +21,18 @@ base_url = "http://127.0.0.1:10100/v1" api_backend = "chat_completions" api_key = "opencodex-loopback" name = "OCX gpt-5.6-sol" -# ... one [model.ocx-*] table per visible model ... +extra_headers = { "x-opencodex-grok" = "1" } +context_window = 372000 +supports_reasoning_effort = true +reasoning_effort = "low" + +[[model.ocx-gpt-5-6-sol.reasoning_efforts]] +id = "low" +value = "low" +label = "Low" +description = "Quick, fast implementations" +default = true +# ... autres niveaux de ce modèle, puis une table [model.ocx-*] par modèle visible ... # <<< opencodex managed block <<< ``` @@ -33,7 +44,7 @@ name = "OCX gpt-5.6-sol" - **Supprimé à l’arrêt :** `ocx stop`, `ocx eject`, `ocx uninstall` et l’arrêt normal du démon hors service suppriment le bloc délimité et restaurent votre fichier octet pour octet. Sous un gestionnaire de service, le démontage passe par `ocx stop`/`ocx - uninstall` (les processus en mode service maintiennent intentionnellement le blocage lors des réapparitions). + uninstall` (les processus en mode service conservent intentionnellement le bloc lors des relancements). - **Les alias en conflit** déjà définis dans vos propres tables `[model.*]` sont respectés (opencodex ajoute un suffixe à ses propres entrées) ; un bloc délimité endommagé (marqueur de début sans marqueur de fin) refuse tout changement automatique et demande une réparation manuelle. @@ -51,15 +62,20 @@ grok -m ocx-anthropic-claude-opus-4-8 -p "hello" Les commandes `/effort` et `--effort` de Grok Build ne fonctionnent que pour les modèles dont l’entrée de catalogue annonce une échelle d’effort : la récupération de la liste des modèles lit la réponse brute de `GET /v1/models`, et les entrées doivent contenir `supports_reasoning_effort` ainsi que les choix du menu -`reasoning_efforts`. Pour les entrées de modèles routés, opencodex reflète les niveaux configurés pour le fournisseur +`reasoning_efforts`. Une projection compatible avec Grok de cette échelle est également écrite dans chaque table +`[model.*]` gérée, avec `supports_reasoning_effort`, la valeur par défaut `reasoning_effort` et les lignes +`[[model..reasoning_efforts]]`, afin que le menu soit présent lorsque Grok lit le modèle depuis +`config.toml`. Pour les entrées de modèles routés, opencodex reflète les niveaux configurés pour le fournisseur (`reasoningEfforts` / `modelReasoningEfforts`, et la valeur par défaut de -`modelDefaultReasoningEfforts`) dans cette réponse. Ces métadonnées décrivent l’échelle des modèles routés -configurée dans le proxy ; elles ne prétendent pas que le fournisseur prend nativement en charge ces niveaux. +`modelDefaultReasoningEfforts`). Ces métadonnées décrivent l’échelle des modèles routés configurée dans le proxy ; +elles ne prétendent pas que le fournisseur prend nativement en charge ces niveaux. Les adaptateurs peuvent émuler le raisonnement ou mapper les niveaux sur des champs propres au fournisseur. Les modèles routés qui possèdent une échelle configurée affichent le contrôle de l’effort dans Grok Build comme dans Codex. Ceux dont la liste de niveaux est vide n’affichent aucun contrôle d’effort, conformément au comportement de Codex. Les entrées GPT-5.6 natives sont distinctes : elles conservent et exposent leurs échelles de raisonnement -en amont fixes, et non les métadonnées configurées pour les modèles routés. +en amont fixes, et non les métadonnées configurées pour les modèles routés. Les niveaux Grok valides, notamment +`none` et `minimal`, sont conservés lorsqu’ils sont annoncés. Les niveaux non pris en charge ou en double, +notamment `ultra`, propre à Codex, sont omis du fichier afin que chaque option générée reste sélectionnable. Grok Build communique avec opencodex au moyen de Chat Completions et envoie `reasoning_effort` lorsque l’échelle est annoncée. Dans ce cas, le traducteur Chat Completions entrant définit par défaut le champ Responses @@ -74,26 +90,25 @@ Grok Build exige une clé API non vide pour les modèles personnalisés, même s injectées contiennent une valeur fictive (`opencodex-loopback`) ; opencodex ignore les clés d’admission pour les connexions de bouclage, de sorte qu’aucun véritable secret n’est utilisé. -**L’enregistrement automatique est réservé au bouclage.** Lorsque opencodex se lie à un hôte hors bouclage, y compris -les caractères génériques `0.0.0.0` et `::`, qui exposent chaque interface — les requêtes ont besoin de votre réel -jeton d’admission, et un bloc géré ne peut pas en transporter un en toute sécurité. Écrire le jeton littéral -mettez votre secret dans `~/.grok/config.toml` et écrasez tout ce que vous y avez défini lors du prochain -`ocx start`/`ensure`/`restart`. Donc opencodex n’écrit rien du tout dans ce cas (et supprime -tout bloc restant d'une liaison de bouclage précédente), et vous configurez les modèles vous-même -en dehors des marqueurs gérés, où rien de ce que opencodex fait ne peut les écraser. Voir -[Recette manuelle](#recette-manuelle-sans-enregistrement-automatique) pour le tableau exact et réglez les deux -`base_url` (un hôte réellement accessible à partir de l'endroit où vous exécutez `grok`) et `api_key` -(votre `OPENCODEX_API_AUTH_TOKEN`). - -Ne remplacez pas `api_key` par `env_key` ici. Sans `model_provider` défini, un `env_key` -qui ne parvient pas à résoudre n'arrête pas la demande — Grok passe à votre xAI session -et l'envoie à n'importe quel `base_url` nom d'entrée, ce qui pour un LAN déploiement est un -texte en clair HTTP point de terminaison qui n'est pas xAI. - -Le modèle injecté `api_key` se trouve en premier dans la chaîne d'informations d'identification de Grok pour ces modèles, -donc les tours contre opencodex n'ont pas besoin de connexion Grok supplémentaire. Gardez votre `grok login` / -`XAI_API_KEY` configuration pour les modèles Grok natifs et toutes les fonctionnalités de harnais qui contactent xAI -directement. +**L’enregistrement automatique est réservé au bouclage.** Lorsque opencodex écoute sur une adresse qui n’est pas +de bouclage — y compris les caractères génériques `0.0.0.0` et `::`, qui exposent toutes les interfaces — les +requêtes doivent présenter votre véritable jeton d’admission, qu’un bloc géré ne peut pas transporter en toute +sécurité. Inscrire ce jeton en clair stockerait votre secret dans `~/.grok/config.toml` et écraserait toute valeur +que vous y auriez définie lors du prochain `ocx start`/`ensure`/`restart`. Dans ce cas, opencodex n’écrit donc rien +(et supprime tout bloc laissé par une ancienne liaison de bouclage) ; vous configurez vous-même les modèles en +dehors des marqueurs gérés, où aucune opération opencodex ne peut les écraser. Consultez la +[recette manuelle](#recette-manuelle-sans-enregistrement-automatique) pour obtenir la table exacte, puis définissez +`base_url` (une adresse réellement accessible depuis l’endroit où vous exécutez `grok`) et `api_key` (votre +`OPENCODEX_API_AUTH_TOKEN`). + +Ne remplacez pas `api_key` par `env_key` ici. En l’absence de `model_provider`, un `env_key` qui ne peut pas être +résolu n’interrompt pas la requête : Grok utilise alors votre jeton de session xAI et l’envoie à l’adresse +`base_url` indiquée par l’entrée. Pour un déploiement sur le réseau local, cette adresse est un point de terminaison +HTTP en clair qui n’appartient pas à xAI. + +La valeur `api_key` injectée pour chaque modèle se trouve en tête de la chaîne d’identifiants de Grok. Les requêtes +adressées à opencodex ne nécessitent donc aucune connexion Grok supplémentaire. Conservez votre configuration +habituelle `grok login` / `XAI_API_KEY` pour les modèles Grok natifs et les fonctions qui contactent directement xAI. ## Recette manuelle (sans enregistrement automatique) @@ -144,9 +159,10 @@ l'identifiant `grok-4.5`. Les alias générés évitent entièrement les points prévisibles. Grok Build surveille `~/.grok/config.toml` et recharge la configuration lorsque la table `[model]` change réellement (temporisation d’environ une seconde, avec comparaison du contenu) ; un bloc actualisé atteint une session ouverte sans redémarrage. Pour confirmer ce que Grok a analysé, - run `grok inspect` : il répertorie les sources de configuration qu'il a chargées et avertit de tout champ qu'il a chargé - rejeté. Il n'imprime pas la liste des modèles résolus. Notez qu’une seule erreur TOML - invalide *l'intégralité* de la couche de configuration utilisateur, c'est pourquoi opencodex écrit le fichier - atomiquement - Grok ne voit jamais une configuration à moitié écrite. + exécutez `grok inspect` : il répertorie les sources de configuration chargées et signale les champs + rejetés. Il n'affiche pas la liste des modèles résolus. La version actuelle de Grok Build signale et + ignore les champs de modèle invalides tout en conservant le reste de l'entrée. Une erreur de syntaxe + TOML empêche toujours le chargement du fichier. opencodex écrit de manière atomique, de sorte que + Grok observe un document complet à chaque rechargement. - **Mises à jour du catalogue :** le bloc délimité reflète le catalogue au moment de l’injection. Après l’ajout de fournisseurs ou de modèles, exécutez `ocx ensure` (ou redémarrez le proxy) pour l’actualiser. diff --git a/docs-site/src/content/docs/guides/grok-build.md b/docs-site/src/content/docs/guides/grok-build.md index 75dfadf84e..992a036686 100644 --- a/docs-site/src/content/docs/guides/grok-build.md +++ b/docs-site/src/content/docs/guides/grok-build.md @@ -21,7 +21,18 @@ base_url = "http://127.0.0.1:10100/v1" api_backend = "chat_completions" api_key = "opencodex-loopback" name = "OCX gpt-5.6-sol" -# ... one [model.ocx-*] table per visible model ... +extra_headers = { "x-opencodex-grok" = "1" } +context_window = 372000 +supports_reasoning_effort = true +reasoning_effort = "low" + +[[model.ocx-gpt-5-6-sol.reasoning_efforts]] +id = "low" +value = "low" +label = "Low" +description = "Quick, fast implementations" +default = true +# ... remaining rungs for this model, then one [model.ocx-*] table per visible model ... # <<< opencodex managed block <<< ``` @@ -51,15 +62,22 @@ grok -m ocx-anthropic-claude-opus-4-8 -p "hello" Grok Build's `/effort` (and `--effort`) only works for models whose catalog entry advertises the ladder: its model list fetch reads the raw `GET /v1/models` response, and entries there must carry `supports_reasoning_effort` plus `reasoning_efforts` menu -options. For routed model entries, opencodex mirrors the configured provider tiers -(`reasoningEfforts` / `modelReasoningEfforts`, and the default from -`modelDefaultReasoningEfforts`) onto that response. This metadata describes the -proxy-configured routed ladder — it does not claim native upstream reasoning support, -and adapters may emulate reasoning or map levels onto provider-specific fields. Routed -models with a configured ladder show the effort control in Grok Build just like they do -in Codex. Models with an empty tier list keep no effort control, matching Codex -behavior. Native GPT-5.6 entries are separate: they preserve and expose their pinned -upstream reasoning ladders rather than provider-configured routed metadata. +options. A Grok-compatible projection of that ladder is written into each managed +`[model.*]` table +(`supports_reasoning_effort`, default `reasoning_effort`, and +`[[model..reasoning_efforts]]` picker rows) so the menu is present when Grok +reads the model from `config.toml`. For routed model entries, opencodex mirrors the +configured provider tiers (`reasoningEfforts` / `modelReasoningEfforts`, and the default +from `modelDefaultReasoningEfforts`). This metadata describes the proxy-configured +routed ladder. Adapters may emulate reasoning or map levels onto provider-specific +fields. Routed models with a configured ladder show the effort control in Grok Build +just like they do in Codex. +Models with an empty tier list keep no effort control, matching Codex behavior. Native +GPT-5.6 entries are separate: they preserve and expose their pinned upstream reasoning +ladders rather than provider-configured routed metadata. Valid Grok rungs, including +`none` and `minimal`, are preserved when advertised. Unsupported or duplicate rungs, +including Codex-only `ultra`, are omitted from the file, keeping every emitted picker +option selectable. Grok Build talks to opencodex over Chat Completions and sends `reasoning_effort` when the ladder is advertised. The Chat Completions inbound translator defaults the internal @@ -145,8 +163,9 @@ the id `grok-4.5`. Generated aliases avoid dots entirely for this reason. `[model]` table actually changes (roughly a one-second debounce, compared by content), so a refreshed block reaches an open session without a restart. To confirm what Grok parsed, run `grok inspect`: it lists the config sources it loaded and warns about any field it - rejected. It does not print the resolved model list. Note that a single TOML error - invalidates the *entire* user config layer, which is why opencodex writes the file - atomically — Grok never sees a half-written config. + rejected. It does not print the resolved model list. Current Grok Build reports and skips + invalid model fields while retaining the rest of the model entry. A TOML syntax error still + prevents the file from loading. opencodex writes atomically, so Grok observes a complete + document on every reload. - **Catalog updates:** the fenced block reflects the catalog at injection time. After adding providers or models, run `ocx ensure` (or restart the proxy) to refresh it. diff --git a/docs-site/src/content/docs/ja/guides/grok-build.md b/docs-site/src/content/docs/ja/guides/grok-build.md index 6dc6b2f2e4..c33d417ab0 100644 --- a/docs-site/src/content/docs/ja/guides/grok-build.md +++ b/docs-site/src/content/docs/ja/guides/grok-build.md @@ -17,7 +17,18 @@ base_url = "http://127.0.0.1:10100/v1" api_backend = "chat_completions" api_key = "opencodex-loopback" name = "OCX gpt-5.6-sol" -# ... one [model.ocx-*] table per visible model ... +extra_headers = { "x-opencodex-grok" = "1" } +context_window = 372000 +supports_reasoning_effort = true +reasoning_effort = "low" + +[[model.ocx-gpt-5-6-sol.reasoning_efforts]] +id = "low" +value = "low" +label = "Low" +description = "Quick, fast implementations" +default = true +# ... remaining rungs for this model, then one [model.ocx-*] table per visible model ... # <<< opencodex managed block <<< ``` @@ -38,6 +49,32 @@ grok -m ocx-anthropic-claude-opus-4-8 -p "hello" # or in the TUI: /model ocx-anthropic-claude-opus-4-8 ``` +## 推論 effort + +Grok Build の `/effort`(および `--effort`)は、カタログ項目がラダーを公開している +モデルで動作します。モデル一覧は生の `GET /v1/models` 応答を読み、その項目には +`supports_reasoning_effort` と `reasoning_efforts` のメニュー選択肢が必要です。ラダーを +Grok 互換に投影した内容が、管理対象の各 `[model.*]` テーブルにも +`supports_reasoning_effort`、既定の +`reasoning_effort`、`[[model..reasoning_efforts]]` の各行として書き込まれます。 +ルーティングされたモデルでは、opencodex が設定済みのプロバイダー階層 +(`reasoningEfforts` / `modelReasoningEfforts` と +`modelDefaultReasoningEfforts` の既定値)を反映します。このメタデータはプロキシで +設定されたラダーを表し、アダプターは推論をエミュレートしたり、レベルを +プロバイダー固有のフィールドへ変換したりできます。空の階層リストでは effort +コントロールを表示しません。ネイティブ GPT-5.6 項目は、固定された上流の推論 +ラダーを保持します。モデルが公開する有効な Grok 段階(`none` と `minimal` を含む)は +保持されます。Codex 固有の `ultra` を含む、未対応または重複する段階はファイルから +除外され、出力された選択肢はすべて実行できます。 + +Grok Build は Chat Completions 経由で opencodex と通信し、ラダーが公開されている +場合は `reasoning_effort` を送ります。Chat Completions の入力変換は、この場合に +内部 Responses の `reasoning.summary` を `auto` に設定するため、推論トレースは +`delta.reasoning_content` として Grok に届きます。トレースを返さずにモデルに +推論させるクライアントは、`include_reasoning: false`(または +`reasoning.summary: "none"`)を設定できます。両方が指定された場合は、明示的な +`reasoning.summary` が優先されます。 + ## 認証メモ Grok Build では、ループバックでもカスタム モデルに対して空ではない API キーが必要です。挿入されたエントリにはプレースホルダー (`opencodex-loopback`) が含まれます。opencodex はループバック接続のアドミッション キーを無視するため、実際の秘密は関係しません。 @@ -80,6 +117,6 @@ api_key = "your-OPENCODEX_API_AUTH_TOKEN" アップストリーム沈黙中の `/v1/responses` ストリーム。 Grok Build の Responses デコーダは未知のイベント タイプを拒否するため、手動で構成された `api_backend = "responses"` モデルは低速なアップストリームではターン中に失敗する可能性があります。自動登録されたエントリは `api_backend = "chat_completions"` をピン留めしますが、生のハートビート フレームが表示されることはありません。 - **サービスでインストールされた `ocx restart`:** 実行中のプロキシが再起動の認可とドレインの調整を担当し、古いプロセスの終了後はインストール済みのサービス マネージャーが置換プロセスを起動します。サービス監視は維持されます。ループバックの自動登録を使用している場合に限り、マネージド ブロックもハンドオフ中に維持されます。非ループバック構成では Grok 設定を手動管理します。同じポートで、別の ID 検証済みプロセスが正常になったことを確認した場合にのみ成功します。 - **構成読み取りタイミング:** 最初に opencodex を起動し、その後 `grok` を起動します。 -予測可能な結果。 Grok Build は `~/.grok/config.toml` を監視し、`[model]` テーブルが実際に変更されると (内容で比較すると約 1 秒のデバウンス) 再ロードするため、更新されたブロックは再起動せずに開いているセッションに到達します。 Grok が解析した内容を確認するには、`grok inspect` を実行します。ロードされた設定ソースがリストされ、拒否されたフィールドについて警告が表示されます。解決されたモデルのリストは出力されません。単一の TOML エラーがユーザー設定レイヤー「全体」を無効にすることに注意してください。これが、opencodex がファイルをアトミックに書き込む理由です。Grok は書きかけの設定を決して認識しません。 +予測可能な結果。 Grok Build は `~/.grok/config.toml` を監視し、`[model]` テーブルが実際に変更されると (内容で比較すると約 1 秒のデバウンス) 再ロードするため、更新されたブロックは再起動せずに開いているセッションに到達します。 Grok が解析した内容を確認するには、`grok inspect` を実行します。ロードされた設定ソースがリストされ、拒否されたフィールドについて警告が表示されます。解決されたモデルのリストは出力されません。現在の Grok Build は無効なモデルフィールドを警告してスキップし、残りのモデル項目を保持します。TOML 構文エラーがあるとファイルは読み込まれません。opencodex はファイルをアトミックに書き込むため、Grok は再読み込みのたびに完全な文書を認識します。 - **カタログの更新:** フェンスで囲まれたブロックには、射出時のカタログが反映されます。後 プロバイダーまたはモデルを追加するには、`ocx ensure` を実行して (またはプロキシを再起動して) 更新します。 diff --git a/docs-site/src/content/docs/ko/guides/grok-build.md b/docs-site/src/content/docs/ko/guides/grok-build.md index 79bd048367..420849f232 100644 --- a/docs-site/src/content/docs/ko/guides/grok-build.md +++ b/docs-site/src/content/docs/ko/guides/grok-build.md @@ -17,7 +17,18 @@ base_url = "http://127.0.0.1:10100/v1" api_backend = "chat_completions" api_key = "opencodex-loopback" name = "OCX gpt-5.6-sol" -# ... one [model.ocx-*] table per visible model ... +extra_headers = { "x-opencodex-grok" = "1" } +context_window = 372000 +supports_reasoning_effort = true +reasoning_effort = "low" + +[[model.ocx-gpt-5-6-sol.reasoning_efforts]] +id = "low" +value = "low" +label = "Low" +description = "Quick, fast implementations" +default = true +# ... remaining rungs for this model, then one [model.ocx-*] table per visible model ... # <<< opencodex managed block <<< ``` @@ -34,6 +45,31 @@ grok -m ocx-anthropic-claude-opus-4-8 -p "hello" # or in the TUI: /model ocx-anthropic-claude-opus-4-8 ``` +## 추론 강도 + +Grok Build의 `/effort`(및 `--effort`)는 카탈로그 항목이 추론 단계 목록을 제공하는 +모델에서 동작합니다. 모델 목록은 원시 `GET /v1/models` 응답을 읽으며, 항목에는 +`supports_reasoning_effort`와 `reasoning_efforts` 메뉴 선택지가 있어야 합니다. 단계 목록을 +Grok 호환 형태로 투영한 결과가 각 관리형 `[model.*]` 테이블에도 +`supports_reasoning_effort`, 기본 +`reasoning_effort`, `[[model..reasoning_efforts]]` 선택 행으로 기록됩니다. +라우팅 모델의 경우 opencodex는 설정된 공급자 단계(`reasoningEfforts` / +`modelReasoningEfforts`와 `modelDefaultReasoningEfforts`의 기본값)를 반영합니다. 이 +메타데이터는 프록시에 설정된 라우팅 단계를 설명하며, 어댑터는 추론을 에뮬레이션하거나 +단계를 공급자 전용 필드로 매핑할 수 있습니다. 단계 목록이 비어 있는 모델은 effort +컨트롤을 표시하지 않습니다. 네이티브 GPT-5.6 항목은 고정된 업스트림 추론 단계를 +유지합니다. 모델이 제공하는 유효한 Grok 단계는 `none`과 `minimal`을 포함해 유지됩니다. +Codex 전용 `ultra`를 포함해 지원되지 않거나 중복된 단계는 파일에서 제외되어 기록된 +모든 선택지는 실행 가능합니다. + +Grok Build는 Chat Completions를 통해 opencodex와 통신하고 단계 목록이 제공되면 +`reasoning_effort`를 보냅니다. 이 경우 Chat Completions 입력 변환기는 내부 Responses의 +`reasoning.summary` 기본값을 `auto`로 설정하므로 추론 트레이스가 +`delta.reasoning_content`로 Grok에 전달됩니다. 모델은 추론하되 트레이스를 반환하지 +않도록 하려는 클라이언트는 `include_reasoning: false`(또는 +`reasoning.summary: "none"`)를 설정할 수 있습니다. 두 값이 함께 있으면 명시적인 +`reasoning.summary`가 우선합니다. + ## 인증 참고 Grok Build는 루프백에서도 사용자 정의 모델에 비어 있지 않은 API 키를 요구합니다. 주입되는 항목에는 자리표시자(`opencodex-loopback`)가 들어갑니다. opencodex는 루프백 연결의 admission key를 무시하므로 실제 비밀값은 들어가지 않습니다. @@ -74,5 +110,5 @@ api_key = "your-OPENCODEX_API_AUTH_TOKEN" - **Responses 백엔드와 keep-alive:** 상위 업스트림이 조용한 동안 opencodex는 `/v1/responses` 스트림에 `response.heartbeat` keep-alive를 보냅니다. Grok Build의 Responses 디코더는 알 수 없는 이벤트 타입을 거부하므로, 수동으로 설정한 `api_backend = "responses"` 모델은 느린 업스트림에서 턴 도중 실패할 수 있습니다. 자동 등록된 항목은 `api_backend = "chat_completions"`로 고정되며, 원시 heartbeat 프레임을 노출하지 않습니다. - **서비스 설치된 `ocx restart`:** 실행 중인 프록시는 재시작 권한 확인과 드레인 조정을 담당하고, 기존 프로세스가 종료된 뒤 설치된 서비스 관리자가 교체 프로세스를 시작합니다. 서비스 감독은 그대로 유지됩니다. 루프백 자동 등록을 사용하는 경우에만 관리 블록도 핸드오프 동안 유지되며, 비루프백 배포에서는 Grok 설정을 수동으로 관리합니다. 같은 포트에서 신원이 확인된 다른 프로세스가 정상 상태가 된 뒤에만 명령이 성공합니다. -- **설정 읽기 시점:** 가장 예측 가능한 결과를 얻으려면 opencodex를 먼저 시작하고 그다음 `grok`를 실행합니다. Grok Build는 `~/.grok/config.toml`을 감시하다가 `[model]` 테이블이 실제로 바뀔 때 다시 불러옵니다(내용을 기준으로 비교하는 약 1초 디바운스). 그래서 새로 고친 블록은 재시작 없이 열린 세션에도 들어갑니다. Grok가 무엇을 파싱했는지 확인하려면 `grok inspect`를 실행합니다. 이 명령은 로드한 설정 원본을 나열하고 거부한 필드가 있으면 경고합니다. 해석된 모델 목록은 출력하지 않습니다. TOML 오류 하나만으로도 사용자 설정 레이어 전체가 무효가 되므로, opencodex가 파일을 원자적으로 쓰는 이유도 여기에 있습니다. Grok는 절반만 써진 설정을 보지 않습니다. +- **설정 읽기 시점:** 가장 예측 가능한 결과를 얻으려면 opencodex를 먼저 시작하고 그다음 `grok`를 실행합니다. Grok Build는 `~/.grok/config.toml`을 감시하다가 `[model]` 테이블이 실제로 바뀔 때 다시 불러옵니다(내용을 기준으로 비교하는 약 1초 디바운스). 그래서 새로 고친 블록은 재시작 없이 열린 세션에도 들어갑니다. Grok가 무엇을 파싱했는지 확인하려면 `grok inspect`를 실행합니다. 이 명령은 로드한 설정 원본을 나열하고 거부한 필드가 있으면 경고합니다. 해석된 모델 목록은 출력하지 않습니다. 현재 Grok Build는 잘못된 모델 필드를 경고와 함께 건너뛰고 나머지 모델 항목을 유지합니다. TOML 구문 오류가 있으면 파일을 불러올 수 없습니다. opencodex는 파일을 원자적으로 기록하므로 Grok는 다시 읽을 때마다 완전한 문서를 봅니다. - **카탈로그 업데이트:** 펜스 블록은 주입 시점의 카탈로그를 반영합니다. 공급자나 모델을 추가한 뒤에는 `ocx ensure`를 실행하거나 프록시를 재시작해 갱신합니다. diff --git a/docs-site/src/content/docs/ru/guides/grok-build.md b/docs-site/src/content/docs/ru/guides/grok-build.md index 096bb7aa39..fff44723d8 100644 --- a/docs-site/src/content/docs/ru/guides/grok-build.md +++ b/docs-site/src/content/docs/ru/guides/grok-build.md @@ -21,7 +21,18 @@ base_url = "http://127.0.0.1:10100/v1" api_backend = "chat_completions" api_key = "opencodex-loopback" name = "OCX gpt-5.6-sol" -# ... one [model.ocx-*] table per visible model ... +extra_headers = { "x-opencodex-grok" = "1" } +context_window = 372000 +supports_reasoning_effort = true +reasoning_effort = "low" + +[[model.ocx-gpt-5-6-sol.reasoning_efforts]] +id = "low" +value = "low" +label = "Low" +description = "Quick, fast implementations" +default = true +# ... remaining rungs for this model, then one [model.ocx-*] table per visible model ... # <<< opencodex managed block <<< ``` @@ -46,6 +57,30 @@ grok -m ocx-anthropic-claude-opus-4-8 -p "hello" # or in the TUI: /model ocx-anthropic-claude-opus-4-8 ``` +## Уровень рассуждения + +Команда Grok Build `/effort` (и флаг `--effort`) работает для моделей, чья запись в каталоге +публикует шкалу уровней. Список моделей читает исходный ответ `GET /v1/models`; записи в нём +должны содержать `supports_reasoning_effort` и пункты меню `reasoning_efforts`. Совместимая с +Grok проекция этой шкалы записывается в каждую управляемую таблицу `[model.*]` через +`supports_reasoning_effort`, +значение `reasoning_effort` по умолчанию и строки +`[[model..reasoning_efforts]]`. Для маршрутизируемых моделей opencodex отражает +настроенные уровни провайдера (`reasoningEfforts` / `modelReasoningEfforts` и значение по +умолчанию из `modelDefaultReasoningEfforts`). Эти метаданные описывают шкалу прокси; адаптеры +могут эмулировать рассуждение или преобразовывать уровни в поля конкретного провайдера. Модели +с пустым списком уровней не показывают управление effort. Нативные записи GPT-5.6 сохраняют +закреплённые upstream-шкалы. Допустимые уровни Grok, включая `none` и `minimal`, сохраняются, +когда модель их объявляет. Неподдерживаемые или повторяющиеся уровни, в том числе предназначенный +для Codex `ultra`, исключаются из файла; каждый записанный пункт остаётся доступным для выбора. + +Grok Build обращается к opencodex через Chat Completions и отправляет `reasoning_effort`, когда +шкала опубликована. В этом случае входной преобразователь Chat Completions задаёт внутреннему +Responses `reasoning.summary` значение `auto`, поэтому трассировка рассуждений приходит в Grok +как `delta.reasoning_content`. Клиент может оставить рассуждение модели и скрыть трассировку с +помощью `include_reasoning: false` (или `reasoning.summary: "none"`). При наличии обоих +параметров приоритет имеет явно заданный `reasoning.summary`. + ## Замечание об аутентификации Grok Build требует непустой API-ключ для custom-моделей даже на loopback. Внедряемые записи несут @@ -122,8 +157,8 @@ api_key = "your-OPENCODEX_API_AUTH_TOKEN" содержимому), поэтому обновлённый блок доходит до уже открытой сессии без перезапуска. Чтобы проверить, что именно разобрал Grok, выполните `grok inspect`: он перечисляет источники конфигурации и предупреждает о полях, которые отверг. Список разрешённых моделей при этом не - печатается. Учтите, что одна TOML-ошибка делает недействительным *весь* пользовательский слой - конфигурации, поэтому opencodex пишет файл атомарно — Grok никогда не увидит полузаписанный - `config.toml`. + печатается. Текущая версия Grok Build сообщает о недопустимых полях модели, пропускает их и + сохраняет остальные данные записи. Синтаксическая ошибка TOML препятствует загрузке файла. + opencodex пишет файл атомарно, поэтому при каждой перезагрузке Grok видит целый документ. - **Обновления каталога:** fenced-блок отражает каталог на момент внедрения. После добавления провайдеров или моделей выполните `ocx ensure` (или перезапустите прокси), чтобы его обновить. diff --git a/docs-site/src/content/docs/tr/guides/grok-build.md b/docs-site/src/content/docs/tr/guides/grok-build.md index 44e4e324a7..492de6263e 100644 --- a/docs-site/src/content/docs/tr/guides/grok-build.md +++ b/docs-site/src/content/docs/tr/guides/grok-build.md @@ -22,7 +22,18 @@ base_url = "http://127.0.0.1:10100/v1" api_backend = "chat_completions" api_key = "opencodex-loopback" name = "OCX gpt-5.6-sol" -# ... görünür model başına bir [model.ocx-*] tablosu ... +extra_headers = { "x-opencodex-grok" = "1" } +context_window = 372000 +supports_reasoning_effort = true +reasoning_effort = "low" + +[[model.ocx-gpt-5-6-sol.reasoning_efforts]] +id = "low" +value = "low" +label = "Low" +description = "Quick, fast implementations" +default = true +# ... remaining rungs for this model, then one [model.ocx-*] table per visible model ... # <<< opencodex managed block <<< ``` @@ -55,18 +66,27 @@ grok -m ocx-anthropic-claude-opus-4-8 -p "hello" Grok Build'in `/effort` (ve `--effort`) ayarı yalnızca katalog girdisi merdiveni bildiren modeller için çalışır: model listesi getirme işlemi ham `GET /v1/models` yanıtını okur ve buradaki girdiler `supports_reasoning_effort` artı -`reasoning_efforts` menü seçeneklerini taşımalıdır. Yönlendirilen model -girdileri için opencodex, yapılandırılmış sağlayıcı katmanlarını -(`reasoningEfforts` / `modelReasoningEfforts` ve `modelDefaultReasoningEfforts` -varsayılanı) bu yanıta yansıtır. Bu meta veriler proxy tarafından -yapılandırılmış yönlendirilen merdiveni açıklar — yerel yukarı akış akıl yürütme -desteğini iddia etmez ve adaptörler akıl yürütmeyi taklit edebilir veya -seviyeleri sağlayıcıya özgü alanlarla eşleyebilir. Yapılandırılmış bir merdivene -sahip yönlendirilen modeller, tıpkı Codex'te olduğu gibi Grok Build'de de çaba -denetimini gösterir. Boş bir katman listesine sahip modeller, Codex davranışıyla -eşleşecek şekilde çaba denetimi tutmaz. Yerel GPT-5.6 girdileri ayrıdır: -sağlayıcı tarafından yapılandırılmış yönlendirilen meta veriler yerine -sabitlenmiş yukarı akış akıl yürütme merdivenlerini korur ve ortaya çıkarır. +`reasoning_efforts` menü seçeneklerini taşımalıdır. Merdivenin Grok ile uyumlu +bir izdüşümü, yönetilen her `[model.*]` tablosuna `supports_reasoning_effort`, varsayılan +`reasoning_effort` ve `[[model..reasoning_efforts]]` seçim satırlarıyla +yazılır. Yönlendirilen model girdileri için opencodex, yapılandırılmış sağlayıcı +katmanlarını (`reasoningEfforts` / `modelReasoningEfforts` ve +`modelDefaultReasoningEfforts` varsayılanı) yansıtır. Bu meta veriler proxy +tarafından yapılandırılmış merdiveni açıklar; adaptörler akıl yürütmeyi taklit +edebilir veya seviyeleri sağlayıcıya özgü alanlarla eşleyebilir. Boş bir katman +listesine sahip modeller çaba denetimi göstermez. Yerel GPT-5.6 girdileri, +sabitlenmiş yukarı akış akıl yürütme merdivenlerini korur. Modelin bildirdiği +geçerli Grok katmanları, `none` ve `minimal` dahil olmak üzere korunur. Codex'e özgü +`ultra` dahil desteklenmeyen veya yinelenen katmanlar dosyadan çıkarılır; yazılan her +seçenek seçilebilir durumda kalır. + +Grok Build, opencodex ile Chat Completions üzerinden konuşur ve merdiven +bildirildiğinde `reasoning_effort` gönderir. Bu durumda Chat Completions giriş +dönüştürücüsü, dahili Responses `reasoning.summary` değerini varsayılan olarak +`auto` yapar; böylece akıl yürütme izleri Grok'a `delta.reasoning_content` +olarak ulaşır. Modelin akıl yürütmesini sürdürüp izi gizlemek isteyen bir istemci +`include_reasoning: false` (veya `reasoning.summary: "none"`) ayarlayabilir. Her +iki seçenek de bulunduğunda açıkça belirtilen `reasoning.summary` önceliklidir. ## Kimlik doğrulama notu @@ -160,11 +180,10 @@ adlar bu nedenle noktalardan tamamen kaçınır. oturuma ulaşır. Grok'un neyi ayrıştırdığını doğrulamak için `grok inspect` komutunu çalıştırın: yüklediği yapılandırma kaynaklarını listeler ve reddettiği herhangi bir alan hakkında uyarır. Çözümlenen model listesini - yazdırmaz. Tek bir TOML hatasının *tüm* kullanıcı yapılandırma katmanını - geçersiz kıldığını unutmayın; bu nedenle opencodex dosyayı atomik olarak yazar - — Grok asla yarı yazılmış bir yapılandırma görmez. + yazdırmaz. Güncel Grok Build, geçersiz model alanlarını uyarıyla atlar ve model + girdisinin kalanını korur. Bir TOML sözdizimi hatası dosyanın yüklenmesini + engeller. opencodex dosyayı atomik olarak yazar; Grok her yeniden yüklemede + eksiksiz bir belge görür. - **Katalog güncellemeleri:** çitle çevrili blok, enjeksiyon anındaki kataloğu yansıtır. Sağlayıcılar veya modeller ekledikten sonra yenilemek için `ocx ensure` çalıştırın (veya proxy'yi yeniden başlatın). - - diff --git a/docs-site/src/content/docs/zh-cn/guides/grok-build.md b/docs-site/src/content/docs/zh-cn/guides/grok-build.md index ffd9e43254..66284aaef5 100644 --- a/docs-site/src/content/docs/zh-cn/guides/grok-build.md +++ b/docs-site/src/content/docs/zh-cn/guides/grok-build.md @@ -17,7 +17,18 @@ base_url = "http://127.0.0.1:10100/v1" api_backend = "chat_completions" api_key = "opencodex-loopback" name = "OCX gpt-5.6-sol" -# ... one [model.ocx-*] table per visible model ... +extra_headers = { "x-opencodex-grok" = "1" } +context_window = 372000 +supports_reasoning_effort = true +reasoning_effort = "low" + +[[model.ocx-gpt-5-6-sol.reasoning_efforts]] +id = "low" +value = "low" +label = "Low" +description = "Quick, fast implementations" +default = true +# ... remaining rungs for this model, then one [model.ocx-*] table per visible model ... # <<< opencodex managed block <<< ``` @@ -34,6 +45,27 @@ grok -m ocx-anthropic-claude-opus-4-8 -p "hello" # or in the TUI: /model ocx-anthropic-claude-opus-4-8 ``` +## 推理强度 + +Grok Build 的 `/effort`(以及 `--effort`)适用于目录条目声明了推理档位的模型。 +模型列表会读取原始 `GET /v1/models` 响应,其中的条目需要包含 +`supports_reasoning_effort` 和 `reasoning_efforts` 菜单选项。这组档位经过 Grok 兼容投影后 +会写入每个受管理的 `[model.*]` 表,包括 `supports_reasoning_effort`、默认 +`reasoning_effort` 和 `[[model..reasoning_efforts]]` 选择项。对于路由模型, +opencodex 会映射已配置的提供方档位(`reasoningEfforts` / +`modelReasoningEfforts`,以及 `modelDefaultReasoningEfforts` 中的默认值)。这些元数据 +描述代理配置的路由档位;适配器可以模拟推理,或将档位映射到提供方专用字段。档位列表 +为空的模型不会显示 effort 控件。原生 GPT-5.6 条目会保留固定的上游推理档位。 +模型声明的有效 Grok 档位(包括 `none` 和 `minimal`)都会保留。不受支持或重复的档位 +(包括 Codex 专用的 `ultra`)会从文件中省略,从而确保写出的每个选项都可执行。 + +Grok Build 通过 Chat Completions 与 opencodex 通信,并在声明档位时发送 +`reasoning_effort`。在这种情况下,Chat Completions 入站转换器会将内部 Responses 的 +`reasoning.summary` 默认设为 `auto`,因此推理轨迹会以 `delta.reasoning_content` +到达 Grok。需要模型执行推理且不返回轨迹的客户端,可以设置 +`include_reasoning: false`(或 `reasoning.summary: "none"`)。两个选项同时出现时, +显式的 `reasoning.summary` 优先。 + ## 认证说明 即使在 loopback 上,Grok Build 对自定义模型也要求一个非空 API key。注入的条目携带的是占位符(`opencodex-loopback`)——opencodex 会忽略 loopback 连接的接入密钥,因此这里不涉及任何真实机密。 @@ -74,5 +106,5 @@ api_key = "your-OPENCODEX_API_AUTH_TOKEN" - **Responses 后端与保活:** opencodex 在 `/v1/responses` 流上、上游静默期间会发送 `response.heartbeat` 保活事件。Grok Build 的 Responses 解码器会拒绝未知事件类型,因此手动配置为 `api_backend = "responses"` 的模型在上游较慢时可能会在对话中途失败。自动注册的条目会固定为 `api_backend = "chat_completions"`,这样就不会暴露原始的心跳帧。 - **服务安装后的 `ocx restart`:** 运行中的代理负责重启授权和排空协调;旧进程退出后,由已安装的服务管理器启动替换进程。服务监督始终保留。仅在 loopback 自动注册模式下,受管理区块也会在交接期间保留;非 loopback 部署使用手动管理的 Grok 配置。只有确认同一端口上出现另一个经过身份验证且健康的进程后,命令才会成功。 -- **配置读取时机:** 先启动 opencodex,再启动 `grok`,结果最可预测。Grok Build 会监视 `~/.grok/config.toml`,并在 `[model]` 表实际发生变化时重新加载(大约一秒的防抖,按内容比较),因此刷新后的区块可以在无需重启的情况下进入已打开的会话。要确认 Grok 解析到了什么,可以运行 `grok inspect`:它会列出已加载的配置来源,并提示被拒绝的字段,但不会打印最终解析出的模型列表。注意,单个 TOML 错误会使*整个*用户配置层失效,这也是 opencodex 以原子方式写入文件的原因——Grok 不会看到半写入的配置。 +- **配置读取时机:** 先启动 opencodex,再启动 `grok`,结果最可预测。Grok Build 会监视 `~/.grok/config.toml`,并在 `[model]` 表实际发生变化时重新加载(大约一秒的防抖,按内容比较),因此刷新后的区块可以在无需重启的情况下进入已打开的会话。要确认 Grok 解析到了什么,可以运行 `grok inspect`:它会列出已加载的配置来源,并提示被拒绝的字段,但不会打印最终解析出的模型列表。当前 Grok Build 会报告并跳过无效的模型字段,同时保留该模型条目的其余部分。TOML 语法错误仍会阻止文件加载。opencodex 会以原子方式写入文件,因此 Grok 每次重新加载时都会看到完整文档。 - **目录更新:** 有边界线的区块反映的是注入时的目录状态。添加提供方或模型后,运行 `ocx ensure`(或重启代理)以刷新它。 diff --git a/docs-site/src/content/docs/zh-tw/guides/grok-build.md b/docs-site/src/content/docs/zh-tw/guides/grok-build.md index 32ad708527..38c1f09763 100644 --- a/docs-site/src/content/docs/zh-tw/guides/grok-build.md +++ b/docs-site/src/content/docs/zh-tw/guides/grok-build.md @@ -17,7 +17,18 @@ base_url = "http://127.0.0.1:10100/v1" api_backend = "chat_completions" api_key = "opencodex-loopback" name = "OCX gpt-5.6-sol" -# ... one [model.ocx-*] table per visible model ... +extra_headers = { "x-opencodex-grok" = "1" } +context_window = 372000 +supports_reasoning_effort = true +reasoning_effort = "low" + +[[model.ocx-gpt-5-6-sol.reasoning_efforts]] +id = "low" +value = "low" +label = "Low" +description = "Quick, fast implementations" +default = true +# ... remaining rungs for this model, then one [model.ocx-*] table per visible model ... # <<< opencodex managed block <<< ``` @@ -38,13 +49,20 @@ grok -m ocx-anthropic-claude-opus-4-8 -p "hello" Grok Build 的 `/effort`(以及 `--effort`)只對目錄條目宣告了階梯的模型有效:它的模型清單擷取會讀取 原始的 `GET /v1/models` 回應,而該處的條目必須帶有 `supports_reasoning_effort` 以及 -`reasoning_efforts` 選單選項。對已路由的模型條目,opencodex 會把設定的供應商階梯 -(`reasoningEfforts` / `modelReasoningEfforts`,以及 `modelDefaultReasoningEfforts` 的預設值) -映象到該回應上。這份中繼資料描述的是 proxy 設定的路由階梯——它不代表原生產品的 reasoning 支援, -而 adapter 可能模擬 reasoning 或把檔位對映到供應商專用欄位。設定了階梯的路由模型在 Grok Build 中 -會顯示 effort 控制項,就像在 Codex 中一樣。階梯清單為空的模型不會保留 effort 控制項,這也與 -Codex 行為一致。原生 GPT-5.6 條目則分開處理:它們保留並暴露固定於上游的 reasoning 階梯,而不是 -供應商設定的路由中繼資料。 +`reasoning_efforts` 選單選項。這組階梯經 Grok 相容投影後會寫入每個受管理的 `[model.*]` 表格,包括 +`supports_reasoning_effort`、預設 `reasoning_effort`,以及 +`[[model..reasoning_efforts]]` 選項列。對已路由的模型條目,opencodex 會映射設定的 +供應商階梯(`reasoningEfforts` / `modelReasoningEfforts`,以及 +`modelDefaultReasoningEfforts` 的預設值)。這份中繼資料描述 proxy 設定的路由階梯;adapter 可以模擬 +reasoning,或把檔位對映到供應商專用欄位。階梯清單為空的模型不會顯示 effort 控制項。原生 GPT-5.6 +條目會保留固定於上游的 reasoning 階梯。模型宣告的有效 Grok 檔位(包括 `none` 與 `minimal`)都會 +保留。不受支援或重複的檔位(包括 Codex 專用的 `ultra`)會從檔案省略,確保寫出的每個選項都能實際使用。 + +Grok Build 透過 Chat Completions 與 opencodex 通訊,並在條目宣告階梯時送出 +`reasoning_effort`。在這種情況下,Chat Completions 入站轉換器會把內部 Responses 的 +`reasoning.summary` 預設設為 `auto`,因此推理軌跡會以 `delta.reasoning_content` 傳給 Grok。 +需要模型執行推理且不回傳軌跡的用戶端,可以設定 `include_reasoning: false`(或 +`reasoning.summary: "none"`)。兩個選項同時出現時,明確設定的 `reasoning.summary` 優先。 ## 認證注意事項 @@ -85,6 +103,6 @@ api_key = "your-OPENCODEX_API_AUTH_TOKEN" ## 已知限制 - **Responses 後端與 keep-alive:** opencodex 會在上游靜默期間,於 `/v1/responses` 串流上發出 `response.heartbeat` keep-alive。Grok Build 的 Responses 解碼器會拒絕未知的事件類型,因此手動設定 `api_backend = "responses"` 的模型,可能在上游較慢時於回合中途失敗。自動註冊的項目會固定為 `api_backend = "chat_completions"`,不會露出原始 heartbeat 框架。 -- **以服務安裝的 `ocx restart`:** 當 opencodex 在服務管理員下執行時,`ocx restart` 目前會停止服務並以非受管程序取代——服務持續性(自動重啟、開機啟動)會遺失,直到下次 `ocx service` 設定;若該非受管程序死亡,受管理區塊可能指向已死的代理程式,直到下一次 `ocx start`/`ocx ensure` 重新整理它。 -- **設定讀取時機:** 先啟動 opencodex,再啟動 `grok`,結果最可預期。Grok Build 會監看 `~/.grok/config.toml`,並在 `[model]` 表格實際變更時重新載入(約一秒 debounce,依內容比對),因此重新整理後的區塊可在不重啟的情況下到達開啟中的工作階段。若要確認 Grok 解析了什麼,執行 `grok inspect`:它會列出已載入的設定來源,並對任何被拒絕的欄位發出警告。它不會印出解析後的模型清單。請注意,單一 TOML 錯誤會使*整個*使用者設定層失效,這也是 opencodex 以原子方式寫入檔案的原因——Grok 永遠看不到半寫入的設定。 +- **以服務安裝的 `ocx restart`:** 執行中的代理負責重啟授權與排空協調;舊行程結束後,由已安裝且可用的服務管理員再拉起替換行程。服務監督會維持安裝狀態。在 loopback 自動註冊下,受管理區塊也會在交接期間保留;非 loopback 部署則改用手動管理的 Grok 設定。只有在同一連接埠上確認另一個經過身分驗證且健康的行程後,此命令才會成功。 +- **設定讀取時機:** 先啟動 opencodex,再啟動 `grok`,結果最可預期。Grok Build 會監看 `~/.grok/config.toml`,並在 `[model]` 表格實際變更時重新載入(約一秒 debounce,依內容比對),因此重新整理後的區塊可在不重啟的情況下到達開啟中的工作階段。若要確認 Grok 解析了什麼,執行 `grok inspect`:它會列出已載入的設定來源,並對任何被拒絕的欄位發出警告。它不會印出解析後的模型清單。目前的 Grok Build 會回報並略過無效的模型欄位,同時保留該模型條目的其餘內容。TOML 語法錯誤仍會阻止檔案載入。opencodex 會以原子方式寫入檔案,因此 Grok 每次重新載入時都會看到完整文件。 - **目錄更新:** 圍欄區塊反映注入當下的目錄。新增供應商或模型後,請執行 `ocx ensure`(或重啟代理程式)以重新整理它。 diff --git a/src/grok/effort.ts b/src/grok/effort.ts new file mode 100644 index 0000000000..2942640375 --- /dev/null +++ b/src/grok/effort.ts @@ -0,0 +1,83 @@ +/** + * Grok Build's accepted thinking-intensity rungs for a managed `[model.*]` table. + * + * Official settings reference documents the two scalars (`supports_reasoning_effort`, + * `reasoning_effort`). The picker menu is the working `[[model..reasoning_efforts]]` + * shape (id / value / label / description / default). Model-specific menus may include + * `none` and `minimal`; Codex-only `ultra` sits outside Grok's accepted set. Omitting it + * keeps every generated picker option executable and preserves the effort menu. + */ +export const GROK_REASONING_EFFORTS = [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh", + "max", +] as const; +export type GrokReasoningEffort = typeof GROK_REASONING_EFFORTS[number]; + +const GROK_REASONING_SET = new Set(GROK_REASONING_EFFORTS); + +/** Picker copy proven in a live `~/.grok/config.toml` `[[model.*.reasoning_efforts]]` menu. */ +const GROK_EFFORT_OPTIONS: Record = { + none: { label: "None", description: "No reasoning" }, + minimal: { label: "Minimal", description: "Minimal reasoning" }, + low: { label: "Low", description: "Quick, fast implementations" }, + medium: { label: "Medium", description: "Balanced effort" }, + high: { label: "High", description: "Highest quality with extensive reasoning" }, + xhigh: { label: "XHigh", description: "Extra high reasoning effort" }, + max: { label: "Max", description: "Maximum reasoning effort" }, +}; + +export function isGrokReasoningEffort(effort: string): effort is GrokReasoningEffort { + return GROK_REASONING_SET.has(effort); +} + +/** Keep catalog order; drop Grok-invalid rungs such as `ultra` and duplicates. */ +export function sanitizeGrokReasoningEfforts(efforts: readonly string[] | undefined): GrokReasoningEffort[] { + if (!efforts || efforts.length === 0) return []; + const seen = new Set(); + const out: GrokReasoningEffort[] = []; + for (const effort of efforts) { + if (!isGrokReasoningEffort(effort) || seen.has(effort)) continue; + seen.add(effort); + out.push(effort); + } + return out; +} + +/** + * Same fallback as the raw `GET /v1/models` Grok advertisement: configured default + * when it is on the (already sanitized) ladder, then medium, then high, then first. + */ +export function grokDefaultReasoningEffort( + efforts: readonly string[], + configuredDefault?: string, +): string | undefined { + if (efforts.length === 0) return undefined; + if (configuredDefault && efforts.includes(configuredDefault)) { + return configuredDefault; + } + if (efforts.includes("medium")) return "medium"; + if (efforts.includes("high")) return "high"; + return efforts[0]; +} + +export function grokReasoningEffortOption(effort: GrokReasoningEffort, isDefault: boolean): { + id: GrokReasoningEffort; + value: GrokReasoningEffort; + label: string; + description: string; + default: boolean; +} { + const meta = GROK_EFFORT_OPTIONS[effort]; + return { + id: effort, + value: effort, + label: meta.label, + description: meta.description, + default: isDefault, + }; +} diff --git a/src/grok/inject.ts b/src/grok/inject.ts index 9304d923ca..6782610dca 100644 --- a/src/grok/inject.ts +++ b/src/grok/inject.ts @@ -3,11 +3,19 @@ import { homedir } from "node:os"; import { join } from "node:path"; import { atomicWriteFile } from "../config"; import { applyEol, dominantEol, isLoopbackHostname, providerBaseHost } from "../codex/inject"; +import { + grokDefaultReasoningEffort, + grokReasoningEffortOption, + sanitizeGrokReasoningEfforts, +} from "./effort"; export interface GrokInjectModel { id: string; name?: string; contextWindow?: number; + /** Catalog ladder. Empty or absent omits every thinking-intensity field. */ + reasoningEfforts?: string[]; + defaultReasoningEffort?: string; } export interface GrokInjectResult { @@ -335,6 +343,28 @@ export function buildGrokManagedBlock( if (Number.isFinite(model.contextWindow) && (model.contextWindow ?? 0) > 0) { lines.push(`context_window = ${model.contextWindow}`); } + // Array-of-tables MUST follow every parent keyval (including inline extra_headers). + // Keep every picker option inside Grok's accepted CLI vocabulary; ultra is Codex-only. + const efforts = sanitizeGrokReasoningEfforts(model.reasoningEfforts); + const defaultEffort = grokDefaultReasoningEffort(efforts, model.defaultReasoningEffort); + if (defaultEffort !== undefined) { + lines.push( + "supports_reasoning_effort = true", + `reasoning_effort = ${tomlString(defaultEffort)}`, + ); + for (const effort of efforts) { + const option = grokReasoningEffortOption(effort, effort === defaultEffort); + lines.push( + "", + `[[model.${alias}.reasoning_efforts]]`, + `id = ${tomlString(option.id)}`, + `value = ${tomlString(option.value)}`, + `label = ${tomlString(option.label)}`, + `description = ${tomlString(option.description)}`, + `default = ${option.default}`, + ); + } + } } lines.push(END_MARKER); diff --git a/src/grok/models.ts b/src/grok/models.ts new file mode 100644 index 0000000000..fc778bee87 --- /dev/null +++ b/src/grok/models.ts @@ -0,0 +1,54 @@ +import { + filterCatalogVisibleModels, + nativeDefaultReasoningEffort, + nativeOpenAiContextWindow, + nativeReasoningEfforts, + visibleNativeSlugs, + type CatalogModel, +} from "../codex/catalog"; +import type { OcxConfig } from "../types"; +import type { GrokInjectModel } from "./inject"; + +/** + * Catalog → inject payload used by both `syncGrokConfig` and the dashboard enable + * path. Native rows carry the pinned ladder; routed rows carry + * `reasoningEfforts` / `defaultReasoningEffort`. The writer sanitizes Grok-invalid + * rungs — this function forwards the same lists `/v1/models` already advertises. + */ +export function buildGrokInjectModels( + config: Pick, + routed: CatalogModel[], +): GrokInjectModel[] { + return [ + ...visibleNativeSlugs(config).map(id => { + const contextWindow = nativeOpenAiContextWindow(id); + const reasoningEfforts = nativeReasoningEfforts(id); + const defaultReasoningEffort = nativeDefaultReasoningEffort(id); + return { + id, + ...(contextWindow !== undefined ? { contextWindow } : {}), + ...(reasoningEfforts.length > 0 ? { reasoningEfforts } : {}), + ...(defaultReasoningEffort !== undefined ? { defaultReasoningEffort } : {}), + }; + }), + ...routed.map(model => { + const efforts = model.reasoningEfforts ?? []; + return { + id: model.alias ?? `${model.provider}/${model.id}`, + ...(model.contextWindow !== undefined ? { contextWindow: model.contextWindow } : {}), + ...(efforts.length > 0 ? { reasoningEfforts: efforts } : {}), + ...(model.defaultReasoningEffort !== undefined + ? { defaultReasoningEffort: model.defaultReasoningEffort } + : {}), + }; + }), + ]; +} + +/** Visible routed catalog, then the shared inject list. */ +export function grokInjectModelsFromCatalog( + config: OcxConfig, + catalog: CatalogModel[], +): GrokInjectModel[] { + return buildGrokInjectModels(config, filterCatalogVisibleModels(catalog, config)); +} diff --git a/src/grok/sync.ts b/src/grok/sync.ts index 3fc9aaa08d..23a5628ba5 100644 --- a/src/grok/sync.ts +++ b/src/grok/sync.ts @@ -6,9 +6,10 @@ * * Deps are injectable (mirrors src/codex/sync.ts) so tests can run without a live proxy. */ -import { visibleNativeSlugs, filterCatalogVisibleModels, nativeOpenAiContextWindow, type CatalogModel } from "../codex/catalog"; +import type { CatalogModel } from "../codex/catalog"; import type { OcxConfig } from "../types"; import { injectGrokConfig, type GrokInjectModel, type GrokInjectResult } from "./inject"; +import { grokInjectModelsFromCatalog } from "./models"; export interface GrokSyncDeps { fetchAllModels: (config: OcxConfig) => Promise; @@ -34,20 +35,7 @@ export async function syncGrokConfig( ): Promise { let models: GrokInjectModel[]; try { - const routed = filterCatalogVisibleModels(await deps.fetchAllModels(config), config); - models = [ - // Native slugs carry their context window too. Without it Grok falls back to its own - // default (200k) and understates models like gpt-5.6-sol, which is 372k. This is the same - // accessor the dashboard's native rows use, so the two cannot disagree. - ...visibleNativeSlugs(config).map(id => { - const contextWindow = nativeOpenAiContextWindow(id); - return { id, ...(contextWindow !== undefined ? { contextWindow } : {}) }; - }), - ...routed.map(m => ({ - id: m.alias ?? `${m.provider}/${m.id}`, - ...(m.contextWindow !== undefined ? { contextWindow: m.contextWindow } : {}), - })), - ]; + models = grokInjectModelsFromCatalog(config, await deps.fetchAllModels(config)); } catch (err) { return { ok: false, diff --git a/src/server/index.ts b/src/server/index.ts index 7241eea139..12c9b2711f 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -19,6 +19,7 @@ import { getConfigDir, websocketsEnabled, } from "../config"; +import { grokDefaultReasoningEffort } from "../grok/effort"; import { reconcileOAuthProviders } from "../oauth"; import { withCatalogWriteSerialization } from "../codex/catalog-write-serialization"; import { invalidateCodexModelsCacheWithPermit } from "../codex/catalog/sync"; @@ -1015,10 +1016,8 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server { - if (efforts.length === 0) return {}; - const defaultEffort = configuredDefault && efforts.includes(configuredDefault) - ? configuredDefault - : efforts.includes("medium") ? "medium" : efforts.includes("high") ? "high" : efforts[0]; + const defaultEffort = grokDefaultReasoningEffort(efforts, configuredDefault); + if (defaultEffort === undefined) return {}; return { supports_reasoning_effort: true, reasoning_effort: defaultEffort, diff --git a/src/server/management/native-integration-routes.ts b/src/server/management/native-integration-routes.ts index 7a42475093..86000e29cb 100644 --- a/src/server/management/native-integration-routes.ts +++ b/src/server/management/native-integration-routes.ts @@ -18,9 +18,10 @@ * 011 (Claude Code), 012 (Grok). */ import { loadConfig, readRuntimePort, saveConfigPreservingClaudeCode } from "../../config"; -import { desktopVisibleNativeSlugs, filterCatalogVisibleModels, nativeOpenAiContextWindow, visibleNativeSlugs } from "../../codex/catalog"; +import { desktopVisibleNativeSlugs, filterCatalogVisibleModels } from "../../codex/catalog"; import { inspectDesktop3pConfigLibrary, removeDesktop3pStandardPivot, writeDesktop3pConfig } from "../../claude/desktop-3p"; import { injectGrokConfig, stripGrokConfig, type GrokInjectModel } from "../../grok/inject"; +import { grokInjectModelsFromCatalog } from "../../grok/models"; import { inspectGrokConfig } from "../../grok/inspect"; import { grokConfigPath } from "../../grok/status"; import { assertNativeTeardownOwned } from "../../integrations/native/ownership-preflight"; @@ -501,19 +502,7 @@ async function handleGrokToggle(ctx: ManagementContext): Promise { const fetchModels = deps.fetchAllModels ?? defaultFetchAllModels; let models: GrokInjectModel[]; try { - const routed = filterCatalogVisibleModels(await fetchModels(config), config); - models = [ - // Native slugs carry their context window: without it Grok falls back - // to its own 200k default and understates a 372k model. - ...visibleNativeSlugs(config).map(id => { - const contextWindow = nativeOpenAiContextWindow(id); - return { id, ...(contextWindow !== undefined ? { contextWindow } : {}) }; - }), - ...routed.map(m => ({ - id: m.alias ?? `${m.provider}/${m.id}`, - ...(m.contextWindow !== undefined ? { contextWindow: m.contextWindow } : {}), - })), - ]; + models = grokInjectModelsFromCatalog(config, await fetchModels(config)); } catch (error) { // A catalog failure must never write an empty fence (syncGrokConfig // guards this; the route inherits the rule). Nothing was written. diff --git a/tests/grok-effort-inject.test.ts b/tests/grok-effort-inject.test.ts new file mode 100644 index 0000000000..ce76e9c7ab --- /dev/null +++ b/tests/grok-effort-inject.test.ts @@ -0,0 +1,299 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { homedir, tmpdir } from "node:os"; +import { join } from "node:path"; +import { + nativeDefaultReasoningEffort, + nativeReasoningEfforts, + type CatalogModel, +} from "../src/codex/catalog"; +import { buildGrokManagedBlock, injectGrokConfig, type GrokInjectModel } from "../src/grok/inject"; +import { grokDefaultReasoningEffort, sanitizeGrokReasoningEfforts } from "../src/grok/effort"; +import { syncGrokConfig } from "../src/grok/sync"; +import { handleManagementAPI } from "../src/server/management-api"; +import type { ManagementApiDeps } from "../src/server/management/context"; +import type { OcxConfig } from "../src/types"; + +const NATIVE_SOL = "gpt-5.6-sol"; +const ROUTED_WITH_LADDER: CatalogModel = { + provider: "kimi", + id: "k3", + contextWindow: 262_144, + reasoningEfforts: ["low", "high", "max"], + defaultReasoningEffort: "high", +}; +const ROUTED_EMPTY_LADDER: CatalogModel = { + provider: "kimi", + id: "kimi-for-coding", + contextWindow: 262_144, + reasoningEfforts: [], +}; + +type GrokTomlModel = { + model?: string; + supports_reasoning_effort?: boolean; + reasoning_effort?: string; + reasoning_efforts?: Array<{ + id: string; + value: string; + label: string; + description: string; + default: boolean; + }>; +}; + +function baseConfig(): OcxConfig { + return { port: 10190, hostname: "127.0.0.1", defaultProvider: "openai", providers: {} } as OcxConfig; +} + +function tempGrokHome(): { root: string; grokHome: string } { + const root = mkdtempSync(join(tmpdir(), "ocx-grok-effort-")); + const grokHome = join(root, ".grok"); + mkdirSync(grokHome); + return { root, grokHome }; +} + +function parseModels(content: string): Record { + expect(() => Bun.TOML.parse(content)).not.toThrow(); + const parsed = Bun.TOML.parse(content) as { model?: Record }; + expect(parsed.model).toBeDefined(); + return parsed.model!; +} + +function tableByRoutedId(models: Record, id: string): GrokTomlModel { + const table = Object.values(models).find(entry => entry.model === id); + expect(table).toBeDefined(); + return table!; +} + +function expectedNativeEfforts(slug: string): string[] { + return sanitizeGrokReasoningEfforts(nativeReasoningEfforts(slug)); +} + +describe("Grok managed-block thinking-intensity injection", () => { + test("writer emits the proven Grok picker shape for a custom subset and omits an empty ladder", () => { + const withLadder = buildGrokManagedBlock(10100, [{ + id: "kimi/k3", + contextWindow: 262_144, + reasoningEfforts: ["low", "high", "max"], + defaultReasoningEffort: "high", + }]); + expect(withLadder).toContain("supports_reasoning_effort = true"); + expect(withLadder).toContain('reasoning_effort = "high"'); + expect(withLadder).toContain("[[model.ocx-kimi-k3.reasoning_efforts]]"); + expect(withLadder).toContain('id = "low"'); + expect(withLadder).toContain('value = "low"'); + expect(withLadder).toContain('label = "Low"'); + expect(withLadder).toContain('description = "Quick, fast implementations"'); + expect(withLadder).toContain("default = true"); + expect(withLadder).toContain("default = false"); + expect(withLadder).not.toContain("medium"); + expect(withLadder).not.toContain("xhigh"); + expect(withLadder).not.toContain("ultra"); + + const parsed = parseModels(withLadder); + const k3 = parsed["ocx-kimi-k3"]; + expect(k3.supports_reasoning_effort).toBe(true); + expect(k3.reasoning_effort).toBe("high"); + expect(k3.reasoning_efforts?.map(row => row.value)).toEqual(["low", "high", "max"]); + expect(k3.reasoning_efforts?.filter(row => row.default)).toEqual([ + { + id: "high", + value: "high", + label: "High", + description: "Highest quality with extensive reasoning", + default: true, + }, + ]); + + const empty = buildGrokManagedBlock(10100, [{ id: "kimi/plain" }]); + expect(empty).not.toContain("supports_reasoning_effort"); + expect(empty).not.toContain("reasoning_effort"); + expect(empty).not.toContain("reasoning_efforts"); + const emptyParsed = parseModels(empty); + expect(emptyParsed["ocx-kimi-plain"]?.supports_reasoning_effort).toBeUndefined(); + expect(emptyParsed["ocx-kimi-plain"]?.reasoning_effort).toBeUndefined(); + expect(emptyParsed["ocx-kimi-plain"]?.reasoning_efforts).toBeUndefined(); + }); + + test("writer drops Codex-only ultra and falls back when that was the configured default", () => { + const block = buildGrokManagedBlock(10100, [{ + id: "gpt-5.6-sol", + reasoningEfforts: ["low", "max", "ultra"], + defaultReasoningEffort: "ultra", + }]); + expect(block).not.toContain("ultra"); + const table = parseModels(block)["ocx-gpt-5-6-sol"]; + expect(table.reasoning_efforts?.map(row => row.value)).toEqual(["low", "max"]); + expect(table.reasoning_effort).toBe("low"); + }); + + test("writer preserves none and minimal while dropping Codex-only ultra", () => { + const block = buildGrokManagedBlock(10100, [{ + id: "voice/dual-mode", + reasoningEfforts: ["none", "minimal", "low", "ultra"], + defaultReasoningEffort: "minimal", + }]); + expect(block).not.toContain("ultra"); + + const table = parseModels(block)["ocx-voice-dual-mode"]; + expect(table.reasoning_effort).toBe("minimal"); + expect(table.reasoning_efforts?.map(row => row.value)).toEqual(["none", "minimal", "low"]); + expect(table.reasoning_efforts?.slice(0, 2)).toEqual([ + { + id: "none", + value: "none", + label: "None", + description: "No reasoning", + default: false, + }, + { + id: "minimal", + value: "minimal", + label: "Minimal", + description: "Minimal reasoning", + default: true, + }, + ]); + }); + + test("sync writes each model's own ladder into a parseable config.toml", async () => { + const { root, grokHome } = tempGrokHome(); + try { + const result = await syncGrokConfig(10190, baseConfig(), { grokHome }, { + fetchAllModels: async () => [ROUTED_WITH_LADDER], + injectGrokConfig, + }); + expect(result).toMatchObject({ ok: true, changed: true }); + const content = readFileSync(join(grokHome, "config.toml"), "utf8"); + const models = parseModels(content); + + const sol = tableByRoutedId(models, NATIVE_SOL); + const nativeLadder = expectedNativeEfforts(NATIVE_SOL); + expect(nativeReasoningEfforts(NATIVE_SOL)).toContain("ultra"); + expect(nativeLadder).not.toContain("ultra"); + expect(sol.supports_reasoning_effort).toBe(true); + expect(sol.reasoning_effort).toBe( + grokDefaultReasoningEffort(nativeLadder, nativeDefaultReasoningEffort(NATIVE_SOL)), + ); + expect(sol.reasoning_efforts?.map(row => row.value)).toEqual(nativeLadder); + expect(sol.reasoning_efforts?.filter(row => row.default)).toHaveLength(1); + expect(sol.reasoning_efforts?.find(row => row.default)?.value).toBe(sol.reasoning_effort); + + const k3 = tableByRoutedId(models, "kimi/k3"); + expect(k3.supports_reasoning_effort).toBe(true); + expect(k3.reasoning_effort).toBe("high"); + expect(k3.reasoning_efforts?.map(row => row.value)).toEqual(["low", "high", "max"]); + expect(k3.reasoning_efforts?.map(row => row.value)).not.toEqual(nativeLadder); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + test("sync omits thinking-intensity fields for a routed model with an empty tier list", async () => { + const { root, grokHome } = tempGrokHome(); + try { + const result = await syncGrokConfig(10190, baseConfig(), { grokHome }, { + fetchAllModels: async () => [ROUTED_EMPTY_LADDER], + injectGrokConfig, + }); + expect(result).toMatchObject({ ok: true, changed: true }); + const content = readFileSync(join(grokHome, "config.toml"), "utf8"); + const models = parseModels(content); + const plain = tableByRoutedId(models, "kimi/kimi-for-coding"); + expect(plain.supports_reasoning_effort).toBeUndefined(); + expect(plain.reasoning_effort).toBeUndefined(); + expect(plain.reasoning_efforts).toBeUndefined(); + expect(content).not.toMatch(/ocx-kimi-kimi-for-coding[\s\S]*supports_reasoning_effort/); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); +}); + +describe("dashboard Grok enable apply writes the same ladders", () => { + let grokHome: string; + let fixtureRoot: string; + let previousGrokHome: string | undefined; + let previousOpencodexHome: string | undefined; + const cleanup: string[] = []; + + beforeEach(() => { + previousGrokHome = process.env.GROK_HOME; + grokHome = mkdtempSync(join(tmpdir(), "ocx-grok-effort-apply-")); + cleanup.push(grokHome); + process.env.GROK_HOME = grokHome; + previousOpencodexHome = process.env.OPENCODEX_HOME; + fixtureRoot = mkdtempSync(join(tmpdir(), "ocx-owned-home-")); + cleanup.push(fixtureRoot); + process.env.OPENCODEX_HOME = fixtureRoot; + writeFileSync(join(fixtureRoot, "service-state.json"), JSON.stringify({ + version: 2, + codexHome: process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), + opencodexHome: fixtureRoot, + backend: "scheduler", + })); + }); + + afterEach(() => { + if (previousGrokHome === undefined) delete process.env.GROK_HOME; + else process.env.GROK_HOME = previousGrokHome; + if (previousOpencodexHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previousOpencodexHome; + while (cleanup.length) rmSync(cleanup.pop()!, { recursive: true, force: true }); + }); + + test("PUT /api/native-integrations/grok injects native + routed ladders", async () => { + const config = baseConfig(); + const deps: ManagementApiDeps = { + readRuntimePort: () => null, + fetchAllModels: (async () => [ROUTED_WITH_LADDER]) as never, + }; + const url = new URL("http://127.0.0.1:10190/api/native-integrations/grok"); + const res = await handleManagementAPI( + new Request(url, { + method: "PUT", + headers: { Host: url.host, "Content-Type": "application/json" }, + body: JSON.stringify({ enabled: true }), + }), + url, + config, + deps, + ); + expect(res?.status).toBe(200); + const content = readFileSync(join(grokHome, "config.toml"), "utf8"); + const models = parseModels(content); + const sol = tableByRoutedId(models, NATIVE_SOL); + expect(sol.supports_reasoning_effort).toBe(true); + expect(sol.reasoning_efforts?.map(row => row.value)).toEqual(expectedNativeEfforts(NATIVE_SOL)); + const k3 = tableByRoutedId(models, "kimi/k3"); + expect(k3.reasoning_effort).toBe("high"); + expect(k3.reasoning_efforts?.map(row => row.value)).toEqual(["low", "high", "max"]); + }); +}); + +describe("inject payload threading", () => { + test("sync and apply payloads both carry the catalog ladder, not a rebuilt one", async () => { + const { root, grokHome } = tempGrokHome(); + try { + const config = baseConfig(); + const catalog = [ROUTED_WITH_LADDER]; + let syncModels: GrokInjectModel[] | null = null; + await syncGrokConfig(10190, config, { grokHome }, { + fetchAllModels: async () => catalog, + injectGrokConfig: ((port, models, opts) => { + syncModels = models; + return injectGrokConfig(port, models, opts); + }) as typeof injectGrokConfig, + }); + const k3 = syncModels?.find(model => model.id === "kimi/k3"); + expect(k3?.reasoningEfforts).toEqual(["low", "high", "max"]); + expect(k3?.defaultReasoningEffort).toBe("high"); + const sol = syncModels?.find(model => model.id === NATIVE_SOL); + expect(sol?.reasoningEfforts).toEqual(nativeReasoningEfforts(NATIVE_SOL)); + expect(sol?.reasoningEfforts).toContain("ultra"); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); +}); diff --git a/tests/grok-orphan-adoption.test.ts b/tests/grok-orphan-adoption.test.ts index 2520904089..d7fa25c653 100644 --- a/tests/grok-orphan-adoption.test.ts +++ b/tests/grok-orphan-adoption.test.ts @@ -409,6 +409,32 @@ describe("Grok orphan adoption — fence boundary (#511 follow-up)", () => { expect(content).not.toContain("ocx-gpt-5-6-sol-2"); }); + test("a below-fence orphan still gets its reasoning_efforts tables swept", () => { + writeFileSync(configPath, [ + "[models]", + 'default = "ocx-gpt-5-6-sol"', + "", + ...fence("ocx-placeholder"), + "", + ...orphan("ocx-gpt-5-6-sol"), + "", + "[[model.ocx-gpt-5-6-sol.reasoning_efforts]]", + 'id = "low"', + 'value = "low"', + 'label = "Low"', + 'description = "Quick, fast implementations"', + "default = true", + "", + ].join("\n")); + + injectGrokConfig(10100, MODELS, { grokHome }); + const content = readFileSync(configPath, "utf8"); + + expect(content).not.toContain("[[model.ocx-gpt-5-6-sol.reasoning_efforts]]"); + expect(tables(content)).toEqual(["ocx-gpt-5-6-sol"]); + expect(content).not.toContain("ocx-gpt-5-6-sol-2"); + }); + test("an adjacent orphan with no blank line before the marker is still bounded", () => { writeFileSync(configPath, [ "[models]",