Skip to content

feat(codex): choose reasoning effort and fast mode per turn - #394

Closed
yuga-hashimoto wants to merge 5 commits into
mainfrom
ccr-d1e4054d-xnh8mt
Closed

yuga-hashimoto wants to merge 5 commits into
mainfrom
ccr-d1e4054d-xnh8mt

Conversation

@yuga-hashimoto

Copy link
Copy Markdown
Owner

Summary

Supersedes #389 by @doublepi123. #389 no longer merged cleanly after #391 landed on main, and its branch lives in a fork this session cannot push to, so its head is merged here onto the current main. The author's three commits are kept as they are; this branch adds one merge commit that resolves the conflicts, and one docs commit.

What was resolved (ChatViewModel.kt, ChatHomeScreen.kt; both sides kept):

What was added: docs/CODEX.md now says plainly that the per-turn settings replace ~/.codex/config.toml (see "Decision for the maintainer"), and records that serviceTier is a plain string | null in the 0.142.5 and 0.155.1 turn/start schema (generated with codex app-server generate-json-schema), so the explicit "default" cannot fail as an unknown enum value there.

The author's own comparison holds after the merge: the changed lines of #389 alone (64e90a99..58947621) and of this branch against main are the same set, so nothing of #389 was dropped or altered and nothing of main was lost.

Original description of #389

Codex's model/list reports each model's supported reasoning efforts and service tiers, but the app only read id and name, and turn/start carried neither setting. So the thinking chip never appeared for Codex, and fast mode could not be turned on.

  • Reasoning effort: CodexModels turns supportedReasoningEfforts into model variants, in the server's order, so the existing thinking chip lists them. none, minimal and ultra get labels in all locales. An effort the app does not know yet shows its raw id, capitalized.
  • Fast mode: a model whose serviceTiers lists the "Fast" tier (priority), or that uses the deprecated additionalSpeedTiers: ["fast"], gets a bolt Fast chip beside the thinking chip. The tier's own server description is the chip's screen-reader text. The user's choice is global and persisted. Unset means each model follows its own defaultServiceTier.
  • Explicit per-turn settings: every turn sends effort and serviceTier explicitly (resolveCodexTurnSettings). The app-server applies both to subsequent turns as well, so an omitted value would silently keep the previous turn's.
    • Behaviour change worth noting: this also overrides a model_reasoning_effort or service_tier set in ~/.codex/config.toml, because the app now always says what the chips show.
  • PromptRequest.serviceTier is Codex-only. The OpenCode V1 and V2 clients build their request bodies field by field, so it never reaches an OpenCode server.
  • docs/CODEX.md documents both controls.

Decision for the maintainer

Every turn sends effort and serviceTier, so a model_reasoning_effort or service_tier in ~/.codex/config.toml stops applying to turns sent from AndCode, even if the chips were never touched. The app-server keeps both for later turns, so not sending them would carry a previous model's ultra or a fast tier over to the next one; the author chose to be explicit. The review below judged this non-blocking, but it is a behaviour change for existing Codex users, so please accept it knowingly. The scope is wider than chat: scheduled tasks, voice and quick input also go through CodexTarget, pass no variant and no tier, and therefore always use the model defaults and do not follow the global Fast choice. Alternatives, if you would rather not accept it, are in the follow-ups below.

Testing

  • Local, with an Android SDK 35 installed in the session: ./gradlew :app:testGithubDebugUnitTest restricted to ChatViewModelTest (77), *Codex* (the Codex test classes, including the new CodexModelsTest, CodexTurnStartParamsTest, CodexServiceTierTest), LegalDisclosureComplianceTest (19), RuntimeCatalogRepositoryTest (16) and OpenCodeAgentSelectionTest (7): 209 tests, 0 failures.
  • Local: ./gradlew detekt spotlessCheck :app:compileGithubDebugAndroidTestKotlin: passed.
  • Local: every key of values/strings.xml exists in all seven locale files, no duplicate names in any file.
  • Not run locally: the full unit test suite, :app:lintGithubDebug, the release build and any on-device test; CI covers them. Nothing here was run against a live, signed-in Codex.

Review note

The repo-reviewer subagent type is not registered in this environment. The review below was produced by a general read-only agent that was told to read .opencode/agents/repo-reviewer.md and follow it, with the output format it prescribes. It covered d63f345b, the first version of the merge commit; the merge was since reworded (its message said main renamed the attachment list, which the review rightly flagged as inaccurate) and docs/CODEX.md gained the paragraph described above. The code is unchanged since the review.

Pre-PR review

判定: APPROVE

ブロッカー

なし

提案(非ブロッキング)

  • [要確認・優先度高] 古い Codex app-server で "default" が受け付けられるか未確認 — app/src/main/java/com/yugahashimoto/andcode/runtime/local/CodexRuntime.kt:698-705 / CodexTarget.kt:260
    • 現状: resolveCodexTurnSettings は serviceTier を常に非 null で返す。何も選んでいなければ "default" になり、全ターンの turn/start に必ず載る。
    • 古い版も対象にしている: CodexModels.kt:100-101 は serviceTiers を返さない旧サーバー(additionalSpeedTiers: ["fast"] だけを返す版)を明示的に扱っている。docs/CODEX.md:214 も「フィールドが無い版ではチップが出ないだけ」と書いている。
    • 心配な点: 縮退しているのは UI だけで、送信側は同じ版にも "default" を送る。その版の TurnStartParams.serviceTier が fast/flex の列挙型だった場合、"default" はデシリアライズエラーになる。すると全ターンが SessionError で失敗する。
    • 古い版のユーザーは実在しうる: CodexInstaller はインストール時点の latest を入れるだけで、自動更新はしない。docs によれば、端末で動いている実績があるのは 0.155.1。一方、ライブ検証は 0.163.0 だけ。
    • 確認方法: 0.155.1 などの旧版で codex app-server generate-json-schema を実行し、TurnStartParams.serviceTier の型を見る。
    • 受け付けない場合: "default" を送るのは、そのサーバーが serviceTiers / defaultServiceTier を返したときだけに絞る。拒否されると確認できたら、これはブロッカーとして扱うべき。
  • 設計判断(~/.codex/config.toml の model_reasoning_effort / service_tier を毎ターン上書きする件): 非ブロッキングと判断します
    • 非ブロッキングとする根拠:
      • (a) ブロッカー基準のうち該当しうるのは「挙動を壊す」だけ。ただしこれはバグではなく、作者が意図して PR 本文で告知した仕様変更。
      • (b) 送らない案にも実害がある。app-server は effort/tier を後続ターンへ持ち越すため、送らないとモデルを切り替えた後も前のモデルの ultra や fast が残る。毎ターン明示する方が決定的で正しい。
      • (c) アプリは config.toml を effort/tier の設定場所として案内していない(docs/CODEX.md の config.toml 言及は MCP の節だけ)。上書きされてもチップで戻せる。
    • それでも残る課題:
      • ① 影響はチャットに限らない。CodexTarget を通る全経路が対象で、ScheduleExecutionService.kt:299 / AndCodeVoiceSession.kt:383 / QuickInputActivity.kt:148 も含む。これらは variant も serviceTier も渡さないので、常に「モデル既定値 + "default"」になり、グローバルの Fast 設定も反映されない。
      • ② 戻す手段が不完全。
        • selectedVariant は保存されず、モデル切替や再起動で null に戻る(ChatViewModel.kt:931)。config.toml で high を使っていた人は毎回選び直しになる。
        • flex など fast 以外の tier は UI から選べない。service_tier = "flex" は常に "default" に上書きされ、これはコストが上がる方向。
        • thinking チップの「Default」は、Codex では config.toml ではなく model/list の既定値という意味になる。
      • ③ docs/CODEX.md:228-241 は「毎ターン明示送信する」とは書いているが、config.toml の値が効かなくなることは明記していない。依頼文の前提(docs に明記済み)とは異なる。PR 本文はネットワーク不可のため未確認。1 行の追記を推奨。
    • フォローアップ案:
      • 一度でも明示値を送ったスレッドだけ、以後明示送信を続ける。触っていないスレッドは送らずに config.toml に任せる。
      • 既定値の出どころを、model/list の値ではなく app-server の実効設定にする(実効設定を読む API がその版にあれば)。
  • モデルを選んでいないとき、3 か所が別のモデルを見ている — AndCodeApp.kt:1107-1108 / AndCodeApp.kt:650-658 / CodexTarget.kt:273
    • 入力欄は fallbackModelId(カタログ先頭)の efforts を thinking チップに出す。
    • Fast チップは chatState の選択モデル(null)で判定するので出ない。
    • CodexTarget は isDefault のモデルの既定値で解決する。Codex 本体は config.toml の model か組み込み既定を使う。
    • この PR の前は Codex の variants が空で、チップ自体が出なかった。fallback 時も Fast チップを同じモデルで判定するか、表示用モデルと isDefault を揃えることを推奨。
  • Fast を「モデル既定に従う(null)」へ戻す UI がない — ChatHomeScreen.kt:1794
    • toggleable は true/false しか返さないので、ChatViewModel.setFastMode(null) には UI から到達できない。
    • defaultServiceTier=priority のモデルで一度でも触ると、以後ずっとグローバルに固定される。docs の「unset follows the model」と運用がずれる。メニューか設定画面に「既定に戻す」を足すとよい。
  • テスト不足 — CodexTarget.kt:246-273
    • sendMessage のモデル解決に単体テストがない。対象は isDefault へのフォールバック、成功したときだけ modelsRefreshed=true にする処理、未知モデルのときの 1 回だけの再取得。
    • 純関数は CodexTurnStartParamsTest が押さえている。上記はその外側なので、解決部分を純関数に切り出してテストすることを推奨。
  • マージコミット d63f345b のメッセージが不正確(任意修正)

チェック済み項目

  • 範囲: git log、git diff --stat、git show --remerge-diff HEAD で確認。4 コミット・24 ファイル。merge-base は origin/main 47c8383、feat(codex): choose reasoning effort and fast mode per turn #389 の元の base は 64e90a9。
  • 競合の解消: remerge-diff で衝突箇所を確認した。ChatHomeScreen の import が 1 ハンク、ChatViewModel のコンストラクタ引数と PromptRequest が計 2 ハンクで、どれも両側を残している。
  • 作者の変更が改変されていないこと: PR 単体の diff(64e90a99..5894762)と、統合後の diff(origin/main..HEAD)を比べた。変更行(+/-)の集合が完全に一致した。feat(codex): choose reasoning effort and fast mode per turn #389 の変更に欠落・改変はなく、main 側の変更も失われていない。
  • pendingAttachments の扱い:
    • git show origin/main: で見ると、main でも名前は変わっておらず、fix: keep runtime error text on device and copy unsupported attachments into the workspace #391 が分割を足しただけ(origin/main の ChatViewModel.kt:1327-1409)。
    • HEAD の sendMessage(ChatViewModel.kt:1405-1556)では、PromptRequest に modelAttachments と serviceTier が渡る。楽観表示は pendingAttachments、offline/queue 系は modelAttachments。PromptRequest に旧名のまま渡している箇所は残っていない。
    • キューの再送は sendQueued から sendMessage に戻るので、serviceTier も付く。
    • serviceTierForPrompt() と variant は、どちらも coroutine 内で評価する既存パターンどおり。ワークスペース参照は本文テキストに入るだけで、Codex の入力組み立てとは干渉しない。
  • OpenCodeApiModels.kt: fix(opencode): decode agents whose hidden flag is null #388 の OpenCodeAgent.hidden: Boolean? と、feat(codex): choose reasoning effort and fast mode per turn #389 の OpenCodeModel/PromptRequest へのフィールド追加は別クラスで、干渉しない。PromptRequest は OpenCodeApiClient / V2ApiClient がフィールドごとに組み立てているため、serviceTier は OpenCode に送られない。
  • ChatViewModel の 2 つの構築経路:
    • AndCodeApp.kt:313-314 と ViewModelModule.kt:22-38 の両方に onCodexFastModeChanged / initialCodexFastMode / workspaceAttachmentNoteTemplate / videoNotSupportedMessage / awaitForeground が揃っている。
    • Koin 側の Repository 2 つは AppModule.kt:52,54 で single として登録済み。
  • ChatHomeScreen.kt:
  • strings.xml(8 ファイル): python3 -I で解析した。重複 0、欠落 0。新規 6 キーは全ロケールにあり、%1$s も一致。fix(chat): retry agents that fail to load instead of a dead mode chip #390 の chat_agents_* との衝突や重複もない。
  • AppPreferencesRepository / SecureSettingsRepository: contains で「未設定(null)」と「false」を区別できている。main 側の CrashReporter の追加とも干渉しない。
  • CodexModels / CodexTarget の前提:
    • catalog() は常にプロバイダ 1 つを返すので、.single() は安全。
    • @Volatile の 2 フィールドが別々に更新される点と、modelsRefreshed の確認と更新の間の競合は、起きても model/list を余分に読むか直近のカタログが混ざる程度で無害。
    • スラッシュコマンドの経路は、Codex が commands() 非対応のため通らない。
  • 未実施: テストの実行(読み取り専用のため、実行済みとされた結果を信用した)、lint、Codex 旧版での実機確認、PR 本文の確認(ネットワーク不可)。

Follow-ups (not done here, from the review)

  • Old servers and "default": the review's top item, whether an old server rejects "default", was checked for the schema only: serviceTier is a plain string in 0.142.5 and 0.155.1. Whether those servers treat "default" as standard speed at runtime was not run (needs a signed-in account).
  • Fast cannot be reset to "follow the model": the Fast chip toggles only true/false, so setFastMode(null) is unreachable from the UI.
  • Model shown vs model resolved: with no model selected, the thinking chip lists the first catalogue model's efforts, the Fast chip looks at the (null) selection, and CodexTarget resolves against the isDefault model.
  • Missing tests for CodexTarget.sendMessage's model resolution (fallback to isDefault, one refresh for an unknown model, modelsRefreshed only after a successful read).
  • Alternatives to always sending both settings: keep sending explicit values only on threads where the user chose something, or send the tier as serviceTierForTurn (turn-scoped, does not persist), leaving effort explicit.

Generated by Claude Code

doublepi123 and others added 5 commits October 18, 2026 02:01
Codex's model/list reports each model's supported reasoning efforts and
service tiers, but the app only read id and name, and turn/start carried
neither setting, so the thinking chip never appeared for Codex and fast
mode could not be turned on.

- CodexModels fills variants from supportedReasoningEfforts (server order)
  and records the fast tier, its description, and each model's default
  effort and service tier
- every turn sends explicit effort and serviceTier: the app-server applies
  them to subsequent turns too, so an omitted value would silently keep the
  previous turn's (resolveCodexTurnSettings)
- a global fast-mode choice (null = follow each model's default) is
  persisted and exposed to the chat as fastModeAvailable/Enabled
- composer gets a Fast chip next to the thinking chip; labels for the
  none/minimal/ultra efforts in all locales
- verified against codex 0.163.0 generated app-server types and a live
  model/list response
Conflicts with #391 (workspace attachments) in ChatViewModel and
ChatHomeScreen, resolved by keeping both sides:
- ChatViewModel constructor: keep workspaceAttachmentNoteTemplate and the
  Codex fast-mode parameters.
- Prompt request: send the Codex service tier together with modelAttachments.
  #391 split pendingAttachments into modelAttachments and
  workspaceAttachments, and #389 still passed pendingAttachments to the
  request; pendingAttachments stays for the optimistic message and previews.
- ChatHomeScreen imports: keep Role and the semantics imports.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xw2ucui6t6dQdyGRrnCbHb
The pre-PR review found that docs/CODEX.md explains why every turn sends
effort and serviceTier, but not that a model_reasoning_effort or
service_tier in ~/.codex/config.toml therefore stops applying to turns sent
from the app, nor that scheduled tasks, voice and quick input (which pass no
variant and no tier) always use the model defaults. Also record that
serviceTier is a plain string in the 0.142.5 and 0.155.1 turn/start schema,
so the explicit "default" is not rejected as an unknown enum value there.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xw2ucui6t6dQdyGRrnCbHb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants