diff --git a/docs-site/astro.config.mjs b/docs-site/astro.config.mjs
index c1dfc83613..ab56fea23b 100644
--- a/docs-site/astro.config.mjs
+++ b/docs-site/astro.config.mjs
@@ -85,6 +85,7 @@ export default defineConfig({
translations: { ko: "가이드", "zh-CN": "指南", "zh-TW": "指南", ru: "Руководства", ja: "ガイド", tr: "Kılavuzlar" },
items: [
{ label: "Providers", translations: { ko: "프로바이더", "zh-CN": "提供商", "zh-TW": "供應商", ru: "Провайдеры", ja: "プロバイダー", tr: "Sağlayıcılar" }, slug: "guides/providers" },
+ { label: "Factory Droid Bridge", translations: { ko: "Factory Droid 브리지" }, slug: "guides/factory-droid" },
{ label: "Model Routing", translations: { ko: "모델 라우팅", "zh-CN": "模型路由", "zh-TW": "模型路由", ru: "Маршрутизация моделей", ja: "モデルルーティング", tr: "Model Yönlendirme" }, slug: "guides/model-routing" },
{ label: "Codex Integration", translations: { ko: "Codex 통합", "zh-CN": "Codex 集成", "zh-TW": "Codex 整合", ru: "Интеграция с Codex", ja: "Codex 連携", tr: "Codex Entegrasyonu" }, slug: "guides/codex-integration" },
{ label: "Codex App Model Picker", translations: { ko: "Codex App 모델 선택기", "zh-CN": "Codex App 模型选择器", "zh-TW": "Codex App 模型選擇器", ru: "Выбор модели в Codex App", ja: "Codex App モデルピッカー", tr: "Codex App Model Seçici" }, slug: "guides/codex-app-models" },
@@ -95,6 +96,7 @@ export default defineConfig({
{ label: "opencode", translations: { ko: "opencode", "zh-CN": "opencode", "zh-TW": "opencode", ru: "opencode", ja: "opencode", tr: "opencode" }, slug: "guides/opencode" },
{ label: "Pi", translations: { ko: "Pi", "zh-CN": "Pi", "zh-TW": "Pi", ru: "Pi", ja: "Pi", tr: "Pi" }, slug: "guides/pi" },
{ label: "Integrations", translations: { ko: "연동", "zh-CN": "集成", "zh-TW": "整合", ru: "Интеграции", ja: "連携", tr: "Entegrasyonlar" }, slug: "guides/integrations" },
+ { label: "MiniMax clients", translations: { ko: "MiniMax 클라이언트", "zh-CN": "MiniMax 客户端", "zh-TW": "MiniMax 客戶端", ru: "Клиенты MiniMax", ja: "MiniMax クライアント", tr: "MiniMax İstemcileri" }, slug: "guides/minimax" },
{ label: "Sidecars: Web Search & Vision", translations: { ko: "사이드카: 웹 검색 & 비전", "zh-CN": "边车:网络搜索与视觉", "zh-TW": "邊車:網路搜尋與視覺", ru: "Сайдкары: веб-поиск и зрение", ja: "サイドカー: ウェブ検索 & ビジョン", tr: "Sidecar'lar: Web Arama ve Görme" }, slug: "guides/sidecars" },
{ label: "Image Bridge", translations: { ko: "이미지 브릿지", "zh-CN": "图像桥接", "zh-TW": "圖像橋接", ru: "Image Bridge", ja: "画像ブリッジ", tr: "Image Bridge" }, slug: "guides/image-bridge" },
{ label: "Video Bridge", translations: { ko: "비디오 브릿지", "zh-CN": "视频桥接", "zh-TW": "影片橋接", ru: "Video Bridge", ja: "動画ブリッジ", tr: "Video Bridge" }, slug: "guides/video-bridge" },
diff --git a/docs-site/public/screenshots/minimax-code-integration.png b/docs-site/public/screenshots/minimax-code-integration.png
new file mode 100644
index 0000000000..824452c0cb
Binary files /dev/null and b/docs-site/public/screenshots/minimax-code-integration.png differ
diff --git a/docs-site/src/content/docs/guides/combos.md b/docs-site/src/content/docs/guides/combos.md
index 434cdcbb46..6bc5f3b6ee 100644
--- a/docs-site/src/content/docs/guides/combos.md
+++ b/docs-site/src/content/docs/guides/combos.md
@@ -211,6 +211,14 @@ default and leaves the target's own behavior unchanged. Supported values are `lo
`high`, `xhigh`, `max`, and `ultra`; omit the field or set it to `null` to leave effort entirely to
the caller and target.
+## Image / multimodal capability
+
+By default a combo publishes the **intersection** of its targets' input modalities (image is
+enabled only when every target advertises it). Set `imageInput: "disabled"` to force text-only
+even when every target supports images — the catalog drops `image` from `inputModalities`, and
+image-bearing requests are rejected with HTTP 400 before any target is called. `"auto"` (or
+omitting the field) keeps the automatic intersection.
+
## Encrypted v2 sub-agent tasks
There is one important limitation for Codex v2 sub-agents ([issue #92](https://github.com/lidge-jun/opencodex/issues/92)).
@@ -304,6 +312,7 @@ Combos are stored in the top-level `combos` object, keyed by combo id:
| `strategy` | No | `"failover"` | `"failover"` or `"round-robin"`. |
| `stickyLimit` | No | `1` | Integer from 1 to 100 successful requests per round-robin selection. |
| `defaultEffort` | No | `null` | `low`, `medium`, `high`, `xhigh`, `max`, or `ultra`; applied only when the caller omits effort and the target advertises support. |
+| `imageInput` | No | `"auto"` | `"auto"` or `"disabled"`. `"auto"` publishes image support only when every target supports images; `"disabled"` forces text-only (drops image from published modalities and rejects image-bearing requests before dispatch). |
| `alias` | No | none | Optional trimmed public model id; use the alias rules above. An empty value is stored as no alias. |
| `nativeAlias` | No | `false` | Explicitly permit a currently supported bare native `alias` to take routing and catalog precedence. Never inferred from the alias. |
| `displayName` | No | none | Bounded display-only catalog label. Required and non-empty when `nativeAlias` is true. |
diff --git a/docs-site/src/content/docs/guides/factory-droid.md b/docs-site/src/content/docs/guides/factory-droid.md
new file mode 100644
index 0000000000..55a8983792
--- /dev/null
+++ b/docs-site/src/content/docs/guides/factory-droid.md
@@ -0,0 +1,177 @@
+---
+title: Factory Droid bridge
+description: Connect Factory Droid models to opencodex through a local Responses-compatible bridge.
+---
+
+Factory Droid is an agent runtime, not a documented OpenAI-compatible inference endpoint. If a
+custom provider pointed at an internal Factory LLM URL returns `403 Forbidden`, changing only the
+opencodex adapter or adding provider headers does not make that private route a supported public API.
+
+The working integration is:
+
+```text
+Text-only Responses client
+ -> opencodex (http://127.0.0.1:10100/v1/responses)
+ -> local Responses bridge (http://127.0.0.1:11435/v1/responses)
+ -> official droid exec command
+ -> Factory account and selected model
+```
+
+This keeps the Factory credential inside the official Droid client. OpenCodex receives a separate,
+local-only bridge token.
+
+## What failed and why
+
+| Symptom | Cause | Fix |
+| --- | --- | --- |
+| `403 Forbidden` from a Factory LLM URL | The URL is not a documented general-purpose OpenAI endpoint for third-party clients | Invoke Factory through the official Droid CLI or SDK |
+| `404` at `/models/models` | The provider base URL already ended in `/models` | Use an API root as `baseUrl`; never include the discovery path |
+| Model search fails | The bridge does not expose a complete live catalog | Set `liveModels: false` and provide a static `models` list |
+| Loopback provider is rejected | Private-network access is denied by default | Set `allowPrivateNetwork: true` only for the loopback bridge |
+| `${DROID_BRIDGE_TOKEN}` is unresolved | The variable is missing from the opencodex service environment | Inject it into the service process, not only an interactive shell |
+| `OutputTextDelta without active item` | The bridge emitted a text delta before opening an output item and content part | Emit the complete Responses SSE lifecycle in order |
+
+The same Factory credential can therefore work in `droid exec` while a direct request to an
+undocumented LLM URL still returns `403`. Those results test different products and should not be
+treated as contradictory.
+
+## Prerequisites
+
+1. Install and sign in to the [Droid CLI](https://docs.factory.ai/droid-cli/quickstart).
+2. Confirm a bounded headless request works:
+
+ ```bash
+ droid exec --model glm-5.2 --output-format json "Reply with DROID_OK only."
+ ```
+
+3. Run a local bridge that invokes `droid exec` (or the official Droid SDK) and exposes:
+
+ - `GET /healthz`
+ - `GET /v1/models`
+ - `POST /v1/responses`
+
+Factory documents `droid exec` as its non-interactive automation surface and recommends JSON output
+for scripts. For a longer-lived integration, Factory also documents stream JSON-RPC and official
+TypeScript and Python SDKs in the
+[Droid Exec guide](https://docs.factory.ai/droid-exec/overview).
+
+## Bridge contract
+
+Bind the bridge to `127.0.0.1`, require a randomly generated bearer token, cap request sizes, and
+allowlist model IDs. The minimal bridge accepts only these Responses `input` shapes:
+
+- a non-empty string; or
+- an array containing only `message` items. Each message must have a `user`, `developer`, `system`,
+ or `assistant` role and either string content or text-only content parts (`input_text` for input
+ roles and `output_text` for assistant history).
+
+Validate the complete request before invoking Droid. If an input part is an image or file, `tools`
+contains any tool definition, or `input` contains a tool call or result (`function_call`,
+`function_call_output`, `custom_tool_call`, or `custom_tool_call_output`), return HTTP `400` with a
+Responses-style `invalid_request_error`. Use a stable bridge-specific code such as
+`unsupported_bridge_input` and identify the rejected field in the message. Do this before starting
+SSE, even when `stream: true`; never discard, stringify, or flatten unsupported content into the
+prompt.
+
+```json
+{
+ "error": {
+ "type": "invalid_request_error",
+ "code": "unsupported_bridge_input",
+ "param": "tools",
+ "message": "The minimal Droid bridge does not accept tool definitions."
+ }
+}
+```
+
+For an accepted request, the bridge should:
+
+1. Convert the accepted Responses `input` to a prompt.
+2. invoke `droid exec --model --output-format json `;
+3. parse the final `result` and `session_id`;
+4. return an OpenAI Responses envelope; and
+5. map `previous_response_id` to the Droid session ID when continuation is required.
+
+For streaming responses, emit this lifecycle in order:
+
+```text
+response.created
+response.output_item.added
+response.content_part.added
+response.output_text.delta
+response.output_text.done
+response.content_part.done
+response.output_item.done
+response.completed
+```
+
+Do not expose the bridge on `0.0.0.0` and do not reuse the Factory credential as the bridge bearer
+token.
+
+## OpenCodex provider configuration
+
+Create the custom provider with the explicit provider ID `droid`:
+
+```bash
+ocx provider add droid \
+ --adapter openai-responses \
+ --base-url http://127.0.0.1:11435/v1 \
+ --default-model glm-5.2 \
+ --allow-private-network
+```
+
+This creates the `providers.droid` config entry. In the dashboard, open **Providers → droid → Edit
+JSON** and replace that provider's value with:
+
+```json
+{
+ "adapter": "openai-responses",
+ "baseUrl": "http://127.0.0.1:11435/v1",
+ "responsesPath": "/responses",
+ "allowPrivateNetwork": true,
+ "authMode": "key",
+ "apiKey": "${DROID_BRIDGE_TOKEN}",
+ "liveModels": false,
+ "models": ["glm-5.2", "glm-5.2-fast", "kimi-k3"],
+ "defaultModel": "glm-5.2"
+}
+```
+
+The model IDs are examples. Keep only models that `droid exec` can use for the signed-in Factory
+account. Do not add Factory-specific inference headers to this provider: its upstream is the local
+bridge, not a Factory HTTP endpoint.
+
+After saving a provider or changing its static catalog, synchronize and restart the Codex
+app-server so new sessions read the updated catalog:
+
+```bash
+ocx sync --restart-codex
+ocx doctor
+```
+
+Restarting Codex app-server processes interrupts active Codex work. Run the restart only after
+finishing or saving those sessions.
+
+## Verify the complete route
+
+Check each boundary separately:
+
+```bash
+curl -fsS http://127.0.0.1:11435/healthz
+ocx doctor
+ocx access test droid/glm-5.2 --protocol responses
+```
+
+A provider row or model-picker entry proves only catalog visibility. The integration is working only
+after the Responses probe returns through the `droid/` route.
+
+## Current limitation
+
+The minimal bridge above translates text and the Responses SSE lifecycle. It does **not** implement
+the full bidirectional Codex function/tool-call protocol. Codex App and `codex exec` normally send
+tool definitions even when a prompt says not to call tools, and the current Codex CLI has no general
+flag that removes those definitions. The minimal bridge must reject those requests with the `400`
+contract above. Tool definitions, tool calls, tool results, permissions, cancellation, and rich
+Droid events require a stateful bridge built on Factory's stream JSON-RPC mode or an official Droid
+SDK. Treat `ocx access test` success as text-path verification, not Codex agent or tool-path
+verification.
diff --git a/docs-site/src/content/docs/guides/integrations.md b/docs-site/src/content/docs/guides/integrations.md
index 92e35af06e..f8b2c578d8 100644
--- a/docs-site/src/content/docs/guides/integrations.md
+++ b/docs-site/src/content/docs/guides/integrations.md
@@ -1,10 +1,10 @@
---
title: Integrations
-description: Connect opencodex to OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, Gajae Code and DeepSeek Harness from the dashboard — one switch per client, with a backup taken before every write.
+description: Connect opencodex to OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, Gajae Code, DeepSeek Harness and MiniMax Code from the dashboard — one switch per client, with a backup taken before every write.
---
The **Integrations** tab writes opencodex's provider block into a client's own config
-file, and removes it again. Eight clients work this way, each with a switch:
+file, and removes it again. Nine clients work this way, each with a switch:
| Client | Config file | Format | When the change takes effect | Credential |
|---|---|---|---|---|
@@ -16,6 +16,7 @@ file, and removes it again. Eight clients work this way, each with a switch:
| Kimi Code | `~/.kimi-code/config.toml` | TOML | on restart, or `/reload` | loopback placeholder |
| Gajae Code | `~/.gjc/agent/models.yml` | YAML | new sessions, or when you open `/model` |`OPENCODEX_GAJAE_API_KEY` |
| DeepSeek Harness (DSH) | `$DSH_HOME/settings.yaml` (default `~/.dsh/settings.yaml`) | YAML | hot reload | non-secret loopback bearer placeholder |
+| MiniMax Code | `~/.minimax/config.yaml` | YAML | new sessions, or after opening the model picker | loopback placeholder |
Managed DSH support has a compatibility floor of **DSH 0.1.0-rc.6**. OpenCodex owns only
`llm-pi-ai.providers.opencodex`; Apply and Refresh replace that fragment, Disable removes only that
@@ -23,6 +24,12 @@ fragment, and Restore puts back a recorded snapshot. DSH hot reloads provider ch
operations do not change the user's default model or the native `deepseek-official` provider.
The managed DSH integration is currently loopback-only and never writes a real credential.
+MiniMax Code follows `MINIMAX_DATA_DIR`, then `MAVIS_DATA_DIR`, before falling
+back to `~/.minimax`. Its managed block owns only `custom_provider.opencodex`.
+It does not change `defaultModel`, the selected MiniMax credential source, or
+the user's MiniMax login. Choose a `custom_provider:opencodex/`
+entry in MCode after connecting it.
+
Paths honor each client's own environment override where it has one. For OMP,
`OMP_PROFILE` wins over `PI_PROFILE` by presence, even when explicitly empty. A named profile
uses `PI_CONFIG_DIR` as a directory name relative to the user's home and ignores `PI_CODING_AGENT_DIR`; without a named profile,
@@ -83,8 +90,8 @@ than 1000 levels — which locks the switch instead, so nothing is silently chan
**OMP** is unaffected by sibling edits too, for a different reason: its writer
patches only its own `providers.opencodex` range byte-wise, so the rest of the
file is never rewritten. For the remaining formats that can carry comments
-(Hermes, OpenClaw, Kimi Code, Gajae Code — YAML, JSON5 and TOML written as whole
-documents), or
+(Hermes, OpenClaw, Kimi Code, Gajae Code, MiniMax Code — YAML, JSON5 and TOML
+written as whole documents), or
whenever our own entries were edited, the switch locks and disable refuses rather
than guessing which edits were yours.
@@ -106,8 +113,8 @@ changed value and calling it success. You will see the file named and nothing on
disk will have moved. Editing that file by hand still works; it is only our
automatic rewrite that declines.
-**Pi, Kimi Code, Gajae Code and the managed DSH integration only work against a loopback bind.**
-The first three have no config field for the `x-opencodex-api-key` header a non-loopback bind
+**Pi, Kimi Code, Gajae Code, MiniMax Code and the managed DSH integration only work against a loopback bind.**
+The first four have no config field for the `x-opencodex-api-key` header a non-loopback bind
requires. DSH has a generic headers map, but rc.6 does not document that dedicated admission
header as a supported integration contract, so the managed writer fails closed instead of
guessing. Give them loopback access through an SSH tunnel or a local forwarder that adds the header.
@@ -138,6 +145,29 @@ ocx integration client history --client hermes
ocx integration client restore --op [--confirm-drift]
```
+For MiniMax Code, connect the provider once and launch through the checked wrapper:
+
+```bash
+ocx integration client enable --client mcode
+ocx mcode
+```
+
+The separate MiniMax platform CLI (`mmx`) is not a file-toggle integration. Its text
+commands use MiniMax's Anthropic-compatible endpoint, so OpenCodex provides a
+credential-isolated, loopback-only launcher:
+
+```bash
+ocx mmx text chat --model anthropic/claude-opus-5 --message "Hello"
+ocx mmx text repl --model openai/gpt-5.6-sol
+```
+
+Only `mmx text chat` and `mmx text repl` are proxied. Run plain `mmx` for
+MiniMax-native image, video, speech, music, vision, search, quota, auth, config, file
+and update commands. The wrapper uses a temporary config containing only a non-secret
+loopback placeholder; it never loads your `~/.mmx` OAuth or API-key credentials, and
+it refuses `--api-key`, `--base-url` and `--region` overrides. See
+[MiniMax clients](/guides/minimax/) for the complete workflow and limits.
+
`--confirm-drift` is never assumed. If the file changed after the operation you are
restoring, the command refuses and tells you, because replacing your newer edits is your
decision to make.
diff --git a/docs-site/src/content/docs/guides/minimax.md b/docs-site/src/content/docs/guides/minimax.md
new file mode 100644
index 0000000000..b7ad0fdd2c
--- /dev/null
+++ b/docs-site/src/content/docs/guides/minimax.md
@@ -0,0 +1,110 @@
+---
+title: MiniMax clients
+description: Route MiniMax Code and MiniMax CLI text commands through OpenCodex without exposing MiniMax credentials.
+---
+
+MiniMax publishes two different command-line products. OpenCodex integrates each at
+the protocol boundary it actually exposes:
+
+- **MiniMax Code** (`mcode`) is a coding agent with custom Anthropic Messages providers.
+- **MiniMax CLI** (`mmx`) is a multimodal platform CLI. Only its `text` resource speaks
+ the Anthropic-compatible API that OpenCodex can route.
+
+## MiniMax Code
+
+Install and sign in to MiniMax Code using MiniMax's instructions first. Then start
+OpenCodex and connect the reversible file integration:
+
+```bash
+ocx start
+ocx integration client enable --client mcode
+ocx mcode
+```
+
+
+
+The integration merges one block into `~/.minimax/config.yaml`:
+
+```yaml
+custom_provider:
+ opencodex:
+ name: OpenCodex
+ kind: custom
+ enabled: true
+ api: anthropic-messages
+ options:
+ apiKey: opencodex-loopback
+ baseURL: http://127.0.0.1:10100
+ authMode: api-key
+ models:
+ anthropic/claude-opus-5: {}
+```
+
+The real generated model list comes from the running OpenCodex catalog. The block does
+not write a real key, does not replace `defaultModel`, and does not change your MiniMax
+login. In MCode, choose a model under `custom_provider:opencodex/...`.
+
+`ocx mcode` verifies that this provider points at the currently running proxy before it
+launches the client. If the port changed, refresh the managed block by running the enable
+command again. Disable or restore it through the same audited integration system:
+
+```bash
+ocx integration client disable --client mcode
+ocx integration client history --client mcode
+ocx integration client restore --op [--confirm-drift]
+```
+
+`MINIMAX_DATA_DIR` and the legacy `MAVIS_DATA_DIR` are honored. Relative overrides are
+refused because OpenCodex and MCode may start in different working directories.
+
+## MiniMax CLI (`mmx`)
+
+Install the official CLI separately:
+
+```bash
+npm install -g mmx-cli
+mmx --version
+```
+
+Route a text command through OpenCodex by using the wrapper and an OpenCodex model id:
+
+```bash
+ocx mmx text chat \
+ --model anthropic/claude-opus-5 \
+ --message "Explain this function"
+
+ocx mmx --output json text chat \
+ --model openai/gpt-5.6-sol \
+ --message "Return a JSON summary"
+```
+
+MMX hard-codes `/anthropic/v1/messages` below its API base URL. The wrapper starts a
+temporary loopback bridge for the lifetime of the child process. It accepts only POST
+requests to that Messages path and `/anthropic/v1/messages/count_tokens`, mapping them
+to OpenCodex's existing `/v1/messages` and `/v1/messages/count_tokens` data plane while
+preserving request bodies and query data. Canonical OpenCodex request translation,
+usage accounting and configured downstream provider authentication remain in effect;
+providers receive `x-api-key` or bearer transport according to their configuration.
+Streaming preserves Anthropic message and content events. Before forwarding, the bridge
+removes incoming admission credential headers and pins the public
+`opencodex-loopback` placeholder. Arbitrary Anthropic resources are not proxied, and
+the bridge is never exposed beyond loopback.
+
+The wrapper also creates a temporary `MMX_CONFIG_DIR` containing only that placeholder,
+then deletes it after `mmx` exits. Your `~/.mmx/config.json`, OAuth tokens and MiniMax
+API key are never loaded or copied.
+
+The following limits are intentional:
+
+- Only `text chat` and `text repl` are routed through OpenCodex.
+- `--api-key`, `--base-url` and `--region` are refused by the wrapper so caller
+ credentials or destination selectors cannot conflict with the isolated bridge.
+- The wrapper is loopback-only because MMX cannot send OpenCodex's dedicated
+ `x-opencodex-api-key` admission header for a remote bind.
+- Run plain `mmx` for `image`, `video`, `speech`, `music`, `vision`, `search`, `quota`,
+ `auth`, `config`, `file` and `update`; those call MiniMax-specific APIs that OpenCodex
+ does not emulate.
+
+`mmx` defaults its text model to `MiniMax-M3`. Pass `--model ` when you
+want a specific OpenCodex route; otherwise normal OpenCodex model routing rules decide
+whether the default id is available.
diff --git a/docs-site/src/content/docs/guides/model-ordering.md b/docs-site/src/content/docs/guides/model-ordering.md
index 87d209dee7..696f631a58 100644
--- a/docs-site/src/content/docs/guides/model-ordering.md
+++ b/docs-site/src/content/docs/guides/model-ordering.md
@@ -29,6 +29,7 @@ The relevant no-selector priorities are:
| --- | ---: | --- |
| `subagentModels[i]` | `i` (`0` through `4`) | The featured rank map in `src/codex/catalog/sync.ts` |
| Other routed models | `5` | Routed entry creation in `src/codex/catalog/sync.ts` |
+| Non-featured routed models listed in `modelPickerOrder` | `1000 + i` | Display-only picker rank in `src/codex/catalog/sync.ts` |
| Native GPT slugs by default | `9` | Native entry creation in `src/codex/catalog/sync.ts` |
| Unselected native models while a featured list exists | At least `featured.length + 100` | Native catalog merge in `src/codex/catalog/sync.ts` |
@@ -108,14 +109,34 @@ have expanded into selector-qualified groups.
## Changing the order
-The supported way to customize leading model order is to reorder `subagentModels`. The dashboard's
-**Sub-agents** page can reorder bare native and routed ids. Use `ocx agent subagents set` or edit the
-opencodex configuration for exact `/` choices; the dashboard does not
-list those choices and omits them if it saves the roster. Use at most five configured ids. With
-account selectors, one bare native choice can expand into multiple selector-qualified catalog rows,
-so configured choices and advertised rows are not necessarily one-to-one.
-
-There is currently no general `modelOrder`, `providerOrder`, or priority-map setting in `OcxConfig`.
-The supported ordering field is `subagentModels`; `disabledModels` and each provider's
-`selectedModels` are visibility fields. Changing the remaining picker order would require a
-code-level behavior change rather than a configuration edit.
+Use `subagentModels` to choose and order the leading models that Codex also advertises to
+`spawn_agent`. The dashboard's **Sub-agents** page can reorder bare native and routed ids. Use
+`ocx agent subagents set` or edit the opencodex configuration for exact
+`/` choices; the dashboard does not list those choices and omits them
+if it saves the roster. Use at most five configured ids. With account selectors, one bare native
+choice can expand into multiple selector-qualified catalog rows, so configured choices and
+advertised rows are not necessarily one-to-one.
+
+Use `modelPickerOrder` for display-only ordering of routed `/` rows beyond that
+featured block:
+
+```json
+{
+ "modelPickerOrder": [
+ "tyler/deepseek-v4-pro",
+ "jd-chat/kimi-k3",
+ "jd-chat/glm-5.2"
+ ]
+}
+```
+
+Listed routed rows appear in the configured order. A routed row omitted from the array keeps its
+normal priority, so it remains ahead of the `modelPickerOrder` display band; list every routed row
+whose relative position you want to control. A row also present in `subagentModels` keeps its
+featured priority. Bare native and account-qualified native rows are not reordered by
+`modelPickerOrder`; use `subagentModels` for those rows.
+
+`modelPickerOrder` never changes the `spawn_agent` candidate set. It changes only the
+Codex-visible picker priority while opencodex retains each moved row's natural priority for
+sub-agent selection. `disabledModels` and each provider's `selectedModels` remain visibility fields,
+not ordering controls. There is no separate `modelOrder`, `providerOrder`, or priority-map setting.
diff --git a/docs-site/src/content/docs/ko/guides/factory-droid.md b/docs-site/src/content/docs/ko/guides/factory-droid.md
new file mode 100644
index 0000000000..995602e53a
--- /dev/null
+++ b/docs-site/src/content/docs/ko/guides/factory-droid.md
@@ -0,0 +1,177 @@
+---
+title: Factory Droid 브리지
+description: 로컬 Responses 호환 브리지를 통해 Factory Droid 모델을 opencodex에 연결합니다.
+---
+
+Factory Droid는 에이전트 런타임이며, 문서화된 OpenAI 호환 추론 엔드포인트가 아닙니다. 내부
+Factory LLM URL을 사용자 지정 프로바이더로 등록했을 때 `403 Forbidden`이 발생한다면,
+opencodex 어댑터나 프로바이더 헤더만 바꿔도 그 비공개 경로가 지원되는 공개 API로 바뀌지는
+않습니다.
+
+검증된 연결 구조는 다음과 같습니다.
+
+```text
+텍스트 전용 Responses 클라이언트
+ -> opencodex (http://127.0.0.1:10100/v1/responses)
+ -> 로컬 Responses 브리지 (http://127.0.0.1:11435/v1/responses)
+ -> 공식 droid exec 명령
+ -> Factory 계정과 선택 모델
+```
+
+이 구조에서는 Factory 자격 증명을 공식 Droid 클라이언트 안에 유지합니다. OpenCodex에는 별도의
+로컬 전용 브리지 토큰만 전달합니다.
+
+## 실패 원인과 수정 방법
+
+| 증상 | 원인 | 해결 |
+| --- | --- | --- |
+| Factory LLM URL에서 `403 Forbidden` | 해당 URL은 서드파티 클라이언트용 범용 OpenAI 엔드포인트로 문서화되지 않음 | 공식 Droid CLI 또는 SDK를 통해 호출 |
+| `/models/models`에서 `404` | 프로바이더 Base URL에 `/models`가 이미 포함됨 | `baseUrl`에는 API 루트만 사용하고 검색 경로는 넣지 않음 |
+| 모델 검색 실패 | 브리지가 완전한 실시간 카탈로그를 제공하지 않음 | `liveModels: false`와 정적 `models` 목록 사용 |
+| 루프백 프로바이더 거부 | 사설 네트워크 접근은 기본적으로 차단됨 | 루프백 브리지에만 `allowPrivateNetwork: true` 설정 |
+| `${DROID_BRIDGE_TOKEN}`을 찾지 못함 | opencodex 서비스 환경에 변수가 없음 | 대화형 셸이 아니라 서비스 프로세스에 변수 주입 |
+| `OutputTextDelta without active item` | 출력 item과 content part를 열기 전에 text delta를 보냄 | Responses SSE 수명주기 전체를 순서대로 전송 |
+
+따라서 같은 Factory 자격 증명으로 `droid exec`는 성공하지만, 문서화되지 않은 LLM URL 직접
+요청은 `403`을 반환할 수 있습니다. 두 결과는 서로 다른 제품 표면을 시험한 것이므로 모순이
+아닙니다.
+
+## 준비 사항
+
+1. [Droid CLI](https://docs.factory.ai/droid-cli/quickstart)를 설치하고 로그인합니다.
+2. 제한된 headless 요청이 성공하는지 확인합니다.
+
+ ```bash
+ droid exec --model glm-5.2 --output-format json "DROID_OK만 답하세요."
+ ```
+
+3. `droid exec` 또는 공식 Droid SDK를 호출하면서 아래 엔드포인트를 제공하는 로컬 브리지를
+ 실행합니다.
+
+ - `GET /healthz`
+ - `GET /v1/models`
+ - `POST /v1/responses`
+
+Factory는 `droid exec`를 비대화형 자동화 표면으로 문서화하며, 스크립트에서는 JSON 출력을
+권장합니다. 장시간 유지되는 통합에는 stream JSON-RPC와 공식 TypeScript/Python SDK도 사용할 수
+있습니다. 자세한 내용은 [Droid Exec 가이드](https://docs.factory.ai/droid-exec/overview)를
+참고하세요.
+
+## 브리지 계약
+
+브리지는 `127.0.0.1`에만 바인딩하고, 무작위 bearer 토큰을 요구하며, 요청 크기와 모델 ID를
+제한해야 합니다. 최소 브리지는 다음 Responses `input` 형태만 허용합니다.
+
+- 비어 있지 않은 문자열
+- `message` item만 들어 있는 배열. 각 메시지의 role은 `user`, `developer`, `system`,
+ `assistant` 중 하나여야 하며, content는 문자열이거나 텍스트 전용 content part여야 합니다.
+ 입력 role에는 `input_text`, assistant 이력에는 `output_text`만 허용합니다.
+
+Droid를 실행하기 전에 요청 전체를 검증해야 합니다. input part에 이미지나 파일이 있거나,
+`tools`에 도구 정의가 하나라도 있거나, `input`에 도구 호출 또는 결과(`function_call`,
+`function_call_output`, `custom_tool_call`, `custom_tool_call_output`)가 있으면 Responses 형식의
+`invalid_request_error`와 함께 HTTP `400`을 반환합니다. `unsupported_bridge_input`처럼 안정적인
+브리지 전용 code를 사용하고 message에서 거부한 필드를 명시하세요. `stream: true`여도 SSE를
+시작하기 전에 이렇게 거부해야 합니다. 지원하지 않는 내용을 버리거나 문자열로 바꾸거나
+프롬프트에 합치면 안 됩니다.
+
+```json
+{
+ "error": {
+ "type": "invalid_request_error",
+ "code": "unsupported_bridge_input",
+ "param": "tools",
+ "message": "The minimal Droid bridge does not accept tool definitions."
+ }
+}
+```
+
+허용된 요청에 대해 브리지는 다음 작업을 수행합니다.
+
+1. 허용된 Responses `input`을 프롬프트로 변환합니다.
+2. `droid exec --model --output-format json `를 실행합니다.
+3. 최종 `result`와 `session_id`를 파싱합니다.
+4. OpenAI Responses envelope을 반환합니다.
+5. 대화 연속성이 필요하면 `previous_response_id`를 Droid session ID에 매핑합니다.
+
+스트리밍 응답은 다음 수명주기를 순서대로 보내야 합니다.
+
+```text
+response.created
+response.output_item.added
+response.content_part.added
+response.output_text.delta
+response.output_text.done
+response.content_part.done
+response.output_item.done
+response.completed
+```
+
+브리지를 `0.0.0.0`에 노출하지 말고, Factory 자격 증명을 브리지 bearer 토큰으로 재사용하지
+마세요.
+
+## OpenCodex 프로바이더 설정
+
+명시적인 프로바이더 ID `droid`로 사용자 지정 프로바이더를 생성합니다.
+
+```bash
+ocx provider add droid \
+ --adapter openai-responses \
+ --base-url http://127.0.0.1:11435/v1 \
+ --default-model glm-5.2 \
+ --allow-private-network
+```
+
+이 명령은 `providers.droid` 설정 항목을 만듭니다. 대시보드에서 **Providers → droid → JSON
+편집**을 열고 해당 프로바이더의 값을 다음 내용으로 바꿉니다.
+
+```json
+{
+ "adapter": "openai-responses",
+ "baseUrl": "http://127.0.0.1:11435/v1",
+ "responsesPath": "/responses",
+ "allowPrivateNetwork": true,
+ "authMode": "key",
+ "apiKey": "${DROID_BRIDGE_TOKEN}",
+ "liveModels": false,
+ "models": ["glm-5.2", "glm-5.2-fast", "kimi-k3"],
+ "defaultModel": "glm-5.2"
+}
+```
+
+모델 ID는 예시입니다. 로그인한 Factory 계정의 `droid exec`에서 실제로 사용할 수 있는 모델만
+남기세요. 이 프로바이더의 업스트림은 Factory HTTP 엔드포인트가 아니라 로컬 브리지이므로
+Factory 추론 전용 헤더를 추가하지 않습니다.
+
+프로바이더를 저장하거나 정적 카탈로그를 바꾼 뒤에는 새 세션이 갱신된 카탈로그를 읽도록 Codex
+app-server를 동기화하고 재시작합니다.
+
+```bash
+ocx sync --restart-codex
+ocx doctor
+```
+
+Codex app-server 재시작은 진행 중인 Codex 작업을 중단합니다. 해당 세션을 끝내거나 저장한 뒤에만
+재시작하세요.
+
+## 전체 경로 검증
+
+각 경계를 따로 확인합니다.
+
+```bash
+curl -fsS http://127.0.0.1:11435/healthz
+ocx doctor
+ocx access test droid/glm-5.2 --protocol responses
+```
+
+프로바이더 행이나 모델 선택기 표시는 카탈로그 노출만 증명합니다. Responses probe가
+`droid/` 경로를 통해 실제 응답을 반환해야 연동 성공입니다.
+
+## 현재 한계
+
+위 최소 브리지는 텍스트와 Responses SSE 수명주기만 변환합니다. Codex App과 `codex exec`는
+프롬프트에서 도구를 호출하지 말라고 해도 일반적으로 도구 정의를 보내며, 현재 Codex CLI에는 그
+정의를 모두 제거하는 범용 플래그가 없습니다. 최소 브리지는 위 계약에 따라 해당 요청을 `400`으로
+거부해야 합니다. 도구 정의, 도구 호출과 결과, 권한, 취소, 풍부한 Droid 이벤트를 처리하려면
+Factory stream JSON-RPC 모드 또는 공식 Droid SDK를 사용하는 상태 유지 브리지가 필요합니다.
+`ocx access test` 성공을 Codex 에이전트나 도구 경로 성공으로 간주하지 마세요.
diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md
index 62203e285d..e4c20b5fc4 100644
--- a/docs-site/src/content/docs/reference/configuration/providers.md
+++ b/docs-site/src/content/docs/reference/configuration/providers.md
@@ -105,6 +105,7 @@ differing backup and rewrites known legacy namespaced selected ids to bare ids.
| `noPenaltyModels?` | `string[]` | Models that reject presence/frequency penalties. |
| `noStructuredOutputModels?` | `string[]` | Exact model IDs whose `openai-chat` endpoint rejects `response_format`. Only an exact requested-model match omits the field; structured-output translation stays enabled for every other `openai-chat` model. |
| `parallelToolCalls?` | `boolean` | Toggle parallel tool calls. OpenAI Chat defaults on; non-chat adapters advertise only on explicit `true`. |
+| `terminalContinuationGuard?` | `boolean` | Opt in an `openai-chat` provider to one bounded internal re-ask when an actionable turn announces work, then cleanly stops without a tool call. Defaults to `false`; explicit `false` behaves like omission. Combo attempts and routed compaction turns are excluded, and non-`openai-chat` adapters ignore this option. |
| `responsesItemIdRepair?` | `{ message?: string[]; reasoning?: string[]; repairMissingTerminalIds?: boolean; repairInvalidIds?: boolean }` | Disabled-by-default downstream SSE repair for exact placeholder ids, missing terminal ids, and (with `repairInvalidIds`) message/reasoning ids missing the canonical `msg_`/`rs_` prefix. Function-call ids are never rewritten. Built-in DeepSeek enables the last two by default. |
| `responsesSnapshotRepair?` | `boolean` | Disabled-by-default client-facing repair for sparse Responses lifecycle snapshots in SSE and JSON. Fills missing canonical status, output, and tool metadata while raw inspection and persistence remain unchanged. |
| `retryOn429?` | `{ enabled?: boolean; attempts?: number; intervalMs?: number; maxIntervalMs?: number; respectRetryAfter?: boolean }` | API-key providers only (`authMode: "key"`). Opt-in same-target 429 retry: when `retryOn429` is absent the feature is off; object presence enables it unless `enabled: false`. On 429 the proxy waits (upstream `Retry-After` or the fixed interval) and replays the identical request on the same key before any key failover — across the main text-turn recovery loop, the Responses passthrough wire, the image/video bridge, the web-search sidecar, and terminal continuations. Only pre-stream HTTP 429 responses are eligible for replay; custom `runTurn` transports are outside the HTTP retry loop. `attempts` counts same-key replays after the first 429 (total sends = `attempts` + 1) and is one request-wide budget shared by the main recovery loop, the terminal-guard continuation, and bridge retries. Exhausting `attempts` only stops further same-key replays: normal key failover or final-error handling then applies per the available targets — on the key-auth passthrough wire there is no failover, so the exhausted 429 surfaces as-is. Codex itself never retries 429, so this is the only defense for single-key providers. Defaults: `enabled: true`, `attempts: 3`, `intervalMs: 5000`, `maxIntervalMs: 60000` (any single wait is capped at `maxIntervalMs`, itself capped at 600000), `respectRetryAfter: true`. |
diff --git a/docs-site/src/content/docs/reference/configuration/routing.md b/docs-site/src/content/docs/reference/configuration/routing.md
index a2ba4b01b2..795d2210e4 100644
--- a/docs-site/src/content/docs/reference/configuration/routing.md
+++ b/docs-site/src/content/docs/reference/configuration/routing.md
@@ -76,6 +76,7 @@ namespace, and cannot use reserved bare native families such as `gpt-*`, `o1-*`,
| `strategy?` | `"failover" \| "round-robin"` | `"failover"` | Selection strategy. Target order is failover priority; weights shape smooth weighted round-robin. |
| `stickyLimit?` | `number` | `1` | Successful requests retained in one round-robin batch. Range 1–100. |
| `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | Applied only when the caller omits effort and the selected target advertises the requested rung. |
+| `imageInput?` | `"auto" \| "disabled"` | `"auto"` | `"auto"` publishes image only when every target supports images; `"disabled"` forces text-only (drops image from published modalities and rejects image-bearing requests before dispatch). |
| `alias?` | `string` | — | Optional public model id in place of the canonical picker slug. |
| `nativeAlias?` | `boolean` | `false` | Let a currently supported bare native id take precedence only for that unqualified id. Bare `gpt-5.6-*` ids use Codex Pool/Direct credentials. Account-qualified routes remain distinct. Provider-qualified routes such as `openai-apikey/gpt-5.6-*` use their configured API-key route and never fall through to the native alias. |
| `displayName?` | `string` | — | Display-only catalog label, required and non-empty for a native alias. |
diff --git a/docs-site/src/content/docs/tr/guides/integrations.md b/docs-site/src/content/docs/tr/guides/integrations.md
index 88de3e0412..4ddd5e0bfd 100644
--- a/docs-site/src/content/docs/tr/guides/integrations.md
+++ b/docs-site/src/content/docs/tr/guides/integrations.md
@@ -1,10 +1,10 @@
---
title: Entegrasyonlar
-description: Kontrol panelinden OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, Gajae Code ve DeepSeek Harness'ı opencodex'e bağlayın — istemci başına tek bir anahtar ve her yazmadan önce alınan bir yedek.
+description: Kontrol panelinden OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, Gajae Code, DeepSeek Harness ve MiniMax Code'u opencodex'e bağlayın — istemci başına tek bir anahtar ve her yazmadan önce alınan bir yedek.
---
**Entegrasyonlar** sekmesi, opencodex'in sağlayıcı bloğunu istemcinin kendi
-yapılandırma dosyasına yazar ve tekrar kaldırır. Sekiz istemci bu şekilde
+yapılandırma dosyasına yazar ve tekrar kaldırır. Dokuz istemci bu şekilde
çalışır, her biri bir anahtarla:
| İstemci | Yapılandırma dosyası | Format | Değişiklik ne zaman geçerli olur? | Kimlik bilgisi |
@@ -17,6 +17,7 @@ yapılandırma dosyasına yazar ve tekrar kaldırır. Sekiz istemci bu şekilde
| Kimi Code | `~/.kimi-code/config.toml` | TOML | yeniden başlatmada veya `/reload` ile | geri döngü (loopback) yer tutucusu |
| Gajae Code | `~/.gjc/agent/models.yml` | YAML | yeni oturumlarda veya `/model` açtığınızda | `OPENCODEX_GAJAE_API_KEY` |
| DeepSeek Harness (DSH) | `$DSH_HOME/settings.yaml` (varsayılan `~/.dsh/settings.yaml`) | YAML | çalışırken yeniden yükleme | gizli olmayan geri döngü bearer yer tutucusu |
+| MiniMax Code | `~/.minimax/config.yaml` | YAML | yeni oturumlarda veya model seçici açıldıktan sonra | geri döngü (loopback) yer tutucusu |
Yönetilen DSH desteğinin en düşük uyumlu sürümü **DSH 0.1.0-rc.6**'dır. OpenCodex yalnızca
`llm-pi-ai.providers.opencodex` bölümünü yönetir: Uygula ve Yenile bu bölümü değiştirir, Devre Dışı
@@ -25,6 +26,12 @@ sağlayıcı değişikliklerini çalışırken yeniden yükler. Bu işlemler kul
veya yerel `deepseek-official` sağlayıcısını değiştirmez. Yönetilen DSH entegrasyonu şu anda yalnızca
geri döngü içindir ve asla gerçek bir kimlik bilgisi yazmaz.
+MiniMax Code önce `MINIMAX_DATA_DIR`, ardından `MAVIS_DATA_DIR` yolunu izler ve
+son olarak `~/.minimax` dizinine geri döner. Yönetilen blok yalnızca
+`custom_provider.opencodex` alanına sahiptir; `defaultModel` değerini, seçilen
+MiniMax kimlik bilgisi kaynağını veya kullanıcının MiniMax oturumunu değiştirmez.
+Bağladıktan sonra MCode içinde bir `custom_provider:opencodex/` girdisi seçin.
+
Yollar, varsa her istemcinin kendi ortam geçersiz kılmalarını dikkate alır. OMP
için `OMP_PROFILE`, açıkça boş olduğunda bile varlığıyla `PI_PROFILE`'a üstün
gelir. Adlandırılmış bir profil, `PI_CONFIG_DIR`'i kullanıcının ev dizinine göre
@@ -100,7 +107,7 @@ hiçbir şey sessizce değiştirilmez veya düşürülmez. **OMP** de yanındaki
düzenlemelerden etkilenmez, ama başka bir nedenle: writer'ı yalnızca kendi
`providers.opencodex` aralığını bayt bayt yamalar, dosyanın geri kalanı hiçbir
zaman yeniden yazılmaz. Yorum taşıyabilen diğer biçimlerde (Hermes, OpenClaw,
-Kimi Code, Gajae Code — bütün belge olarak yazılan YAML, JSON5 ve TOML) veya
+Kimi Code, Gajae Code, MiniMax Code — bütün belge olarak yazılan YAML, JSON5 ve TOML) veya
kendi girdilerimiz düzenlenmişse, anahtar kilitlenir ve hangi düzenlemelerin
size ait olduğunu tahmin etmek yerine devre dışı bırakmayı reddeder.
@@ -124,8 +131,8 @@ değişen bir değer yazıp buna başarı demek yerine durur ve bunu söyler. Do
adlandırıldığını ve diskte hiçbir şeyin taşınmadığını görürsünüz. Bu dosyayı
elle düzenlemek hala çalışır; yalnızca otomatik yeniden yazmamız reddeder.
-**Pi, Kimi Code, Gajae Code ve yönetilen DSH entegrasyonu yalnızca geri döngü (loopback) bağlantısına karşı
-çalışır.** İlk üçünün yapılandırmasında geri döngü olmayan bir bağlantının gerektirdiği
+**Pi, Kimi Code, Gajae Code, MiniMax Code ve yönetilen DSH entegrasyonu yalnızca geri döngü (loopback) bağlantısına karşı
+çalışır.** İlk dördünün yapılandırmasında geri döngü olmayan bir bağlantının gerektirdiği
`x-opencodex-api-key` başlığı için alan yoktur. DSH genel bir headers haritası sunar, ancak rc.6
bu özel kabul başlığını desteklenen bir entegrasyon sözleşmesi olarak belgelememektedir; bu nedenle
yönetilen writer tahmin yürütmek yerine kapalı biçimde reddeder. Bunun yerine bir SSH tüneli veya
@@ -158,6 +165,29 @@ ocx integration client history --client hermes
ocx integration client restore --op [--confirm-drift]
```
+MiniMax Code için sağlayıcıyı bir kez bağlayın ve denetimli başlatıcı üzerinden çalıştırın:
+
+```bash
+ocx integration client enable --client mcode
+ocx mcode
+```
+
+Ayrı MiniMax platform CLI'si (`mmx`) bir dosya anahtarı entegrasyonu değildir.
+Metin komutları MiniMax'ın Anthropic uyumlu uç noktasını kullandığı için OpenCodex,
+kimlik bilgilerini yalıtan ve yalnızca geri döngüde çalışan bir başlatıcı sağlar:
+
+```bash
+ocx mmx text chat --model anthropic/claude-opus-5 --message "Hello"
+ocx mmx text repl --model openai/gpt-5.6-sol
+```
+
+Yalnızca `mmx text chat` ve `mmx text repl` proxy üzerinden yönlendirilir. MiniMax'a
+özgü diğer komutlar için doğrudan `mmx` çalıştırın. Başlatıcı yalnızca gizli olmayan
+geri döngü yer tutucusunu içeren geçici bir yapılandırma kullanır; `~/.mmx` OAuth veya
+API anahtarı kimlik bilgilerinizi yüklemez ve `--api-key`, `--base-url` ile `--region`
+geçersiz kılmalarını reddeder. Tam iş akışı için
+[MiniMax istemcileri](/guides/minimax/) sayfasına bakın.
+
`--confirm-drift` asla varsayılmaz. Geri yüklediğiniz işlemden sonra dosya
değiştiyse, komut reddeder ve size bildirir; çünkü daha yeni düzenlemelerinizin
üzerine yazmak sizin vereceğiniz bir karardır.
diff --git a/docs-site/src/content/docs/zh-cn/guides/combos.md b/docs-site/src/content/docs/zh-cn/guides/combos.md
index 244d3d71ba..fe89fc04c7 100644
--- a/docs-site/src/content/docs/zh-cn/guides/combos.md
+++ b/docs-site/src/content/docs/zh-cn/guides/combos.md
@@ -158,6 +158,10 @@ combo 失败分为 **跳转** 失败和 **终止** 失败。
当目标能力未知,或者不包含配置的 effort 时,opencodex 会省略默认值,并保持目标自身行为不变。支持的值是 `low`、`medium`、`high`、`xhigh`、`max` 和 `ultra`;省略该字段或将其设为 `null`,就会把 effort 完全交给调用方和目标。
+## 图片 / 多模态能力
+
+默认情况下,combo 会发布其目标 **input modalities 的交集**(只有当每个目标都声明支持图片时,图片才会启用)。设置 `imageInput: "disabled"` 可在目标均支持图片时仍强制仅文本——目录会从 `inputModalities` 中去掉 `image`,带图请求会在分发前以 HTTP 400 拒绝。`"auto"`(或省略该字段)保持自动交集。
+
## 加密的 v2 子代理任务
对于 Codex v2 子代理,有一个重要限制([issue #92](https://github.com/lidge-jun/opencodex/issues/92))。原生父进程只能把新启动 worker 的任务,以为原生 ChatGPT 后端生成的密文形式发送出去。外部 provider 无法读取那段负载。
@@ -241,6 +245,7 @@ combo 会存储在顶层的 `combos` 对象中,并以 combo id 作为键:
| `strategy` | 否 | `"failover"` | `"failover"` 或 `"round-robin"`。 |
| `stickyLimit` | 否 | `1` | 每次轮询选择可连续处理的成功请求数,范围为 1 到 100。 |
| `defaultEffort` | 否 | `null` | `low`、`medium`、`high`、`xhigh`、`max` 或 `ultra`;仅当调用方省略 effort 且目标声明支持时才会应用。 |
+| `imageInput` | 否 | `"auto"` | `"auto"` 或 `"disabled"`。`"auto"` 仅在每个目标都支持图片时发布图片能力;`"disabled"` 强制仅文本(从对外能力中去掉图片,并在分发前拒绝带图请求)。 |
| `alias` | 否 | 无 | 可选的、已修剪的公开模型 id;使用上面的别名规则。空值会以“无别名”形式存储。 |
| `nativeAlias` | 否 | `false` | 显式允许当前受支持的裸原生 alias 接管路由和 catalog 优先级;绝不会根据 alias 自动推断。 |
| `displayName` | 否 | 无 | 仅用于 catalog 展示的有界标签;`nativeAlias` 为 true 时必须非空。 |
diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/routing.md b/docs-site/src/content/docs/zh-cn/reference/configuration/routing.md
index 166a2ce17b..11a3ad97ce 100644
--- a/docs-site/src/content/docs/zh-cn/reference/configuration/routing.md
+++ b/docs-site/src/content/docs/zh-cn/reference/configuration/routing.md
@@ -64,6 +64,7 @@ Codex Auth 页面将此 picker 行为作为选择加入项。关闭它会隐藏
| `strategy?` | `"failover" \| "round-robin"` | `"failover"` | 选择策略。目标顺序表示故障切换优先级;权重会影响平滑加权轮询。 |
| `stickyLimit?` | `number` | `1` | 在单个轮询批次中保留的成功请求数。范围 1–100。 |
| `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | 仅在调用方省略 effort 且所选目标声明了请求的档位时应用。 |
+| `imageInput?` | `"auto" \| "disabled"` | `"auto"` | `"auto"` 仅在每个目标都支持图片时发布图片能力;`"disabled"` 强制仅文本(从对外能力中去掉图片,并在分发前拒绝带图请求)。 |
| `alias?` | `string` | — | 可选的公开 model id,用于替代规范化的选择器 slug。 |
| `nativeAlias?` | `boolean` | `false` | 仅让当前受支持的裸原生 id 对该不带限定前缀的 id 优先;带账号或提供方限定的 OpenAI 路由仍是独立路由。 |
| `displayName?` | `string` | — | 仅用于 catalog 展示的标签;native alias 必须提供非空值。 |
diff --git a/docs-site/src/content/docs/zh-tw/guides/integrations.md b/docs-site/src/content/docs/zh-tw/guides/integrations.md
index 0f9de23e2f..944c6d20fd 100644
--- a/docs-site/src/content/docs/zh-tw/guides/integrations.md
+++ b/docs-site/src/content/docs/zh-tw/guides/integrations.md
@@ -1,9 +1,9 @@
---
title: 整合
-description: 從儀表板把 opencodex 連接到 OpenCode、Pi、OMP、Hermes、OpenClaw、Kimi Code、Gajae Code 與 DeepSeek Harness——每個客戶端一個開關,每次寫入前都會先備份。
+description: 從儀表板把 opencodex 連接到 OpenCode、Pi、OMP、Hermes、OpenClaw、Kimi Code、Gajae Code、DeepSeek Harness 與 MiniMax Code——每個客戶端一個開關,每次寫入前都會先備份。
---
-**整合(Integrations)** 分頁會把 opencodex 的 provider 區塊寫入客戶端自己的設定檔,也會把它移除。共有八個客戶端以這種方式運作,每個都有一個開關:
+**整合(Integrations)** 分頁會把 opencodex 的 provider 區塊寫入客戶端自己的設定檔,也會把它移除。共有九個客戶端以這種方式運作,每個都有一個開關:
| 客戶端 | 設定檔 | 格式 | 變更生效時機 | 憑證 |
|---|---|---|---|---|
@@ -15,6 +15,7 @@ description: 從儀表板把 opencodex 連接到 OpenCode、Pi、OMP、Hermes、
| Kimi Code | `~/.kimi-code/config.toml` | TOML | 重新啟動時,或 `/reload` | loopback 佔位符 |
| Gajae Code | `~/.gjc/agent/models.yml` | YAML | 新 sessions,或當你開啟 `/model` 時 | `OPENCODEX_GAJAE_API_KEY` |
| DeepSeek Harness (DSH) | `$DSH_HOME/settings.yaml`(預設 `~/.dsh/settings.yaml`) | YAML | 熱重載 | 非秘密的 loopback bearer 佔位符 |
+| MiniMax Code | `~/.minimax/config.yaml` | YAML | 新 sessions,或開啟模型選擇器後 | loopback 佔位符 |
受管理 DSH 支援的相容性下限是 **DSH 0.1.0-rc.6**。OpenCodex 只擁有
`llm-pi-ai.providers.opencodex`:Apply 與 Refresh 會取代該片段,Disable 只移除該片段,
@@ -22,6 +23,11 @@ Restore 則放回已記錄的快照。DSH 會熱重載 provider 變更。這些
預設模型,也不會改動原生 `deepseek-official` provider。受管理 DSH 整合目前僅支援
loopback,而且絕不會寫入真實憑證。
+MiniMax Code 依序遵循 `MINIMAX_DATA_DIR`、`MAVIS_DATA_DIR`,最後才回退到
+`~/.minimax`。其受管理區塊只擁有 `custom_provider.opencodex`,不會變更
+`defaultModel`、MiniMax 憑證來源或使用者的 MiniMax 登入。連接後請在 MCode
+中選擇 `custom_provider:opencodex/`。
+
路徑遵循客戶端自己的環境覆寫(environment override)。對 OMP 而言,`OMP_PROFILE` 以存在與否優先於 `PI_PROFILE`,即使明確為空也一樣。具名 profile 會把 `PI_CONFIG_DIR` 當作相對於使用者家目錄的目錄名稱,並忽略 `PI_CODING_AGENT_DIR`;沒有具名 profile 時,`PI_CODING_AGENT_DIR` 勝出。OMP 支援 provider 層級的 headers,但這個最初的整合刻意只支援 loopback;遠端 `x-opencodex-api-key` 的連線設定被延後。搬移過的 `HERMES_HOME`、`KIMI_CODE_HOME` 與 `XDG_CONFIG_HOME` 路徑同樣會被遵循,而非猜測。表格列出每個客戶端的預設值。
對原生 OpenAI 模型,產生的 OMP 區塊會選用其模型層級的 Responses API,保留圖片輸入與 reasoning-effort 控制。路由模型則維持 provider 的 Chat Completions 方言,讓它們既有的 adapters 保持相容。
@@ -44,7 +50,7 @@ opencodex 從自己的環境讀取這些變數。如果你的 gateway 以 profil
- **Restore this point…** 會出現在較舊的操作上,或當檔案在那次操作之後有變更時。跨過這樣的變更做回復會再詢問一次,才覆蓋你的較新編輯——並且也會備份它們,所以那次的回復本身也可以復原。
- 每個客戶端保留十份備份。超過之後,最舊的快照檔案會被移除,其歷史列顯示為 **Backup expired**。
-停用只移除 opencodex 記錄為自己寫入的條目。如果你的檔案在我們寫入之後有變更,後續行為取決於我們自己的條目是否完好,以及檔案的格式。對於嚴格 JSON 設定檔(OpenCode、Pi),在我們的區塊**旁邊**進行的編輯——例如新增 MCP 伺服器或你自己的 provider——會顯示為**需要更新**:重新整理會在保留你的條目的前提下合併寫入,但格式可能會被正規化。例外情況是 JSON 無法精確重寫的內容——例如 `1e999` 這類非有限數字、重寫會被四捨五入的數字(極大的整數,或小到會塌縮成零的數字)、`-0`、同一個物件裡重複出現的鍵,或巢狀層數超過 1000 層——此時開關會鎖定,確保沒有任何值被悄悄改動或刪除。**OMP** 同樣不受旁邊編輯影響,但原因不同:它的 writer 只逐位元組修補自己的 `providers.opencodex` 範圍,檔案其餘部分從不會被重寫。至於其餘可以包含註解的格式(Hermes、OpenClaw、Kimi Code、Gajae Code——以整份文件寫出的 YAML、JSON5 與 TOML),或當我們自己的條目被編輯過時,開關會鎖定,停用會拒絕執行,而不是猜測哪些編輯是你的。
+停用只移除 opencodex 記錄為自己寫入的條目。如果你的檔案在我們寫入之後有變更,後續行為取決於我們自己的條目是否完好,以及檔案的格式。對於嚴格 JSON 設定檔(OpenCode、Pi),在我們的區塊**旁邊**進行的編輯——例如新增 MCP 伺服器或你自己的 provider——會顯示為**需要更新**:重新整理會在保留你的條目的前提下合併寫入,但格式可能會被正規化。例外情況是 JSON 無法精確重寫的內容——例如 `1e999` 這類非有限數字、重寫會被四捨五入的數字(極大的整數,或小到會塌縮成零的數字)、`-0`、同一個物件裡重複出現的鍵,或巢狀層數超過 1000 層——此時開關會鎖定,確保沒有任何值被悄悄改動或刪除。**OMP** 同樣不受旁邊編輯影響,但原因不同:它的 writer 只逐位元組修補自己的 `providers.opencodex` 範圍,檔案其餘部分從不會被重寫。至於其餘可以包含註解的格式(Hermes、OpenClaw、Kimi Code、Gajae Code、MiniMax Code——以整份文件寫出的 YAML、JSON5 與 TOML),或當我們自己的條目被編輯過時,開關會鎖定,停用會拒絕執行,而不是猜測哪些編輯是你的。
## 誠實的預期
@@ -52,7 +58,7 @@ opencodex 從自己的環境讀取這些變數。如果你的 gateway 以 profil
**如果某個值無法忠實重寫,開關會拒絕執行。** 往返覆蓋這些格式在實務上會用到的值種類;當它做不到時——例如使用 `inf` 或 `nan` 的 TOML 檔案,我們可用的 parser 無法準確讀回——套用會停止並說明,而不是寫入被改動的值然後宣稱成功。你會看到檔案被指名,磁碟上沒有任何東西被移動。手動編輯那個檔案仍然有效;只有我們的自動重寫會拒絕。
-**Pi、Kimi Code、Gajae Code 與受管理 DSH 整合只能對 loopback bind 運作。** 前三者的設定沒有非 loopback bind 所需的 `x-opencodex-api-key` header 欄位。DSH 雖然提供通用 headers map,但 rc.6 並未把這個專用准入 header 記錄為受支援的整合契約,因此受管理 writer 會選擇安全拒絕,而不自行猜測。請改用 SSH tunnel,或由本機 forwarder 加上該 header 後再以 loopback 存取。
+**Pi、Kimi Code、Gajae Code、MiniMax Code 與受管理 DSH 整合只能對 loopback bind 運作。** 前四者的設定沒有非 loopback bind 所需的 `x-opencodex-api-key` header 欄位。DSH 雖然提供通用 headers map,但 rc.6 並未把這個專用准入 header 記錄為受支援的整合契約,因此受管理 writer 會選擇安全拒絕,而不自行猜測。請改用 SSH tunnel,或由本機 forwarder 加上該 header 後再以 loopback 存取。
**產生的 OMP 整合也刻意只支援 loopback。** OMP 確實支援 provider 層級的 headers,但這個最初的整合不會發出遠端 `x-opencodex-api-key` 憑證連線。手動的遠端 OMP 設定目前不在受管理的整合範圍內。
@@ -72,6 +78,25 @@ ocx integration client history --client hermes
ocx integration client restore --op [--confirm-drift]
```
+MiniMax Code 先連接一次 provider,再透過會檢查設定的 launcher 啟動:
+
+```bash
+ocx integration client enable --client mcode
+ocx mcode
+```
+
+另一個 MiniMax 平台 CLI(`mmx`)不是檔案開關整合。其文字命令使用 MiniMax 的
+Anthropic 相容端點,因此 OpenCodex 提供憑證隔離、僅限 loopback 的 launcher:
+
+```bash
+ocx mmx text chat --model anthropic/claude-opus-5 --message "Hello"
+ocx mmx text repl --model openai/gpt-5.6-sol
+```
+
+只有 `mmx text chat` 與 `mmx text repl` 會經過 proxy。MiniMax 原生的其他指令請直接
+執行 `mmx`。wrapper 使用只含非機密 loopback 佔位符的暫存設定,不會讀取 `~/.mmx`
+OAuth 或 API key,並拒絕 `--api-key`、`--base-url` 與 `--region` 覆寫。
+
`--confirm-drift` 永遠不會被擅自假設。如果檔案在你正要回復的操作之後有變更,指令會拒絕並告訴你,因為覆蓋你較新的編輯是你的決定。
客戶端細節是針對各專案自己的設定格式驗證過的;檢查了什麼、何時檢查,請見 `devlog/_fin/260802_client_toggle_api/002_client_toggle_matrix.md` 中的研究筆記。
diff --git a/gui/src/combo-capabilities.ts b/gui/src/combo-capabilities.ts
new file mode 100644
index 0000000000..32f91f66b9
--- /dev/null
+++ b/gui/src/combo-capabilities.ts
@@ -0,0 +1,14 @@
+import type { ComboTarget } from "./combo-workspace-data";
+import type { ModelOption } from "./components/combo-workspace-types";
+
+/** Whether every selected target advertises image input (incomplete rows fail closed). */
+export function comboImagesSupported(targets: ComboTarget[], models: ModelOption[]): boolean {
+ if (targets.length === 0) return false;
+ return targets.every((target) => {
+ const provider = target.provider.trim();
+ const modelId = target.model.trim();
+ if (!provider || !modelId) return false;
+ const model = models.find((row) => row.provider === provider && row.id === modelId);
+ return !!model?.inputModalities?.includes("image");
+ });
+}
diff --git a/gui/src/combo-workspace-data.ts b/gui/src/combo-workspace-data.ts
index 56ce088238..4f5e96e0b7 100644
--- a/gui/src/combo-workspace-data.ts
+++ b/gui/src/combo-workspace-data.ts
@@ -60,6 +60,11 @@ export function newComboTarget(partial: Partial = {}): ComboTarget
};
}
+
+function normalizeImageInput(value: unknown): "auto" | "disabled" {
+ return value === "disabled" ? "disabled" : "auto";
+}
+
export interface ComboItem {
id: string;
/** Wire id shown to clients, e.g. combo/free */
@@ -73,6 +78,7 @@ export interface ComboItem {
strategy: ComboStrategy;
stickyLimit: number;
defaultEffort: ComboEffort | null;
+ imageInput?: "auto" | "disabled";
targets: ComboTarget[];
}
@@ -177,6 +183,7 @@ export function parseComboList(payload: unknown): ComboItem[] {
strategy: normalizeStrategy(r.strategy),
stickyLimit: normalizeStickyLimit(r.stickyLimit),
defaultEffort: normalizeDefaultEffort(r.defaultEffort),
+ imageInput: normalizeImageInput(r.imageInput),
targets,
});
}
@@ -236,6 +243,7 @@ export function draftEquals(a: ComboItem, b: ComboItem): boolean {
|| a.strategy !== b.strategy
|| a.stickyLimit !== b.stickyLimit
|| a.defaultEffort !== b.defaultEffort
+ || (a.imageInput ?? "auto") !== (b.imageInput ?? "auto")
) return false;
if (a.targets.length !== b.targets.length) return false;
return a.targets.every((t, i) => {
@@ -252,6 +260,7 @@ export function toPutBody(item: ComboItem, options: { renameFrom?: string } = {}
strategy: ComboStrategy;
stickyLimit?: number;
defaultEffort: ComboEffort | null;
+ imageInput?: "disabled";
alias?: string;
nativeAlias?: true;
displayName?: string;
@@ -266,6 +275,7 @@ export function toPutBody(item: ComboItem, options: { renameFrom?: string } = {}
: { provider: target.provider.trim(), model: target.model.trim() }),
strategy: item.strategy,
defaultEffort: item.defaultEffort,
+ ...(item.imageInput === "disabled" ? { imageInput: "disabled" as const } : {}),
...(item.strategy === "round-robin" ? { stickyLimit: item.stickyLimit } : {}),
...(item.alias && item.alias.trim() ? { alias: item.alias.trim() } : {}),
...(item.nativeAlias ? { nativeAlias: true } : {}),
@@ -372,6 +382,7 @@ export function emptyDraft(id = ""): ComboItem {
strategy: "failover",
stickyLimit: 1,
defaultEffort: null,
+ imageInput: "auto",
targets: [newComboTarget()],
};
}
diff --git a/gui/src/components/apikeys-workspace/client-config-clients.ts b/gui/src/components/apikeys-workspace/client-config-clients.ts
index d1527a5b13..2143b999f0 100644
--- a/gui/src/components/apikeys-workspace/client-config-clients.ts
+++ b/gui/src/components/apikeys-workspace/client-config-clients.ts
@@ -8,7 +8,7 @@
* with EXPORT_CLIENT_IDS by hand; adding a client server-side renders no row
* until this tuple changes.
*/
-export const CLIENTS = ["opencode", "pi", "omp", "hermes", "openclaw", "kimi", "gajae", "dsh"] as const;
+export const CLIENTS = ["opencode", "pi", "omp", "hermes", "openclaw", "kimi", "gajae", "dsh", "mcode"] as const;
export type ExportClientId = (typeof CLIENTS)[number];
export const CLIENT_LABEL_KEYS = {
@@ -20,6 +20,7 @@ export const CLIENT_LABEL_KEYS = {
kimi: "api.clientConfig.clientKimi",
gajae: "api.clientConfig.clientGajae",
dsh: "api.clientConfig.clientDsh",
+ mcode: "api.clientConfig.clientMcode",
} as const;
/**
diff --git a/gui/src/components/combo-workspace-add-modal.tsx b/gui/src/components/combo-workspace-add-modal.tsx
index cff828c5c8..4d334a759f 100644
--- a/gui/src/components/combo-workspace-add-modal.tsx
+++ b/gui/src/components/combo-workspace-add-modal.tsx
@@ -10,7 +10,7 @@ import { IconX } from "../icons";
import { useT } from "../i18n/shared";
import { Notice } from "../ui";
import type { ModelOption, ProviderOption } from "./combo-workspace-types";
-import { EffortSelect, StrategySeg, TargetEditor } from "./combo-workspace-controls";
+import { ComboCapabilities, EffortSelect, StrategySeg, TargetEditor } from "./combo-workspace-controls";
import { clampedNumberInput } from "./combo-workspace-utils";
export function AddComboModal({
@@ -204,6 +204,13 @@ export function AddComboModal({
{draft.strategy === "failover" ? t("cws.targets.failoverHint") : t("cws.targets.roundRobinHint")}
+ setDraft((d) => ({ ...d, ...patch }))}
+ />
diff --git a/gui/src/components/combo-workspace-controls.tsx b/gui/src/components/combo-workspace-controls.tsx
index 8a7277fd48..0a7ecaa65d 100644
--- a/gui/src/components/combo-workspace-controls.tsx
+++ b/gui/src/components/combo-workspace-controls.tsx
@@ -1,8 +1,10 @@
import { useState } from "react";
import type { ComboEffort, ComboStrategy, ComboTarget } from "../combo-workspace-data";
+import { comboImagesSupported } from "../combo-capabilities";
import { COMBO_EFFORTS, newComboTarget } from "../combo-workspace-data";
import { IconArrowDown, IconArrowUp, IconGrip, IconPlus, IconTrash } from "../icons";
import { useT } from "../i18n/shared";
+import { Switch } from "../ui";
import { formatProviderDisplayName } from "../provider-icons";
import type { ModelOption, ProviderOption } from "./combo-workspace-types";
import { clampedNumberInput, enabledProviders, modelsForProvider } from "./combo-workspace-utils";
@@ -83,6 +85,49 @@ export function EffortSelect({
);
}
+
+export function ComboCapabilities({
+ targets,
+ models,
+ imageInput,
+ disabled,
+ onChange,
+}: {
+ targets: ComboTarget[];
+ models: ModelOption[];
+ imageInput: "auto" | "disabled";
+ disabled?: boolean;
+ onChange: (patch: { imageInput?: "auto" | "disabled" }) => void;
+}) {
+ const t = useT();
+ const imagesSupported = comboImagesSupported(targets, models);
+ // Default: checked (auto) when supported; force off when any target lacks image.
+ const effectiveOn = imagesSupported && imageInput !== "disabled";
+
+ return (
+
+ {t("cws.capabilities")}
+