Skip to content

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

Merged
yuga-hashimoto merged 22 commits into
yuga-hashimoto:mainfrom
doublepi123:feat/upstream-codex-effort-fast
Oct 20, 2026
Merged

yuga-hashimoto merged 22 commits into
yuga-hashimoto:mainfrom
doublepi123:feat/upstream-codex-effort-fast

Conversation

@doublepi123

Copy link
Copy Markdown
Contributor

Summary

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.

Testing

  • ./gradlew spotlessCheck detekt :app:lintGithubDebug :app:testGithubDebugUnitTest (JDK 17 + SDK 35): passed.
  • New tests:
    • CodexModelsTest: efforts, fast tier, defaults, and the legacy speed tier, from real model/list shapes.
    • CodexTurnStartParamsTest: the turn/start payload.
    • CodexServiceTierTest.
    • 7 cases in ChatViewModelTest: UI state and what is sent.
  • Checked against the generated app-server types of codex 0.163.0 and a live model/list response.
  • Follow-up commit 58947621 applies three of the reviewer's non-blocking suggestions below:
    • docs/CODEX.md no longer claims 0.163.0 is the first version with these fields.
    • The fastModeDescription KDoc now matches its use.
    • chatViewModel::setFastMode replaces the lambda.

Pre-PR review

判定: APPROVE

ブロッカー

なし

提案(非ブロッキング)

  • docs/CODEX.md:214 — 「codex-cli 0.163.0 が model/list にこれらのフィールドを持つ最初のバージョンで、それより古いバージョンでは省略される」という記述は誤りです。上流の codex-rs/app-server-protocol/src/protocol/v2/model.rs を確認したところ、rust-v0.142.5 と rust-v0.155.1 の時点で supported_reasoning_efforts / default_reasoning_effort / additional_speed_tiers / service_tiers / default_service_tier / is_default がすでにあります。「0.163.0 で live 検証済み」とだけ書き、「first version」の主張は削るのが正確です。CodexModelsTest.kt:11 の「live 0.155.1」という fixture 注記(このフィールドを含まない)とも矛盾しています。
  • CodexTarget.kt:260-266 / CodexRuntime.kt:691 — どのターンにも effort と serviceTier を明示して送るので、ユーザーが ~/.codex/config.toml に書いた model_reasoning_effort や service_tier が上書きされます。以前は送っていなかったので、これは挙動の変化です。設計として意図したもので docs にも理由が書かれていますが、PR 説明に一言あると親切です。代案もあります。0.155.1 以降の TurnStartParams には、そのターンだけに効いて後続ターンに残らない serviceTierForTurn があります("default" でスタンダード指定、省略時はスレッドの tier を継承)。tier をこちらで送れば、tier を毎ターン強制する必要がなくなります。effort には同等の仕組みがないので、現状の方式が妥当です。
  • CodexTarget.kt:246-273 — sendMessage のモデル解決(キャッシュ未ヒット時に model/list を 1 回だけ再取得する、isDefault 経由で既定モデルを使う、再取得失敗なら次のターンで再試行する)を直接テストしていません。resolveCodexTurnSettings 単体はテスト済みです。fake の CodexRuntime を差し込むのが難しいなら、resolveKnownModel と再取得の判定を純関数に切り出してテストする余地があります。
  • AndCodeApp.kt:1139 — { enabled -> chatViewModel.setFastMode(enabled) } は、周囲と同じく chatViewModel::setFastMode と書けます。(Boolean?) -> Unit は (Boolean) -> Unit に代入できます。ラムダは再コンポーズのたびに作り直されるので、周囲の書き方に揃えると良いです。
  • ChatViewModel.kt:518 — fastModeDescription の KDoc に「toggle's subtitle」とありますが、実際の用途はスクリーンリーダー向けの contentDescription です(ChatHomeScreen の FastModeChip)。KDoc を実際の用途に合わせてください。
  • ChatViewModel は runtime ごとの key で作られ、initialCodexFastMode は作成時に一度だけ読まれます。Fast チップが出るのは Codex の VM だけなので、今のところ実害はありません。将来ほかの runtime でも fast mode を出す場合は、AppPreferences.codexFastMode の StateFlow を購読する形にした方が安全です。

チェック済み項目

  • shell が使えなかったので、/tmp/opencode/review-upstream-codex-effort-fast.diff と .summary.txt(2 コミット、24 ファイル)を全 hunk 読み、worktree の実ファイルと照合しました。
  • i18n:追加された 6 キー(chat_thinking_level_none/minimal/ultra、chat_fast_mode、cd_fast_mode、cd_fast_mode_with_description)が values と全 7 ロケール(ar/es/fr/ja/pt-rBR/ru/zh-rCN)の計 8 ファイルにそろっていることを rg で確認しました。%1$s のプレースホルダもすべて一致しています。Kotlin 側にハードコードされたユーザー向け文字列はありません(Preview 内のサンプル文字列は除く)。
  • DI:ChatViewModel の 2 つの構築経路に同じ引数(onCodexFastModeChanged / initialCodexFastMode)が入っていることを確認しました(di/ViewModelModule.kt:22-29 と ui/AndCodeApp.kt:313-314)。AppPreferencesRepository / SecureSettingsRepository は AppModule.kt:52-54 で single として登録されています。
  • 永続化:SecureSettingsRepository.codexFastMode は remove と put で「未選択」と「選択済み」を区別しています。AppPreferencesRepository の初期読み込み(:98)と setCodexFastMode の state 更新も確認しました。
  • プロトコル整合:上流 openai/codex の rust-v0.163.0 と rust-v0.155.1 で、TurnStartParams.effort / serviceTier が後続ターンにも残る(sticky)こと、SERVICE_TIER_DEFAULT_REQUEST_VALUE = "default" が明示的なスタンダード指定であること、ServiceTier::from_request_value が "fast" と "priority" の両方を受け付けること(旧 additionalSpeedTiers 由来の "fast" も有効)、ReasoningEffort に none/minimal/…/ultra があることを確認しました。
  • PromptRequest.serviceTier は OpenCode 側に漏れません。OpenCodeApiClient / OpenCodeV2ApiClient はフィールドを 1 つずつ組み立てています。PromptRequest の他の呼び出し元(QuickInput、Schedule、Voice、OpenCodeBackend.command)は既定の null のままで、Codex 側でモデル既定値に解決されることも確認しました。
  • 状態:selectConfiguration の唯一の呼び出し元(AndCodeApp.kt:614)が fast 関連の 3 フィールドを渡していること、モデルを切り替えると selectedVariant がリセットされる既存の挙動が保たれていることを確認しました。
  • 並行性:CodexTarget の新しいキャッシュは @Volatile の参照を差し替えるだけで、新しい coroutine scope は作っていません。
  • テスト:CodexModelsTest、CodexTurnStartParamsTest、CodexServiceTierTest と、ChatViewModelTest に追加された 7 件が、変換、解決、UI 状態、送信内容をカバーしていることを確認しました。
  • コメント方針、secrets、worktree(.worktree/feat/... 配下)について問題はありません。

yuga-hashimoto and others added 21 commits October 17, 2026 15:21
Providers reject file parts outside images/PDF (for example
`'file part media type application/zip' functionality not supported`), so a
zip or other binary attachment could not be sent at all.

When the composer attaches a MIME a runtime cannot carry as a file part, copy
it into the workspace under `.andcode-attachments/` and reference the path in
the prompt text instead. Images and PDFs keep going as file parts.

This only applies to the on-device runtime, whose `/workspace` is a local
mount (`BackendKind.LOCAL`); a remote runtime is unaffected and still receives
the attachment directly, since there is no local mount to copy into.

PromptAttachment gains a `workspacePath`; ChatViewModel partitions attachments
into model parts and workspace references so the on-disk copy is never sent.
The `.andcode-attachments` directory name is now shared with Antigravity's
inline-attachment path.
…s on opt-in

A runtime/provider session failure was reported to Crashlytics as the raw
provider message wrapped in an exception. That text is free-form - it can
quote prompts, model output, file paths and hosts - and Crashlytics shares
its collection switch with Analytics, which was opt-in. A user who never
enabled analytics still had crash reports collected in release builds.

- Report a session failure as a fixed category only: `Runtime session error:
  <runtime>/<errorType>`, where errorType is accepted only if
  identifier-shaped (ProviderAuthError, ...) and otherwise becomes `unknown`.
  The raw message stays on-device in the notification.
- Disable Crashlytics collection by default in the manifest
  (firebase_crashlytics_collection_enabled=false) and follow the
  analytics/crash opt-in via CrashReporter.setEnabled(); debug builds stay
  off regardless.
- Turn off advertising-ID collection and remove the AD_ID permission.
- Broaden SecretRedaction: the credential behind a Basic Authorization header,
  every Cookie pair, quoted JSON credential fields, env-style key names
  (ANTHROPIC_API_KEY, refresh_token) and URL userinfo are now redacted.
- Refresh the bundled legal docs (EN and JA) to match.
With oh-my-opencode-slim installed, OpenCode's GET /agent reports the
plugin agents (orchestrator, fixer, explorer, ...) with "hidden": null.
OpenCodeAgent.hidden was a non-null Boolean, so kotlinx.serialization
rejected the whole list, the catalogue fell back to no agents, and the
mode chip opened an empty menu - tapping "build" appeared to do nothing.

Make hidden nullable (V1 and V2 DTOs) and treat only true as hidden.
Verified against a live OpenCode 1.19.3 server with the plugin enabled.
A local OpenCode with plugins (oh-my-opencode-slim, opencode-mem, ...)
installs them on its first GET /agent after a start: on a fresh cache that
call took ~25s here, and it can fail outright. The catalogue took that one
answer as final, so the agent list stayed empty until something else
refreshed it, and the mode chip kept showing "build" over a menu with
nothing in it - tapping it did nothing visible. Reproduced on an emulator
against a proxy that fails the first /agent: the chip stayed dead for the
whole session, across 60s and a background/foreground round trip.

- RuntimeCatalogRepository retries GET /agent (4 x 3s) while the answer is a
  failure or an empty list
- refreshAgents() re-reads only the agents, keeping the rest of the catalogue
- the mode chip asks for a refresh when tapped with no agents, instead of
  opening an empty menu
Per review: the agent retry ran inside load()'s refresh lock, holding the
whole catalogue (and every other refresh) behind up to several slow
GET /agent calls. load() now reads agents once and publishes, then a
single background retry (agentRetryJob) runs outside the lock.

- a failed or empty agent read no longer blanks agents this runtime
  already showed (same rule as providers, yuga-hashimoto#373)
- repeated taps on an empty chip share one retry instead of stacking polls
- a runtime that never connected gets a full refresh() from the chip
- success means at least one selectable primary agent, matching the chip
Per review: a first GET /agent failure put "Agents: ..." into the catalogue
error, and the background retry only replaced the list, so the workspaces
screen kept showing the failure after the agents had arrived. The agents
line is kept separately and dropped when the retry fills the list.

Also: a retry left over from a runtime the user switched away from is
cancelled rather than blocking the new runtime's, and starting a retry is
synchronized since taps (main) and load (IO) both reach it.
- decoding errors carry the JSON input on a second line, so the agents
  failure never matched a line of the joined error and stayed on screen
  after a successful retry; it is flattened to one line when recorded
- a chip tap while the runtime is still connecting no longer starts its own
  agent poll ahead of the load, which retries the agents once connected
A local OpenCode with plugins takes ~25s on its first GET /agent after a
start. During that time the mode chip had no agents and a tap only fired
a silent background refresh, so the chip looked dead on every app start or
runtime restart. Reproduced on an emulator with a proxy delaying the first
/agent by 25s: the first 7 taps (26s) showed nothing.

- RuntimeCatalogState.agentsLoading: true while agents are being read and
  none is on screen (load or the background retry), false once they arrive
  or the retries give up
- the chip swaps its shield for a small spinner while loading
- a tap always opens the menu: a disabled "Loading agents…" while loading,
  "Couldn't load agents. Tap to retry." once retries are over; agents that
  arrive while it is open replace the placeholder
- strings in all 8 locales
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
- load()'s final write reads the agents inside the update, so a retry that
  succeeded between its fetch and write is no longer overwritten with an
  empty list (which also left the spinner running)
- a retry tap on a never-connected runtime marks loading synchronously, so
  repeated taps cannot queue one full reconnect each
- the spinner carries the loading content description for TalkBack
- zh-CN uses 智能体 like the rest of the locale
… load loading

Apply the pre-PR review on the agent retry:
- a load whose GET /agent fails but keeps the list already on screen no
  longer leaves an "Agents: ..." line in the catalogue error, which
  nothing would clear until the next refresh
- a background retry that gives up while a load of the same runtime is
  still fetching keeps the chip loading instead of offering a retry that
  would race the load
- the chip itself does not ask for a refresh while it is loading
…386-387

fix: keep runtime error text on device and copy unsupported attachments into the workspace
…gent-hidden-null

fix(opencode): decode agents whose hidden flag is null
…gent-retry

fix(chat): retry agents that fail to load instead of a dead mode chip
Termux dropped proot 5.1.107.96 from packages.termux.dev (404 on every
mirror it was on; grimler.se answers with a body that does not match the
pinned SHA-256). A CI run that misses the generated-runtime cache then
fails in :app:prepareOpenCodeRuntimeAssets before any test starts, as
the re-run of the yuga-hashimoto#388 merge commit did twice.

- Refresh runtime_tools/termux_assets.lock.json with
  `prepare_android_runtime_assets.py --lock-only`. Only proot changes
  (5.1.107.96 -> 5.1.107.97, new filename and SHA-256 for arm64-v8a and
  x86_64); libandroid-shmem and libtalloc are unchanged.
- Mirror the matching termux-packages recipe: only TERMUX_PKG_VERSION and
  TERMUX_PKG_SHA256 differ from the previous one.
- Point THIRD_PARTY_NOTICES.md and its offline copy at the 5.1.107.97
  source archive and SHA-256.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xw2ucui6t6dQdyGRrnCbHb
The mirror is now at proot 5.1.107.97 while the fdroid pin still builds
5.1.107.94, so "a version or two" no longer describes the gap. Applies a
non-blocking suggestion of the pre-PR review, in both the root document and
its offline copy.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xw2ucui6t6dQdyGRrnCbHb
…d-xnh8mt

fix(build): pin proot 5.1.107.97, the version Termux still serves
yuga-hashimoto pushed a commit that referenced this pull request Oct 18, 2026
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
@yuga-hashimoto

Copy link
Copy Markdown
Owner

@copilot この pull request のマージの競合を修正してください。

@yuga-hashimoto
yuga-hashimoto merged commit fafaa64 into yuga-hashimoto:main Oct 20, 2026
5 checks passed
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