diff --git a/desktop/src-tauri/src/provider_icons.rs b/desktop/src-tauri/src/provider_icons.rs
index 2799453c795..94b4b4d6010 100644
--- a/desktop/src-tauri/src/provider_icons.rs
+++ b/desktop/src-tauri/src/provider_icons.rs
@@ -56,6 +56,7 @@ const ALIASES: &[(&str, &str)] = &[
("meta-model", "meta.svg"),
("meta-muse", "meta.svg"),
("mistral", "mistral-color.svg"),
+ ("mirasim", "mirasim.svg"),
("minimax", "minimax.svg"),
("minimax-cn", "minimax.svg"),
("moonshot", "moonshot-color.svg"),
@@ -182,6 +183,7 @@ fn svg(file: &str) -> Option<&'static str> {
"meta.svg" => svg!("meta.svg"),
"minimax.svg" => svg!("minimax.svg"),
"mistral-color.svg" => svg!("mistral-color.svg"),
+ "mirasim.svg" => svg!("mirasim.svg"),
"moonshot-color.svg" => svg!("moonshot-color.svg"),
"nanogpt.svg" => svg!("nanogpt.svg"),
"nebius.svg" => svg!("nebius.svg"),
diff --git a/devlog/_plan/260921_mirasim_provider/010_provider_design.md b/devlog/_plan/260921_mirasim_provider/010_provider_design.md
new file mode 100644
index 00000000000..22dc767da8b
--- /dev/null
+++ b/devlog/_plan/260921_mirasim_provider/010_provider_design.md
@@ -0,0 +1,85 @@
+# Mirasim provider native port
+
+## Goal
+
+Port the current `cpa-plugin-mirasim` protocol into OpenCodex as a native provider without embedding CLIProxyAPI or its native plugin ABI.
+
+Reference implementation pinned for this port:
+
+- `KIDA-MNESIA/cpa-plugin-mirasim` v1.1.0 / `857a984`
+- OpenCodex base after the final pre-port fast-forward: `origin/dev` / `a49974639`
+
+## Architecture
+
+```text
+Codex / Claude Code
+ |
+ v
+OpenCodex router
+ |
+ v
+Mirasim adapter
+ | |
+ | +-- GPT -> existing OpenAI Responses serializer/parser
+ |
+ +-- Claude -> existing Anthropic serializer/parser
+ |
+ v
+Mirasim transport
+ - OAuth access/refresh
+ - Ed25519 device identity
+ - device ticket mint/cache
+ - mrs-sig-v2 request signing
+ - mrs-seal-v1 inference metadata envelope
+ - HTTP/1.1 pin through OpenCodex provider transport
+ |
+ v
+relay.mirasim.ai
+```
+
+The provider composes existing protocol translators. It does not fork Anthropic Messages or OpenAI Responses translation.
+
+## Wire invariants
+
+1. Device/control-plane requests are signed with `mrs-sig-v2`, empty metadata and no sealed envelope.
+2. Inference requests add session/agent/call metadata, include it in the v2 signature, then seal all Mirasim metadata except `x-mirasim-client` into `x-mirasim-enc`.
+3. Relay metadata uses X25519 + HKDF-SHA256 + ChaCha20-Poly1305 (`mrs-seal-v1`).
+4. Device tickets come from `POST /v1/device/session`; 404 falls back to access-token signing for one minute, 501 for fifteen minutes.
+5. GPT models use `/v1/responses`; Claude models use `/v1/messages`.
+6. Existing OAuth account selection must bind access token and device private key from the same stored credential.
+7. Mirasim requests pin OpenCodex upstream transport to HTTP/1.1. Lower-case header spelling should be verified separately at wire level because fetch APIs may canonicalize names.
+8. Auxiliary inference routes reuse the same signed/sealed transport: Claude `/v1/messages/count_tokens`, GPT `/v1/alpha/search`, and GPT `/v1/responses/compact`.
+9. A Claude `[1m]` selector is local routing syntax only. The relay receives the bare model id plus the deduplicated `context-1m-2025-08-07` beta.
+10. Signed account roster fields are authoritative over the static fallback catalog and remain account/device-scoped across access-token rotation.
+
+## Delivery order
+
+1. Golden-vector crypto parity.
+2. OAuth credential shape, login, refresh and protected device key persistence.
+3. Device-ticket and signed/sealed transport.
+4. Dual-wire adapter and registry preset.
+5. Static fallback catalog.
+6. Signed dynamic `/v1/models` + `/v1/model-roster`. **Implemented.**
+7. Provider quota via `/v1/limits`. **Implemented.**
+8. Native `/v1/responses/compact`. **Implemented.**
+9. Claude `count_tokens`, GPT `alpha/search`, and `[1m]` selector parity. **Implemented.**
+10. Browser OAuth plus CLI-only email-code login (`--email` / optional `--code`), refresh, and best-effort `/auth/me` identity enrichment. **Implemented.**
+11. Wire capture parity and real Claude/Codex acceptance tests. **Synthetic wire tests implemented; real account E2E requires a Mirasim login and is currently blocked because the local auth store has no Mirasim account.**
+
+## Safety boundaries
+
+- Device private keys remain only in `~/.opencodex/auth.json`; management/status projections never expose them.
+- Caller-supplied `x-mirasim-*`, authorization and proxy authorization headers are stripped before signing.
+- `x-mirasim-probe` is the only provider-owned control header allowlisted after signing; callers cannot inject it through the ordinary request surface.
+- The request transport reuses OpenCodex's provider-scoped fetch executor so proxy, egress, timeout and HTTP-version policy stay centralized.
+- Protocol golden vectors from the Go reference are the compatibility oracle.
+
+## Intentional host-lifecycle difference
+
+The CPA plugin asks its host to re-enter refresh every five minutes so it can poll `/auth/me`
+for subscription-plan drift. OpenCodex's OAuth resolver has no provider-specific periodic refresh
+scheduler; adding one solely for Mirasim would leak CPA host semantics into the shared OAuth
+lifecycle. The native port therefore keeps `/auth/me` as best-effort login identity enrichment,
+uses normal expiry refresh, and force-refreshes the OAuth snapshot after authenticated relay 401s.
+The signed live model/roster cache is account/device-scoped rather than access-token-scoped, so a
+normal token rotation does not lose the observed Claude thinking shape.
diff --git a/docs-site/src/content/docs/fr/getting-started/quickstart.md b/docs-site/src/content/docs/fr/getting-started/quickstart.md
index cad99b993ba..a3eb2f9fea0 100644
--- a/docs-site/src/content/docs/fr/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/fr/getting-started/quickstart.md
@@ -13,7 +13,7 @@ ocx init
`ocx init` vous accompagne dans les étapes suivantes :
-1. **Choix d’un fournisseur** — sélectionnez l’un des 103 préréglages intégrés au registre, ou `custom` pour saisir une
+1. **Choix d’un fournisseur** — sélectionnez l’un des 104 préréglages intégrés au registre, ou `custom` pour saisir une
URL de base et un adaptateur.
2. **Clé API** — collez une clé ou référencez une variable d’environnement telle que `${ANTHROPIC_API_KEY}`.
3. **Modèle par défaut** — pour les fournisseurs clés, locaux et personnalisés, acceptez le préréglage ou saisissez un identifiant de modèle.
diff --git a/docs-site/src/content/docs/fr/guides/providers.md b/docs-site/src/content/docs/fr/guides/providers.md
index 8a928f1b299..193f37a9ea6 100644
--- a/docs-site/src/content/docs/fr/guides/providers.md
+++ b/docs-site/src/content/docs/fr/guides/providers.md
@@ -295,7 +295,7 @@ existante n'est pas concernée.
## 3. Catalogue des clés API
-opencodex fournit 103 préréglages intégrés : 84 à clé, 15 OAuth, trois locaux et un préréglage par défaut de
+opencodex fournit 104 préréglages intégrés : 84 à clé, 16 OAuth, trois locaux et un préréglage par défaut de
transfert ChatGPT. Dans le tableau de bord, le sélecteur **Ajouter un fournisseur** ouvre le tableau de bord du
fournisseur à clé, valide la clé et l'enregistre ; la validation dépend du fournisseur. Parmi les entrées notables :
diff --git a/docs-site/src/content/docs/getting-started/quickstart.md b/docs-site/src/content/docs/getting-started/quickstart.md
index 41e4485ce6e..99ff1a1d5d7 100644
--- a/docs-site/src/content/docs/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/getting-started/quickstart.md
@@ -18,7 +18,7 @@ ocx init
`ocx init` walks you through:
-1. **Pick a provider** — choose one of the 103 built-in registry presets or `custom` to type a base
+1. **Pick a provider** — choose one of the 104 built-in registry presets or `custom` to type a base
URL and adapter.
2. **API key** — paste a key, or reference an environment variable like `${ANTHROPIC_API_KEY}`.
3. **Default model** — for key, local, and custom providers, accept the preset or enter a model id.
diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md
index 2bbdeb0ed4f..9929c9a700a 100644
--- a/docs-site/src/content/docs/guides/providers.md
+++ b/docs-site/src/content/docs/guides/providers.md
@@ -185,6 +185,7 @@ that route.
```bash
ocx login xai # xAI Grok
ocx login anthropic # Anthropic Claude (Pro/Max)
+ocx login mirasim # Mirasim browser login; use --email
for email verification
ocx login kimi # Moonshot Kimi
ocx login nous # Nous Portal (device grant; free + paid models)
ocx login kiro # import kiro-cli credentials (or token fallback)
@@ -203,6 +204,7 @@ ocx logout
| --- | --- | --- | --- |
| `xai` | `openai-chat` | `https://cli-chat-proxy.grok.com/v1` | OAuth uses the separate Grok CLI subscription gateway. The API-key override uses `https://api.x.ai/v1` and may inject Priority Processing. Live-first Grok catalog; `grok-4.5` is the fallback default. |
| `anthropic` | `anthropic` | `https://api.anthropic.com` | Claude models; live model list fetched from `/v1/models`. |
+| `mirasim` | `mirasim` | `https://relay.mirasim.ai` | Native signed Mirasim account provider. GPT-family models and `kimi-k3` use Responses semantics; Claude-family models use Messages semantics. The signed account roster supplies live model availability/context limits, and the Providers page shows Mirasim account quota windows. |
| `kimi` | `openai-chat` | `https://api.kimi.com/coding/v1` | Kimi Code Plan coding models. Defaults to the stable `kimi-for-coding` alias (currently K2.8 Preview): 1M-token context window, adjustable `low`/`high`/`max` thinking (default `max`), text + image input. Retired `kimi-k2.x` selections are migrated to the alias on upgrade. |
| `kimi-responses` | `openai-responses` | `https://api.kimi.com/coding/v1` | Same Kimi account login (reuses the `kimi` OAuth credential) over the OpenAI Responses wire. Same model roster and capabilities as `kimi`; thinking content stays encrypted server-side, tool calls and results stay visible. |
| `nous` | `openai-chat` | `https://inference-api.nousresearch.com/v1` | Nous Research subscription gateway (same backend Hermes Agent uses). Device-grant login against `portal.nousresearch.com`; the access token is the per-request inference JWT. Mixed paid + `:free` model catalog (`tencent/hy3:free`, `stepfun/step-3.7-flash:free`, ...) discovered live from the signed-in account. Refresh tokens are single-use and rotated on every refresh. |
@@ -216,6 +218,8 @@ ocx logout
Google Antigravity groups a model's complete `low` / `medium` / `high` variants into one entry with an effort selector. This includes Claude Opus and Sonnet 5.5 and future versions discovered during automatic refresh. Partial sets remain separate. Existing enabled/disabled choices carry into the grouped entry; genuinely new models still follow your New model policy. Saved suffix IDs continue to request their original tier. Grouped effort routing survives a proxy restart when model discovery is temporarily unavailable. A running Codex session may need to reload its model list after catalog refresh. Usage groups known Claude Sonnet and Opus 5.5 tier IDs under their base model, including historical tier records. Their displayed costs are derived Anthropic reference estimates, not Antigravity subscription charges.
+For Mirasim email login, `ocx login mirasim --email ` prompts for the one-time code locally. For non-interactive use, pass `--code -` and supply the code on standard input rather than placing it in the process arguments. Claude selectors may append `[1m]` when the signed roster advertises the long-context variant; OpenCodex removes that selector before dispatch and applies the corresponding Claude beta.
+
Google Antigravity account and provider quota probes use fixed Google accounting endpoints, including the models fallback. They support transparent Fake-IP DNS for those destinations while retaining TLS verification, redirect rejection and private-address checks. A custom provider base URL changes model requests, not quota destinations; `NO_PROXY` continues to select the direct-route policy.
### Google tool-schema loss diagnostics
@@ -517,7 +521,7 @@ The account list marks Kiro accounts excluded from automatic selection with a re
## 3. API-key catalog
-opencodex ships 103 built-in presets: 84 key-based, 15 OAuth, three local, and one default
+opencodex ships 104 built-in presets: 84 key-based, 16 OAuth, three local, and one default
ChatGPT-forward preset. The dashboard's **Add provider** picker opens a key provider's dashboard,
validates the key, and stores it; validation is provider-specific. Notable entries:
diff --git a/docs-site/src/content/docs/ja/getting-started/quickstart.md b/docs-site/src/content/docs/ja/getting-started/quickstart.md
index 500c89a2f65..2294fc1d541 100644
--- a/docs-site/src/content/docs/ja/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/ja/getting-started/quickstart.md
@@ -18,7 +18,7 @@ ocx init
`ocx init` では次の手順を説明します。
-1. **プロバイダーを選択してください** — 103 個の組み込みレジストリプリセットのいずれか、または `custom` を選択してベース URL とアダプターを入力します。
+1. **プロバイダーを選択してください** — 104 個の組み込みレジストリプリセットのいずれか、または `custom` を選択してベース URL とアダプターを入力します。
2. **API キー** — キーを貼り付けるか、`${ANTHROPIC_API_KEY}` のような環境変数を参照します。
3. **デフォルト モデル** — キー、ローカル、カスタム プロバイダーの場合は、プリセットを受け入れるか、モデル ID を入力します。
4. **プロキシ ポート** — デフォルトは `10100` です。
diff --git a/docs-site/src/content/docs/ja/guides/providers.md b/docs-site/src/content/docs/ja/guides/providers.md
index a499d77d404..5ebb8d08d5b 100644
--- a/docs-site/src/content/docs/ja/guides/providers.md
+++ b/docs-site/src/content/docs/ja/guides/providers.md
@@ -193,7 +193,7 @@ Kiro のログインには Kiro CLI が必要です。Unix では `curl -fsSL ht
## 3. API キーカタログ
-opencodex には組み込みプリセットが 103 個含まれています。キー方式 84、OAuth 15、ローカル 3、
+opencodex には組み込みプリセットが 104 個含まれています。キー方式 84、OAuth 16、ローカル 3、
デフォルト ChatGPT 転送プリセット 1 です。ダッシュボードの **Add provider** ピッカーはキー発行ページを開き、
入力したキーを検証した後保存します(検証はプロバイダー固有です)。主な項目は以下のとおりです:
diff --git a/docs-site/src/content/docs/ko/getting-started/quickstart.md b/docs-site/src/content/docs/ko/getting-started/quickstart.md
index 22653fc9b91..2c06b538f7f 100644
--- a/docs-site/src/content/docs/ko/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/ko/getting-started/quickstart.md
@@ -18,7 +18,7 @@ ocx init
`ocx init`은 다음 과정을 안내합니다:
-1. **프로바이더 선택** — 내장 레지스트리 프리셋 103개 중 하나를 고르거나 `custom`을 선택해 base URL과 adapter를 직접 입력합니다.
+1. **프로바이더 선택** — 내장 레지스트리 프리셋 104개 중 하나를 고르거나 `custom`을 선택해 base URL과 adapter를 직접 입력합니다.
2. **API 키** — 키를 붙여넣거나 `${ANTHROPIC_API_KEY}` 같은 환경 변수를 참조합니다.
3. **기본 모델** — 키, 로컬, custom 프로바이더에서는 프리셋을 그대로 쓰거나 모델 ID를 직접 입력합니다.
4. **프록시 포트** — 기본값은 `10100`입니다.
diff --git a/docs-site/src/content/docs/ko/guides/providers.md b/docs-site/src/content/docs/ko/guides/providers.md
index 3fda5134b55..9c1ef41b09c 100644
--- a/docs-site/src/content/docs/ko/guides/providers.md
+++ b/docs-site/src/content/docs/ko/guides/providers.md
@@ -190,7 +190,7 @@ Kiro 로그인에는 Kiro CLI가 필요합니다. Unix에서는 `curl -fsSL http
## 3. API 키 카탈로그
-opencodex에는 빌트인 프리셋이 103개 들어 있습니다. 키 방식 84개, OAuth 15개, 로컬 3개,
+opencodex에는 빌트인 프리셋이 104개 들어 있습니다. 키 방식 84개, OAuth 16개, 로컬 3개,
기본 ChatGPT 포워드 프리셋 1개입니다. 대시보드의 **Add provider** 선택기는 키 발급 페이지를 열고,
입력한 키를 검증한 뒤 저장합니다(검증은 프로바이더별로 다릅니다). 주요 항목은 다음과 같습니다:
diff --git a/docs-site/src/content/docs/ru/getting-started/quickstart.md b/docs-site/src/content/docs/ru/getting-started/quickstart.md
index 16108d8d67a..67ff8415a92 100644
--- a/docs-site/src/content/docs/ru/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/ru/getting-started/quickstart.md
@@ -18,7 +18,7 @@ ocx init
`ocx init` проведёт вас по следующим шагам:
-1. **Выбор провайдера** — выберите один из 103 встроенных пресетов реестра или `custom`, чтобы
+1. **Выбор провайдера** — выберите один из 104 встроенных пресетов реестра или `custom`, чтобы
ввести базовый URL и адаптер вручную.
2. **API-ключ** — вставьте ключ или сошлитесь на переменную окружения вида `${ANTHROPIC_API_KEY}`.
3. **Модель по умолчанию** — для провайдеров с ключом, локальных и `custom` примите значение из
diff --git a/docs-site/src/content/docs/ru/guides/providers.md b/docs-site/src/content/docs/ru/guides/providers.md
index 7317ddafbc4..8d0d6e8c981 100644
--- a/docs-site/src/content/docs/ru/guides/providers.md
+++ b/docs-site/src/content/docs/ru/guides/providers.md
@@ -206,7 +206,7 @@ Inline JSON и лишние позиционные аргументы откло
## 3. Каталог API-ключей
-opencodex поставляется с 103 встроенными пресетами: 84 на основе ключей, 15 OAuth, три локальных и
+opencodex поставляется с 104 встроенными пресетами: 84 на основе ключей, 16 OAuth, три локальных и
один пресет ChatGPT-форварда по умолчанию. Селектор **Add provider** в дашборде открывает страницу
выдачи ключей провайдера, проверяет ключ и сохраняет его; проверка зависит от провайдера.
Наиболее заметные записи:
diff --git a/docs-site/src/content/docs/tr/getting-started/quickstart.md b/docs-site/src/content/docs/tr/getting-started/quickstart.md
index 0d707f5e0ee..baa5172c95b 100644
--- a/docs-site/src/content/docs/tr/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/tr/getting-started/quickstart.md
@@ -14,7 +14,7 @@ ocx init
`ocx init` adım adım size rehberlik eder:
-1. **Bir sağlayıcı seçin** — yerleşik kayıt defterindeki 103 önayardan birini
+1. **Bir sağlayıcı seçin** — yerleşik kayıt defterindeki 104 önayardan birini
veya bir temel URL ile adaptör yazmak için `custom` seçeneğini belirleyin.
2. **API anahtarı** — bir anahtar yapıştırın veya `${ANTHROPIC_API_KEY}` gibi
bir ortam değişkenine başvurun.
diff --git a/docs-site/src/content/docs/tr/guides/providers.md b/docs-site/src/content/docs/tr/guides/providers.md
index 75bb1318860..370d42f33f8 100644
--- a/docs-site/src/content/docs/tr/guides/providers.md
+++ b/docs-site/src/content/docs/tr/guides/providers.md
@@ -327,7 +327,7 @@ olmayan bir makineden oturum açmak bundan etkilenmez.
## 3. API anahtarı kataloğu
-opencodex 103 yerleşik önayar ile birlikte gelir: 84 anahtar tabanlı, 15
+opencodex 104 yerleşik önayar ile birlikte gelir: 84 anahtar tabanlı, 16
OAuth, üç yerel ve bir varsayılan ChatGPT iletme önayarı. Kontrol panelinin
**Sağlayıcı ekle** seçicisi bir anahtar sağlayıcısının kontrol panelini açar,
anahtarı doğrular ve saklar; doğrulama sağlayıcıya özgüdür. Dikkate değer
diff --git a/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md b/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md
index c73b77d255f..9d0c51ebf7b 100644
--- a/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md
@@ -18,7 +18,7 @@ ocx init
`ocx init` 会引导你完成:
-1. **选择 provider** — 从内置 registry 的 103 个预设中选择一个,或选择 `custom` 手动输入 base URL 和 adapter。
+1. **选择 provider** — 从内置 registry 的 104 个预设中选择一个,或选择 `custom` 手动输入 base URL 和 adapter。
2. **API key** — 粘贴一个 key,或引用一个环境变量,例如 `${ANTHROPIC_API_KEY}`。
3. **默认模型** — 对于 key、本地和 custom provider,接受预设值或输入模型 id。
4. **代理端口** — 默认为 `10100`。
diff --git a/docs-site/src/content/docs/zh-cn/guides/providers.md b/docs-site/src/content/docs/zh-cn/guides/providers.md
index e4d90fa4495..d56c5b9ea96 100644
--- a/docs-site/src/content/docs/zh-cn/guides/providers.md
+++ b/docs-site/src/content/docs/zh-cn/guides/providers.md
@@ -186,7 +186,7 @@ Kiro 登录需要 Kiro CLI:Unix 使用 `curl -fsSL https://cli.kiro.dev/instal
## 3. API 密钥目录
-opencodex 内置 103 个预设:84 个密钥预设、15 个 OAuth 预设、3 个本地预设,以及 1 个默认的
+opencodex 内置 104 个预设:84 个密钥预设、16 个 OAuth 预设、3 个本地预设,以及 1 个默认的
ChatGPT 转发预设。仪表盘的 **Add provider** 选择器会打开密钥提供商的控制台,验证并保存密钥。
验证因提供商而异。主要条目包括:
diff --git a/docs-site/src/content/docs/zh-tw/getting-started/quickstart.md b/docs-site/src/content/docs/zh-tw/getting-started/quickstart.md
index 0583cbe4473..8a28107b8bc 100644
--- a/docs-site/src/content/docs/zh-tw/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/zh-tw/getting-started/quickstart.md
@@ -13,7 +13,7 @@ ocx init
`ocx init` 會引導你完成:
-1. **選擇 provider** —— 從內建 registry 的 103 個預設中選擇一個,或選擇 `custom` 手動輸入
+1. **選擇 provider** —— 從內建 registry 的 104 個預設中選擇一個,或選擇 `custom` 手動輸入
base URL 和 adapter。
2. **API key** —— 貼上一個 key,或引用一個環境變數,例如 `${ANTHROPIC_API_KEY}`。
3. **預設模型** —— 對於 API key、本機和 custom provider,可接受預設值或輸入模型 id。
diff --git a/docs-site/src/content/docs/zh-tw/guides/providers.md b/docs-site/src/content/docs/zh-tw/guides/providers.md
index 2deea9cb86d..b565cc88553 100644
--- a/docs-site/src/content/docs/zh-tw/guides/providers.md
+++ b/docs-site/src/content/docs/zh-tw/guides/providers.md
@@ -249,7 +249,7 @@ database 並移除目前的 WAL、SHM 與 journal sidecar,再發布先前的 s
## 3. API 金鑰目錄
-opencodex 內建 103 個 preset:84 個 key-based、15 個 OAuth、3 個 local,以及 1 個預設 ChatGPT-forward
+opencodex 內建 104 個 preset:84 個 key-based、16 個 OAuth、3 個 local,以及 1 個預設 ChatGPT-forward
preset。儀表板的 **Add provider** picker 會開啟 key provider 的 dashboard、驗證金鑰並儲存;驗證方式
依 provider 而異。主要條目如下。
diff --git a/gui/public/provider-icons/README.md b/gui/public/provider-icons/README.md
index 0e595d7b4f7..91f20b39c86 100644
--- a/gui/public/provider-icons/README.md
+++ b/gui/public/provider-icons/README.md
@@ -205,6 +205,11 @@ Sourced for the providers that were rendering a coloured initial tile. Every
entry below was fetched from the vendor's own domain, taken from the registry's
`baseUrl`/`dashboardUrl` rather than guessed.
+- `mirasim.svg` — supplied by the operator on 2026-09-21 as a 793x698 RGBA
+ logo image. The committed SVG is a geometric vector reconstruction of that
+ exact black plate / eight white rounded bars so the small provider tile stays
+ sharp; no external brand asset was substituted.
+
Published as SVG and committed with only comments, ``/`` and
`data-name` attributes stripped:
diff --git a/gui/public/provider-icons/mirasim.svg b/gui/public/provider-icons/mirasim.svg
new file mode 100644
index 00000000000..aaab565396a
--- /dev/null
+++ b/gui/public/provider-icons/mirasim.svg
@@ -0,0 +1,14 @@
+
diff --git a/gui/src/components/QuotaBars.tsx b/gui/src/components/QuotaBars.tsx
index fc72b0bb5f3..750ea11bcde 100644
--- a/gui/src/components/QuotaBars.tsx
+++ b/gui/src/components/QuotaBars.tsx
@@ -23,8 +23,11 @@ export type QuotaBarRow = {
* shorter windows first — 5h, weekly, cursor first-party, cursor API, monthly,
* then subscription credits before other custom windows.
*/
+const MIRASIM_MODEL_WEEKLY = /^Model\s*·\s*7d[-_]([a-z0-9][a-z0-9.-]{0,63})$/i;
+
function rawCustomWindowRank(rawLabel: string): number {
if (rawLabel === "5h") return 0;
+ if (MIRASIM_MODEL_WEEKLY.test(rawLabel)) return 1.5;
if (rawLabel === "First-party models") return 2;
if (rawLabel === "API usage") return 3;
if (rawLabel === "Total subscription credits") return 4.5;
@@ -53,6 +56,23 @@ export function isCustomQuotaWindowIncomplete(
}
function localizeCustomQuotaLabel(rawLabel: string, t: TFn): string {
+ const mirasimWeekly = rawLabel.match(MIRASIM_MODEL_WEEKLY);
+ if (mirasimWeekly?.[1]) {
+ const familyId = mirasimWeekly[1].toLowerCase();
+ const knownFamilyKey = ({
+ claude: "quota.modelFamily.claude",
+ fable: "quota.modelFamily.fable",
+ sonnet: "quota.modelFamily.sonnet",
+ opus: "quota.modelFamily.opus",
+ haiku: "quota.modelFamily.haiku",
+ } as const)[familyId as "claude" | "fable" | "sonnet" | "opus" | "haiku"];
+ const family = knownFamilyKey
+ ? t(knownFamilyKey)
+ : familyId.split(/[.-]/).filter(Boolean)
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
+ .join(" ");
+ return `${family} · ${t("quota.weeklyLimit")}`;
+ }
switch (rawLabel) {
case "First-party models":
return t("quota.cursorFirstParty");
diff --git a/gui/src/components/provider-workspace/ProviderAuthPanel.tsx b/gui/src/components/provider-workspace/ProviderAuthPanel.tsx
index da5419c975c..7f1daf32538 100644
--- a/gui/src/components/provider-workspace/ProviderAuthPanel.tsx
+++ b/gui/src/components/provider-workspace/ProviderAuthPanel.tsx
@@ -8,7 +8,11 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { useT } from "../../i18n/shared";
import { IconLock, IconRefresh, IconTrash } from "../../icons";
import type { WorkspaceItem } from "../../provider-workspace/catalog";
-import { oauthAccountDisplayLabel, providerAuthSurface } from "../../provider-workspace/auth";
+import {
+ oauthAccountDisplayLabel,
+ oauthAccountSecondaryIdentity,
+ providerAuthSurface,
+} from "../../provider-workspace/auth";
import { displayAccountId } from "../../lib/privacy";
import AccountAutoSwitchControl from "../AccountAutoSwitchControl";
import {
@@ -561,6 +565,7 @@ export default function ProviderAuthPanel({
const maskedId = displayAccountId(account.id);
const healthLabel = formatOAuthHealthLabel(t, account.health);
const healthSummary = formatOAuthHealthSummary(t, item.name, account.id, account.health);
+ const secondaryIdentity = oauthAccountSecondaryIdentity(account, maskedId, t);
return (
@@ -572,7 +577,7 @@ export default function ProviderAuthPanel({
{label}
- {[account.email, `${t("prov.accountId")}: ${maskedId}`].filter(Boolean).join(" · ")}
+ {secondaryIdentity}
{healthSummary && (
{healthSummary}
)}
diff --git a/gui/src/hooks/useProviderAccountPools.ts b/gui/src/hooks/useProviderAccountPools.ts
index b934bd94f60..65637a6c97d 100644
--- a/gui/src/hooks/useProviderAccountPools.ts
+++ b/gui/src/hooks/useProviderAccountPools.ts
@@ -39,16 +39,37 @@ function selectionRows(rows: T[], id:
return id === undefined ? rows : rows.map(row => ({ ...row, active: row.id === id }));
}
+type FullQuotaLoadOwnership = {
+ generation: number;
+ modes?: ReadonlyMap;
+};
+
+const quotaLoadOwnsRow = (ownership: FullQuotaLoadOwnership | undefined, row: QuotaRow): boolean =>
+ ownership !== undefined && (ownership.modes === undefined || ownership.modes.get(row.id) === row.quotaMode);
+
+function settleUnownedPendingRows(rows: T[], ownership: FullQuotaLoadOwnership | undefined): T[] {
+ return rows.map(row => row.quotaPending === true && !quotaLoadOwnsRow(ownership, row)
+ ? { ...row, quotaPending: false }
+ : row);
+}
+
/** An invalidation read changes membership/selection, not quota probe state. */
-function mergeRosterRows(rows: T[], previous: T[]): T[] {
+function mergeRosterRows(rows: T[], previous: T[], quotaLoad?: FullQuotaLoadOwnership): T[] {
const prior = new Map(previous.map(row => [row.id, row]));
- return mergeQuotaRows(rows, previous, false).map(row => supportsQuotaRead(row) ? {
- ...row,
- quotaPending: prior.get(row.id)?.quotaPending ?? false,
- quotaUnavailable: prior.get(row.id)?.quotaMode === row.quotaMode ? prior.get(row.id)?.quotaUnavailable ?? false : false,
- quotaFailure: row.quotaMode === "probe" && prior.get(row.id)?.quotaMode === row.quotaMode && prior.get(row.id)?.quotaUnavailable
- ? parseQuotaFailureCode(prior.get(row.id)?.quotaFailure) : undefined,
- } : row);
+ return mergeQuotaRows(rows, previous, false).map(row => {
+ if (!supportsQuotaRead(row)) return row;
+ const previousRow = prior.get(row.id);
+ const sameMode = previousRow?.quotaMode === row.quotaMode;
+ const unresolved = !sameMode || (previousRow?.quota === undefined && previousRow?.quotaUnavailable !== true);
+ const owned = quotaLoadOwnsRow(quotaLoad, row);
+ return {
+ ...row,
+ quotaPending: row.quotaMode === "probe" && owned && (previousRow?.quotaPending === true || unresolved),
+ quotaUnavailable: sameMode ? previousRow?.quotaUnavailable ?? false : false,
+ quotaFailure: row.quotaMode === "probe" && sameMode && previousRow?.quotaUnavailable
+ ? parseQuotaFailureCode(previousRow.quotaFailure) : undefined,
+ };
+ });
}
/** A probe started before a newer roster may update quota only on surviving IDs. */
@@ -137,6 +158,7 @@ export function useProviderAccountPools(deps: {
const accountRequestGenerationRef = useRef>({});
const rosterGenerationRef = useRef>({});
const quotaGenerationRef = useRef>({});
+ const fullQuotaLoadRef = useRef>({});
const selectionMutationsRef = useRef(new Map());
const requestsRef = useRef(new Set());
const pausingAccountRef = useRef(new Map());
@@ -148,6 +170,7 @@ export function useProviderAccountPools(deps: {
const requests = requestsRef.current;
const rosterGenerations = rosterGenerationRef.current;
const quotaGenerations = quotaGenerationRef.current;
+ const fullQuotaLoads = fullQuotaLoadRef.current;
const mutations = selectionMutationsRef.current;
const pausingOwners = pausingAccountRef.current;
const switchingOwners = switchingAccountRef.current;
@@ -167,6 +190,7 @@ export function useProviderAccountPools(deps: {
for (const key of Object.keys(generations)) generations[key] += 1;
for (const key of Object.keys(rosterGenerations)) rosterGenerations[key] += 1;
for (const key of Object.keys(quotaGenerations)) quotaGenerations[key] += 1;
+ for (const key of Object.keys(fullQuotaLoads)) delete fullQuotaLoads[key];
mutations.clear();
pausingOwners.clear();
switchingOwners.clear();
@@ -210,6 +234,10 @@ export function useProviderAccountPools(deps: {
const key = `oauth:${provider}`;
const generation = (accountRequestGenerationRef.current[key] ?? 0) + 1;
accountRequestGenerationRef.current[key] = generation;
+ fullQuotaLoadRef.current[key] = { generation };
+ const clearFullQuotaLoad = () => {
+ if (fullQuotaLoadRef.current[key]?.generation === generation) delete fullQuotaLoadRef.current[key];
+ };
const rosterGeneration = (rosterGenerationRef.current[key] ?? 0) + 1;
rosterGenerationRef.current[key] = rosterGeneration;
const currentRequest = () => aliveRef.current && mountedRef.current && serverRef.current === apiBase && accountRequestGenerationRef.current[key] === generation;
@@ -220,14 +248,20 @@ export function useProviderAccountPools(deps: {
// even when Anthropic's usage endpoint is slow or timing out.
const data = await readRoster<{ activeAccountId?: string | null; accounts?: OAuthAccount[] }>(url);
if (!Array.isArray(data.accounts)) throw new Error("Invalid account roster");
- if (!currentRequest()) return false;
+ if (!currentRequest()) { clearFullQuotaLoad(); return false; }
const rows = selectionRows(data.accounts, data.activeAccountId);
+ const ownership = fullQuotaLoadRef.current[key];
+ if (ownership?.generation === generation) {
+ ownership.modes = new Map(rows.filter(supportsQuotaRead).map(row => [row.id, row.quotaMode]));
+ }
setAccountSets(current => currentRoster() ? { ...current, [provider]: {
activeAccountId: data.activeAccountId ?? null,
accounts: mergeQuotaRows(rows, current[provider]?.accounts ?? [], false),
+ } } : current[provider] ? { ...current, [provider]: {
+ ...current[provider], accounts: settleUnownedPendingRows(current[provider].accounts, ownership),
} } : current);
setAccountLoadStates(current => currentRoster() ? { ...current, [provider]: "ready" } : current);
- if (!rows.some(supportsQuotaRead)) return true;
+ if (!rows.some(supportsQuotaRead)) { clearFullQuotaLoad(); return true; }
const enrich = async (): Promise => {
// Manual selection invalidates roster reads, not a per-ID quota probe already sent.
@@ -256,12 +290,15 @@ export function useProviderAccountPools(deps: {
...current, [provider]: { ...current[provider], accounts: unavailableQuotaRows(current[provider].accounts, rows) },
} : current);
return false;
+ } finally {
+ clearFullQuotaLoad();
}
};
if (refresh) return await enrich();
void enrich();
return true;
} catch {
+ clearFullQuotaLoad();
if (!currentRoster()) return false;
setAccountLoadStates(current => currentRoster() ? { ...current, [provider]: "error" } : current);
setAccountSets(current => currentRoster() && current[provider] ? {
@@ -279,6 +316,10 @@ export function useProviderAccountPools(deps: {
const key = `key:${name}`;
const generation = (accountRequestGenerationRef.current[key] ?? 0) + 1;
accountRequestGenerationRef.current[key] = generation;
+ fullQuotaLoadRef.current[key] = { generation };
+ const clearFullQuotaLoad = () => {
+ if (fullQuotaLoadRef.current[key]?.generation === generation) delete fullQuotaLoadRef.current[key];
+ };
const rosterGeneration = (rosterGenerationRef.current[key] ?? 0) + 1;
rosterGenerationRef.current[key] = rosterGeneration;
const currentRequest = () => aliveRef.current && mountedRef.current && serverRef.current === apiBase && accountRequestGenerationRef.current[key] === generation;
@@ -292,10 +333,16 @@ export function useProviderAccountPools(deps: {
try {
const data = await readRoster<{ activeId?: string | null; keys?: ApiKeyEntry[] }>(url);
if (!Array.isArray(data.keys)) throw new Error("Invalid key roster");
- if (!currentRequest()) return false;
+ if (!currentRequest()) { clearFullQuotaLoad(); return false; }
const rows = selectionRows(data.keys, data.activeId);
- setKeyPools(current => currentRoster() ? { ...current, [name]: mergeQuotaRows(rows, current[name] ?? [], false) } : current);
- if (!rows.some(supportsQuotaRead)) return true;
+ const ownership = fullQuotaLoadRef.current[key];
+ if (ownership?.generation === generation) {
+ ownership.modes = new Map(rows.filter(supportsQuotaRead).map(row => [row.id, row.quotaMode]));
+ }
+ setKeyPools(current => currentRoster()
+ ? { ...current, [name]: mergeQuotaRows(rows, current[name] ?? [], false) }
+ : current[name] ? { ...current, [name]: settleUnownedPendingRows(current[name], ownership) } : current);
+ if (!rows.some(supportsQuotaRead)) { clearFullQuotaLoad(); return true; }
const enrich = async (): Promise => {
const quotaGeneration = (quotaGenerationRef.current[key] ?? 0) + 1;
quotaGenerationRef.current[key] = quotaGeneration;
@@ -314,12 +361,14 @@ export function useProviderAccountPools(deps: {
if (currentQuota()) setKeyPools(current => currentQuota()
? { ...current, [name]: unavailableQuotaRows(current[name] ?? [], rows) } : current);
return false;
+ } finally {
+ clearFullQuotaLoad();
}
};
if (refresh) return await enrich();
void enrich();
return true;
- } catch { return failed(); }
+ } catch { clearFullQuotaLoad(); return failed(); }
}));
return results.every(Boolean);
}, [apiBase, aliveRef, readRoster]);
@@ -345,7 +394,7 @@ export function useProviderAccountPools(deps: {
const rows = selectionRows(data.accounts, data.activeAccountId);
setAccountSets(current => currentRequest() ? { ...current, [provider]: {
activeAccountId: data.activeAccountId === undefined ? rows.find(row => row.active)?.id ?? null : data.activeAccountId,
- accounts: mergeRosterRows(rows, current[provider]?.accounts ?? []),
+ accounts: mergeRosterRows(rows, current[provider]?.accounts ?? [], fullQuotaLoadRef.current[key]),
} } : current);
setAccountLoadStates(current => currentRequest() ? { ...current, [provider]: "ready" } : current);
} else {
@@ -353,7 +402,9 @@ export function useProviderAccountPools(deps: {
`${apiBase}/api/providers/keys?name=${encodeURIComponent(provider)}`, signal);
if (!Array.isArray(data.keys) || !currentRequest()) return false;
const rows = selectionRows(data.keys, data.activeId);
- setKeyPools(current => currentRequest() ? { ...current, [provider]: mergeRosterRows(rows, current[provider] ?? []) } : current);
+ setKeyPools(current => currentRequest() ? { ...current, [provider]: mergeRosterRows(
+ rows, current[provider] ?? [], fullQuotaLoadRef.current[key],
+ ) } : current);
}
return true;
} catch {
diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts
index a52321642b6..cc9a100a6e4 100644
--- a/gui/src/i18n/de.ts
+++ b/gui/src/i18n/de.ts
@@ -858,7 +858,9 @@ export const de: Record = {
"models.emptyDiscovery": "Es wurden keine Modelle gefunden. Prüfe den Anbieter-Endpunkt oder füge ein statisches/eigenes Modell hinzu.",
"models.emptyDiscoveryDisabled": "Die Live-Modellerkennung ist aus und es sind keine statischen Modelle konfiguriert.",
"models.discoveryFailedBadge": "Erkennung fehlgeschlagen",
+ "models.discoveryRateLimitedBadge": "Erkennung begrenzt",
"models.discoveryFailedHttp": "Die Modellerkennung ist fehlgeschlagen (HTTP {status}).",
+ "models.discoveryRateLimitedHttp": "Die Modellerkennung ist vorübergehend durch eine Ratenbegrenzung eingeschränkt (HTTP {status}).",
"models.discoveryFailedBlocked": "Die Modellerkennung wurde durch die Zielrichtlinie blockiert.",
"models.discoveryFailedInvalidResponse": "Die Modellerkennung lieferte eine ungültige Antwort.",
"models.discoveryFailedNetwork": "Die Modellerkennung ist an einem Netzwerkfehler gescheitert.",
@@ -2702,6 +2704,11 @@ export const de: Record = {
"quota.observedAgo": "Vor {age} erfasst",
"quota.observedHint": "Meta meldet die Nutzung nur während einer Streaming-Antwort. Dies ist der zuletzt erfasste Wert, keine Live-Messung.",
"quota.weeklyLimit": "Wochenlimit",
+ "quota.modelFamily.claude": "Claude",
+ "quota.modelFamily.fable": "Fable",
+ "quota.modelFamily.sonnet": "Sonnet",
+ "quota.modelFamily.opus": "Opus",
+ "quota.modelFamily.haiku": "Haiku",
"quota.monthlyLimit": "30-Tage-Limit",
"quota.cursorFirstParty": "Erstanbieter-Modelle",
"quota.cursorApiUsage": "API-Nutzung",
diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts
index 8b32907a741..a8203466f4f 100644
--- a/gui/src/i18n/en.ts
+++ b/gui/src/i18n/en.ts
@@ -888,7 +888,9 @@ export const en = {
"models.emptyDiscovery": "No models were discovered. Check the provider endpoint or add a static/custom model.",
"models.emptyDiscoveryDisabled": "Live model discovery is off and no static models are configured.",
"models.discoveryFailedBadge": "Discovery failed",
+ "models.discoveryRateLimitedBadge": "Discovery limited",
"models.discoveryFailedHttp": "Model discovery failed (HTTP {status}).",
+ "models.discoveryRateLimitedHttp": "Model discovery is temporarily rate-limited (HTTP {status}).",
"models.discoveryFailedBlocked": "Model discovery was blocked by the destination policy.",
"models.discoveryFailedInvalidResponse": "Model discovery returned an invalid response.",
"models.discoveryFailedNetwork": "Model discovery failed due to a network error.",
@@ -1597,6 +1599,11 @@ export const en = {
"quota.observedAgo": "Observed {age} ago",
"quota.observedHint": "Meta reports usage only during a streaming response, so this is the last value seen, not a live reading.",
"quota.weeklyLimit": "Weekly limit",
+ "quota.modelFamily.claude": "Claude",
+ "quota.modelFamily.fable": "Fable",
+ "quota.modelFamily.sonnet": "Sonnet",
+ "quota.modelFamily.opus": "Opus",
+ "quota.modelFamily.haiku": "Haiku",
"quota.monthlyLimit": "30-day limit",
"quota.cursorFirstParty": "First-party models",
"quota.cursorApiUsage": "API usage",
diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts
index 002f1cf2955..f781e8ec06b 100644
--- a/gui/src/i18n/fr.ts
+++ b/gui/src/i18n/fr.ts
@@ -873,7 +873,9 @@ export const fr: Record = {
"models.emptyDiscovery": "Aucun modèle n’a été détecté. Vérifiez le point de terminaison du fournisseur ou ajoutez un modèle statique/personnalisé.",
"models.emptyDiscoveryDisabled": "La détection dynamique des modèles est désactivée et aucun modèle statique n’est configuré.",
"models.discoveryFailedBadge": "Échec de la détection",
+ "models.discoveryRateLimitedBadge": "Détection limitée",
"models.discoveryFailedHttp": "Échec de la détection des modèles (HTTP {status}).",
+ "models.discoveryRateLimitedHttp": "La détection des modèles est temporairement limitée par le débit (HTTP {status}).",
"models.discoveryFailedBlocked": "La détection des modèles a été bloquée par la politique de destination.",
"models.discoveryFailedInvalidResponse": "La détection des modèles a renvoyé une réponse non valide.",
"models.discoveryFailedNetwork": "La détection des modèles a échoué en raison d’une erreur réseau.",
@@ -1570,6 +1572,11 @@ export const fr: Record = {
"quota.observedAgo": "Relevé il y a {age}",
"quota.observedHint": "Meta ne communique l'utilisation que pendant une réponse en streaming : il s'agit de la dernière valeur observée, pas d'une mesure en direct.",
"quota.weeklyLimit": "Limite hebdomadaire",
+ "quota.modelFamily.claude": "Claude",
+ "quota.modelFamily.fable": "Fable",
+ "quota.modelFamily.sonnet": "Sonnet",
+ "quota.modelFamily.opus": "Opus",
+ "quota.modelFamily.haiku": "Haiku",
"quota.monthlyLimit": "Limite sur 30 jours",
"quota.cursorFirstParty": "Modèles propriétaires",
"quota.cursorApiUsage": "Utilisation de l’API",
diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts
index 6eb6908d816..c5856d2345b 100644
--- a/gui/src/i18n/ja.ts
+++ b/gui/src/i18n/ja.ts
@@ -795,7 +795,9 @@ export const ja: Record = {
"models.emptyDiscovery": "モデルが見つかりませんでした。プロバイダーのエンドポイントを確認するか、静的/カスタムモデルを追加してください。",
"models.emptyDiscoveryDisabled": "ライブモデル検出がオフで、静的モデルも設定されていません。",
"models.discoveryFailedBadge": "検出に失敗",
+ "models.discoveryRateLimitedBadge": "検出を制限中",
"models.discoveryFailedHttp": "モデル検出に失敗しました(HTTP {status})。",
+ "models.discoveryRateLimitedHttp": "モデル検出は一時的にレート制限されています(HTTP {status})。",
"models.discoveryFailedBlocked": "モデル検出は宛先ポリシーによりブロックされました。",
"models.discoveryFailedInvalidResponse": "モデル検出が無効な応答を返しました。",
"models.discoveryFailedNetwork": "ネットワークエラーによりモデル検出に失敗しました。",
@@ -1480,6 +1482,11 @@ export const ja: Record = {
"quota.observedAgo": "{age}前に取得",
"quota.observedHint": "Meta はストリーミング応答中にのみ使用量を報告します。リアルタイムの値ではなく、最後に取得した値です。",
"quota.weeklyLimit": "週間上限",
+ "quota.modelFamily.claude": "Claude",
+ "quota.modelFamily.fable": "Fable",
+ "quota.modelFamily.sonnet": "Sonnet",
+ "quota.modelFamily.opus": "Opus",
+ "quota.modelFamily.haiku": "Haiku",
"quota.monthlyLimit": "30 日上限",
"quota.cursorFirstParty": "ファーストパーティモデル",
"quota.cursorApiUsage": "API 使用量",
diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts
index 5f2d2a96c89..d13ecba0fe2 100644
--- a/gui/src/i18n/ko.ts
+++ b/gui/src/i18n/ko.ts
@@ -869,7 +869,9 @@ export const ko: Record = {
"models.emptyDiscovery": "발견된 모델이 없습니다. 프로바이더 엔드포인트를 확인하거나 정적/사용자 모델을 추가하세요.",
"models.emptyDiscoveryDisabled": "실시간 모델 검색이 꺼져 있고 정적 모델도 설정되지 않았습니다.",
"models.discoveryFailedBadge": "검색 실패",
+ "models.discoveryRateLimitedBadge": "검색 제한됨",
"models.discoveryFailedHttp": "모델 검색에 실패했습니다(HTTP {status}).",
+ "models.discoveryRateLimitedHttp": "모델 검색이 일시적으로 속도 제한되었습니다(HTTP {status}).",
"models.discoveryFailedBlocked": "대상 정책 때문에 모델 검색이 차단되었습니다.",
"models.discoveryFailedInvalidResponse": "모델 검색이 잘못된 응답을 받았습니다.",
"models.discoveryFailedNetwork": "네트워크 오류로 모델 검색에 실패했습니다.",
@@ -2749,6 +2751,11 @@ export const ko: Record = {
"quota.observedAgo": "{age} 전에 확인한 값",
"quota.observedHint": "Meta는 스트리밍 응답 중에만 사용량을 보고합니다. 실시간 수치가 아니라 마지막으로 확인된 값입니다.",
"quota.weeklyLimit": "주간 한도",
+ "quota.modelFamily.claude": "Claude",
+ "quota.modelFamily.fable": "Fable",
+ "quota.modelFamily.sonnet": "Sonnet",
+ "quota.modelFamily.opus": "Opus",
+ "quota.modelFamily.haiku": "Haiku",
"quota.monthlyLimit": "30일 한도",
"quota.cursorFirstParty": "자사 모델",
"quota.cursorApiUsage": "API 사용량",
diff --git a/gui/src/i18n/pt.ts b/gui/src/i18n/pt.ts
index 299456162f3..d0224c5269f 100644
--- a/gui/src/i18n/pt.ts
+++ b/gui/src/i18n/pt.ts
@@ -887,7 +887,9 @@ export const pt: Record = {
"models.emptyDiscovery": "Nenhum modelo foi descoberto. Verifique o endpoint do provedor ou adicione um modelo estático/personalizado.",
"models.emptyDiscoveryDisabled": "A descoberta de modelos em tempo real está desativada e nenhum modelo estático está configurado.",
"models.discoveryFailedBadge": "Falha na descoberta",
+ "models.discoveryRateLimitedBadge": "Descoberta limitada",
"models.discoveryFailedHttp": "A descoberta de modelos falhou (HTTP {status}).",
+ "models.discoveryRateLimitedHttp": "A descoberta de modelos está temporariamente sujeita a limite de requisições (HTTP {status}).",
"models.discoveryFailedBlocked": "A descoberta de modelos foi bloqueada pela política do destino.",
"models.discoveryFailedInvalidResponse": "A descoberta de modelos retornou uma resposta inválida.",
"models.discoveryFailedNetwork": "A descoberta de modelos falhou por erro de rede.",
@@ -1581,6 +1583,11 @@ export const pt: Record = {
"quota.observedAgo": "Observado há {age}",
"quota.observedHint": "A Meta informa o uso apenas durante uma resposta em streaming, então este é o último valor visto, não uma leitura em tempo real.",
"quota.weeklyLimit": "Limite semanal",
+ "quota.modelFamily.claude": "Claude",
+ "quota.modelFamily.fable": "Fable",
+ "quota.modelFamily.sonnet": "Sonnet",
+ "quota.modelFamily.opus": "Opus",
+ "quota.modelFamily.haiku": "Haiku",
"quota.monthlyLimit": "Limite de 30 dias",
"quota.cursorFirstParty": "Modelos próprios",
"quota.cursorApiUsage": "Uso da API",
diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts
index e11cd3ce4b9..f2d5368a857 100644
--- a/gui/src/i18n/ru.ts
+++ b/gui/src/i18n/ru.ts
@@ -871,7 +871,9 @@ export const ru: Record = {
"models.emptyDiscovery": "Модели не обнаружены. Проверьте адрес провайдера или добавьте статическую/пользовательскую модель.",
"models.emptyDiscoveryDisabled": "Автообнаружение моделей выключено, статические модели не настроены.",
"models.discoveryFailedBadge": "Ошибка обнаружения",
+ "models.discoveryRateLimitedBadge": "Обнаружение ограничено",
"models.discoveryFailedHttp": "Не удалось обнаружить модели (HTTP {status}).",
+ "models.discoveryRateLimitedHttp": "Обнаружение моделей временно ограничено по частоте запросов (HTTP {status}).",
"models.discoveryFailedBlocked": "Обнаружение моделей заблокировано политикой назначения.",
"models.discoveryFailedInvalidResponse": "Обнаружение моделей вернуло недопустимый ответ.",
"models.discoveryFailedNetwork": "Обнаружение моделей не удалось из-за сетевой ошибки.",
@@ -1556,6 +1558,11 @@ export const ru: Record = {
"quota.observedAgo": "Получено {age} назад",
"quota.observedHint": "Meta сообщает об использовании только во время потокового ответа, поэтому это последнее полученное значение, а не текущее.",
"quota.weeklyLimit": "Недельный лимит",
+ "quota.modelFamily.claude": "Claude",
+ "quota.modelFamily.fable": "Fable",
+ "quota.modelFamily.sonnet": "Sonnet",
+ "quota.modelFamily.opus": "Opus",
+ "quota.modelFamily.haiku": "Haiku",
"quota.monthlyLimit": "30-дневный лимит",
"quota.cursorFirstParty": "Собственные модели",
"quota.cursorApiUsage": "Использование API",
diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts
index 57e283fe903..d8a4657fc0e 100644
--- a/gui/src/i18n/tr.ts
+++ b/gui/src/i18n/tr.ts
@@ -874,7 +874,9 @@ export const tr: Record = {
"models.emptyDiscovery": "Keşfedilen model yok.",
"models.emptyDiscoveryDisabled": "Canlı model keşfi kapalı.",
"models.discoveryFailedBadge": "Keşif başarısız",
+ "models.discoveryRateLimitedBadge": "Keşif sınırlandı",
"models.discoveryFailedHttp": "Model keşfi başarısız oldu (HTTP {status}).",
+ "models.discoveryRateLimitedHttp": "Model keşfi geçici olarak hız sınırına takıldı (HTTP {status}).",
"models.discoveryFailedBlocked": "Model keşfi engellendi.",
"models.discoveryFailedInvalidResponse": "Model keşfi geçersiz bir yanıt döndürdü.",
"models.discoveryFailedNetwork": "Model keşfi ağ hatası nedeniyle başarısız oldu.",
@@ -1583,6 +1585,11 @@ export const tr: Record = {
"quota.observedAgo": "{age} önce alındı",
"quota.observedHint": "Meta kullanımı yalnızca akış yanıtı sırasında bildirir; bu canlı bir ölçüm değil, en son alınan değerdir.",
"quota.weeklyLimit": "Haftalık limit",
+ "quota.modelFamily.claude": "Claude",
+ "quota.modelFamily.fable": "Fable",
+ "quota.modelFamily.sonnet": "Sonnet",
+ "quota.modelFamily.opus": "Opus",
+ "quota.modelFamily.haiku": "Haiku",
"quota.monthlyLimit": "30 günlük limit",
"quota.cursorFirstParty": "Birinci taraf modeller",
"quota.cursorApiUsage": "API kullanımı",
diff --git a/gui/src/i18n/vi.ts b/gui/src/i18n/vi.ts
index 96858b1c670..12f0201fa33 100644
--- a/gui/src/i18n/vi.ts
+++ b/gui/src/i18n/vi.ts
@@ -873,7 +873,9 @@ export const vi: Record = {
"models.emptyDiscovery": "Không tìm thấy model nào. Hãy kiểm tra endpoint của provider hoặc thêm model tĩnh/tuỳ chỉnh.",
"models.emptyDiscoveryDisabled": "Tính năng khám phá model trực tiếp đang tắt và không có model tĩnh nào được cấu hình.",
"models.discoveryFailedBadge": "Khám phá thất bại",
+ "models.discoveryRateLimitedBadge": "Khám phá bị giới hạn",
"models.discoveryFailedHttp": "Khám phá model thất bại (HTTP {status}).",
+ "models.discoveryRateLimitedHttp": "Khám phá model đang tạm thời bị giới hạn tần suất (HTTP {status}).",
"models.discoveryFailedBlocked": "Khám phá model bị chặn bởi chính sách đích.",
"models.discoveryFailedInvalidResponse": "Khám phá model trả về phản hồi không hợp lệ.",
"models.discoveryFailedNetwork": "Khám phá model thất bại do lỗi mạng.",
@@ -1568,6 +1570,11 @@ export const vi: Record = {
"quota.observedAgo": "Ghi nhận {age} trước",
"quota.observedHint": "Meta chỉ báo cáo mức sử dụng trong quá trình phản hồi stream, vì vậy đây là giá trị cuối cùng ghi nhận được, không phải số liệu thời gian thực.",
"quota.weeklyLimit": "Giới hạn hàng tuần",
+ "quota.modelFamily.claude": "Claude",
+ "quota.modelFamily.fable": "Fable",
+ "quota.modelFamily.sonnet": "Sonnet",
+ "quota.modelFamily.opus": "Opus",
+ "quota.modelFamily.haiku": "Haiku",
"quota.monthlyLimit": "Giới hạn 30 ngày",
"quota.cursorFirstParty": "Các model first-party",
"quota.cursorApiUsage": "Mức sử dụng API",
diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts
index 8cdd0a2e983..bb096fbdcea 100644
--- a/gui/src/i18n/zh-TW.ts
+++ b/gui/src/i18n/zh-TW.ts
@@ -721,7 +721,9 @@ export const zhTW: Record = {
"models.emptyDiscovery": "未發現任何模型。請檢查供應商端點,或新增靜態/自訂模型。",
"models.emptyDiscoveryDisabled": "即時模型發現已關閉,且尚未配置靜態模型。",
"models.discoveryFailedBadge": "發現失敗",
+ "models.discoveryRateLimitedBadge": "發現受限",
"models.discoveryFailedHttp": "模型發現失敗(HTTP {status})。",
+ "models.discoveryRateLimitedHttp": "模型發現暫時受到速率限制(HTTP {status})。",
"models.discoveryFailedBlocked": "模型發現被目標策略阻止。",
"models.discoveryFailedInvalidResponse": "模型發現返回了無效回應。",
"models.discoveryFailedNetwork": "由於網路錯誤,模型發現失敗。",
@@ -1268,6 +1270,11 @@ export const zhTW: Record = {
"quota.observedAgo": "{age}前取得",
"quota.observedHint": "Meta 僅在串流回應期間回報用量,因此這是最後一次取得的數值,而非即時讀數。",
"quota.weeklyLimit": "每週限額",
+ "quota.modelFamily.claude": "Claude",
+ "quota.modelFamily.fable": "Fable",
+ "quota.modelFamily.sonnet": "Sonnet",
+ "quota.modelFamily.opus": "Opus",
+ "quota.modelFamily.haiku": "Haiku",
"quota.monthlyLimit": "30 天限額",
"quota.cursorFirstParty": "官方模型",
"quota.cursorApiUsage": "API 用量",
diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts
index faead6cee93..b24f436b565 100644
--- a/gui/src/i18n/zh.ts
+++ b/gui/src/i18n/zh.ts
@@ -866,7 +866,9 @@ export const zh: Record = {
"models.emptyDiscovery": "未发现任何模型。请检查提供方端点,或添加静态/自定义模型。",
"models.emptyDiscoveryDisabled": "实时模型发现已关闭,且尚未配置静态模型。",
"models.discoveryFailedBadge": "发现失败",
+ "models.discoveryRateLimitedBadge": "发现受限",
"models.discoveryFailedHttp": "模型发现失败(HTTP {status})。",
+ "models.discoveryRateLimitedHttp": "模型发现暂时受到速率限制(HTTP {status})。",
"models.discoveryFailedBlocked": "模型发现被目标策略阻止。",
"models.discoveryFailedInvalidResponse": "模型发现返回了无效响应。",
"models.discoveryFailedNetwork": "由于网络错误,模型发现失败。",
@@ -2722,6 +2724,11 @@ export const zh: Record = {
"quota.observedAgo": "{age}前获取",
"quota.observedHint": "Meta 仅在流式响应期间报告用量,因此这是最后一次获取的数值,而非实时读数。",
"quota.weeklyLimit": "每周限额",
+ "quota.modelFamily.claude": "Claude",
+ "quota.modelFamily.fable": "Fable",
+ "quota.modelFamily.sonnet": "Sonnet",
+ "quota.modelFamily.opus": "Opus",
+ "quota.modelFamily.haiku": "Haiku",
"quota.monthlyLimit": "30 天限额",
"quota.cursorFirstParty": "官方模型",
"quota.cursorApiUsage": "API 用量",
diff --git a/gui/src/pages/Models.tsx b/gui/src/pages/Models.tsx
index 6c0a446fb81..d552e223328 100644
--- a/gui/src/pages/Models.tsx
+++ b/gui/src/pages/Models.tsx
@@ -71,6 +71,7 @@ import {
THREAD_OPTION_SET,
THREAD_OPTIONS,
writeCollapsedProviders,
+ discoveryFailureBadgeLabel,
discoveryFailureLabel,
filterFreeModelRows,
freeOnlyInForce,
@@ -1477,7 +1478,7 @@ export default function Models({ apiBase, restartEpoch = 0, connected = false, c
role="status"
title={discoveryFailureLabel(t, discoveryFailure)}
>
- {t("models.discoveryFailedBadge")}
+ {discoveryFailureBadgeLabel(t, discoveryFailure)}
)}
{t("models.active", { active: activeCount, total: scoped.length })}
diff --git a/gui/src/pages/models-provider-hints.tsx b/gui/src/pages/models-provider-hints.tsx
index bd824f6b544..be46103fb84 100644
--- a/gui/src/pages/models-provider-hints.tsx
+++ b/gui/src/pages/models-provider-hints.tsx
@@ -2,7 +2,7 @@ import { IconInfo } from "../icons";
import { useT } from "../i18n/shared";
import { navigateHash } from "../hash-routing";
import type { ProviderDiscoverySummary } from "../models-groups";
-import { discoveryFailureLabel } from "./models-shared";
+import { discoveryFailureBadgeLabel, discoveryFailureLabel } from "./models-shared";
export function EmptyProviderHint({
liveModels,
@@ -19,7 +19,7 @@ export function EmptyProviderHint({
- {failed && showFailureBadge && <>{t("models.discoveryFailedBadge")}{" "}>}
+ {failed && showFailureBadge && <>{discoveryFailureBadgeLabel(t, failed)}{" "}>}
{failed
? `${discoveryFailureLabel(t, failed)} `
: `${t(liveModels ? "models.emptyDiscovery" : "models.emptyDiscoveryDisabled")} `}
diff --git a/gui/src/pages/models-shared.ts b/gui/src/pages/models-shared.ts
index 8dc8c4c73aa..afe0c3c701c 100644
--- a/gui/src/pages/models-shared.ts
+++ b/gui/src/pages/models-shared.ts
@@ -5,13 +5,24 @@ import { formatNamespacedModelId } from "../provider-icons";
export type StorageLike = Pick;
+export function discoveryFailureBadgeLabel(
+ t: TFn,
+ discovery: Extract,
+): string {
+ return discovery.reason === "http" && discovery.httpStatus === 429
+ ? t("models.discoveryRateLimitedBadge")
+ : t("models.discoveryFailedBadge");
+}
+
export function discoveryFailureLabel(
t: TFn,
discovery: Extract,
): string {
switch (discovery.reason) {
case "http":
- return t("models.discoveryFailedHttp", { status: discovery.httpStatus });
+ return discovery.httpStatus === 429
+ ? t("models.discoveryRateLimitedHttp", { status: discovery.httpStatus })
+ : t("models.discoveryFailedHttp", { status: discovery.httpStatus });
case "blocked":
return t("models.discoveryFailedBlocked");
case "invalid_response":
diff --git a/gui/src/pages/use-providers-oauth.ts b/gui/src/pages/use-providers-oauth.ts
index ac66bd33b85..095898b654f 100644
--- a/gui/src/pages/use-providers-oauth.ts
+++ b/gui/src/pages/use-providers-oauth.ts
@@ -7,9 +7,25 @@ import { parseBrowserLaunch, type BrowserLaunch } from "../oauth-browser-launch"
import { loginPollAttempts } from "../oauth-login-budget";
import type { OAuthAccount, OAuthStatus } from "./providers-shared";
import { oauthLabel } from "./providers-shared";
+import { getActiveLocale } from "../i18n/shared";
type AccountSet = { activeAccountId: string | null; accounts: OAuthAccount[] };
+export async function reconcileCancelledOAuthProvider(
+ provider: string,
+ _setAccountSets: React.Dispatch>>,
+ fetchAccountSets: (providers: string[]) => Promise,
+ fetchOauth: () => Promise,
+): Promise {
+ // Cancellation ends only the pending login transaction. Existing committed accounts remain
+ // authoritative until a successful backend read replaces them. Clearing optimistically here
+ // made a transient refresh failure hide real credentials from the UI.
+ await Promise.allSettled([
+ fetchAccountSets([provider]),
+ fetchOauth(),
+ ]);
+}
+
export function useProvidersOAuth({
apiBase,
t,
@@ -111,10 +127,12 @@ export function useProvidersOAuth({
activeLoginGenerationsRef.current.delete(provider);
await cancelServerLogin(provider);
if (!aliveRef.current || oauthLoginGenerationRef.current!.get(provider) !== gen) return;
+ await reconcileCancelledOAuthProvider(provider, setAccountSets, fetchAccountSets, fetchOauth);
+ if (!aliveRef.current || oauthLoginGenerationRef.current!.get(provider) !== gen) return;
setBusy(current => current === provider ? null : current);
setLoginInfo(current => current?.provider === provider ? null : current);
notify(t("prov.loginCancelled", { provider: oauthLabel(provider) }), false);
- }, [aliveRef, bumpLoginGeneration, cancelServerLogin, notify, setBusy, setLoginInfo, t]);
+ }, [aliveRef, bumpLoginGeneration, cancelServerLogin, fetchAccountSets, fetchOauth, notify, setAccountSets, setBusy, setLoginInfo, t]);
const loginOAuth = async (provider: string, addAccount = false, accountId?: string) => {
const generation = bumpLoginGeneration(provider);
@@ -131,6 +149,7 @@ export function useProvidersOAuth({
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
provider,
+ ...(provider === "mirasim" ? { locale: getActiveLocale() } : {}),
// Explicit, never inferred, and omitted entirely when this operator has
// expressed no preference — otherwise the request would permanently
// overrule a persisted `oauthOpenBrowser: false`.
@@ -144,7 +163,12 @@ export function useProvidersOAuth({
if (!res.ok) {
const data = await res.json().catch(() => ({})) as { error?: string };
if (!aliveRef.current || oauthLoginGenerationRef.current!.get(provider) !== generation) return;
- notify(data.error || t("prov.loginFailStart", { provider: oauthLabel(provider) }), false);
+ notify(
+ provider === "mirasim"
+ ? t("prov.loginFailStart", { provider: oauthLabel(provider) })
+ : data.error || t("prov.loginFailStart", { provider: oauthLabel(provider) }),
+ false,
+ );
return;
}
const data = await res.json() as { url?: string; instructions?: string; deviceCode?: string; browserLaunch?: unknown };
@@ -170,10 +194,15 @@ export function useProvidersOAuth({
if (s.error) {
setOauthStatus(prev => ({ ...prev, [provider]: s }));
const cancelled = /cancel/i.test(s.error);
+ const timedOut = /timed?\s*out|timeout/i.test(s.error);
notify(
cancelled
? t("prov.loginCancelled", { provider: oauthLabel(provider) })
- : t("prov.loginError", { provider: oauthLabel(provider), error: s.error }),
+ : timedOut
+ ? t("prov.loginTimeout", { provider: oauthLabel(provider) })
+ : provider === "mirasim"
+ ? t("prov.loginRequestFail", { provider: oauthLabel(provider) })
+ : t("prov.loginError", { provider: oauthLabel(provider), error: s.error }),
false,
);
setLoginInfo(null);
diff --git a/gui/src/provider-icons.ts b/gui/src/provider-icons.ts
index 47df487d70b..682f7193a7f 100644
--- a/gui/src/provider-icons.ts
+++ b/gui/src/provider-icons.ts
@@ -42,6 +42,7 @@ const PROVIDER_ICON_ALIASES: Record = {
"meta-model": "meta.svg",
"meta-muse": "meta.svg",
mistral: "mistral-color.svg",
+ mirasim: "mirasim.svg",
minimax: "minimax.svg",
"minimax-cn": "minimax.svg",
moonshot: "moonshot-color.svg",
@@ -162,6 +163,7 @@ const PROVIDER_DISPLAY_NAMES: Record = {
packycode: "PackyCode",
tokenlab: "TokenLab",
mistral: "Mistral",
+ mirasim: "Mirasim",
groq: "Groq",
"meta-model": "Meta Model API",
"meta-muse": "Muse Code",
diff --git a/gui/src/provider-workspace/auth.ts b/gui/src/provider-workspace/auth.ts
index 35ebd15c1ae..3879e9cbd33 100644
--- a/gui/src/provider-workspace/auth.ts
+++ b/gui/src/provider-workspace/auth.ts
@@ -45,3 +45,21 @@ export function oauthAccountDisplayLabel(
const index = accounts.findIndex(candidate => candidate.id === account.id);
return t("pws.accountOrdinal", { count: String(index >= 0 ? index + 1 : 1) });
}
+
+/**
+ * Secondary identity line for OAuth rows.
+ * When the email is already the primary label, repeating it here only adds visual noise.
+ * If an alias owns the primary slot, keep the email as useful disambiguation.
+ */
+export function oauthAccountSecondaryIdentity(
+ account: OAuthAccountIdentity,
+ maskedId: string,
+ t: TFn,
+): string {
+ const alias = account.alias?.trim();
+ const email = account.email?.trim();
+ return [
+ alias && email ? email : undefined,
+ `${t("prov.accountId")}: ${maskedId}`,
+ ].filter(Boolean).join(" · ");
+}
diff --git a/gui/tests/add-provider-oauth-url-leak.test.tsx b/gui/tests/add-provider-oauth-url-leak.test.tsx
index 8b558fe5f33..52725cee897 100644
--- a/gui/tests/add-provider-oauth-url-leak.test.tsx
+++ b/gui/tests/add-provider-oauth-url-leak.test.tsx
@@ -29,6 +29,7 @@ let originalFetch: typeof globalThis.fetch;
let pendingLogins: Array<(url: string) => void> = [];
let oauthStatus: { loggedIn: boolean; error?: string } = { loggedIn: false };
let cancelledProviders: string[] = [];
+let oauthLoginBodies: Array> = [];
const PRESETS = [
{ id: "claude", label: "Claude", adapter: "anthropic", baseUrl: "https://api.anthropic.com", auth: "oauth", oauthProvider: "claude" },
@@ -51,6 +52,7 @@ beforeEach(() => {
pendingLogins = [];
oauthStatus = { loggedIn: false };
cancelledProviders = [];
+ oauthLoginBodies = [];
Object.defineProperty(globalThis, "fetch", {
configurable: true,
value: async (input: RequestInfo | URL, init?: RequestInit) => {
@@ -59,6 +61,7 @@ beforeEach(() => {
if (url.pathname === "/api/provider-presets") return Response.json({ providers: PRESETS });
if (url.pathname === "/api/usage") return Response.json({ providers: [] });
if (url.pathname === "/api/oauth/login" && (init?.method ?? "GET") === "POST") {
+ oauthLoginBodies.push(JSON.parse(String(init?.body ?? "{}")) as Record);
// Held open so the test controls when the URL lands.
return await new Promise((resolve) => {
pendingLogins.push((authUrl: string) => resolve(Response.json({ url: authUrl })));
@@ -257,6 +260,22 @@ test("leaving the providers page cancels its in-flight account login", async ()
expect(cancelledProviders).toEqual(["orcarouter-oauth"]);
});
+test("providers OAuth forwards the selected UI locale for Mirasim browser pages", async () => {
+ localStorage.setItem("ocx-lang", "zh-TW");
+ await mountProvidersOAuthHarness({ provider: "mirasim" });
+ await act(async () => { await new Promise((r) => setTimeout(r, 20)); });
+
+ expect(oauthLoginBodies[0]).toMatchObject({
+ provider: "mirasim",
+ locale: "zh-TW",
+ });
+
+ await act(async () => {
+ pendingLogins.shift()?.(A_URL);
+ await new Promise((r) => setTimeout(r, 20));
+ });
+});
+
test("pagehide cancels an account login and allows another login after bfcache restore", async () => {
await mountProvidersOAuthHarness();
await act(async () => {
diff --git a/gui/tests/fr-localization.test.ts b/gui/tests/fr-localization.test.ts
index 0d39f14bbd9..994029658ff 100644
--- a/gui/tests/fr-localization.test.ts
+++ b/gui/tests/fr-localization.test.ts
@@ -172,6 +172,13 @@ const INTENTIONAL_ENGLISH = new Set([
"claudeDesktop.family.fable",
"claudeDesktop.family.sonnet",
"claudeDesktop.family.haiku",
+ // Mirasim quota rows use the same Claude model-family proper nouns. French keeps these
+ // product/model names unchanged rather than inventing translated family names.
+ "quota.modelFamily.claude",
+ "quota.modelFamily.fable",
+ "quota.modelFamily.sonnet",
+ "quota.modelFamily.opus",
+ "quota.modelFamily.haiku",
"claudeDesktop.supports1m",
"claudeDesktop.effort.supported",
// Correct French words whose spelling is identical to English.
diff --git a/gui/tests/locale-parity.test.ts b/gui/tests/locale-parity.test.ts
index 5dd2690cbe2..85286bf6640 100644
--- a/gui/tests/locale-parity.test.ts
+++ b/gui/tests/locale-parity.test.ts
@@ -88,6 +88,13 @@ const ZH_TW_KEEP_ENGLISH: ReadonlySet = new Set([
"claudeDesktop.family.haiku",
"claudeDesktop.family.opus",
"claudeDesktop.family.sonnet",
+ // Mirasim quota rows expose the same model-family proper nouns. Taiwan usage keeps these
+ // canonical English model names, while the surrounding quota/time-window copy is localized.
+ "quota.modelFamily.claude",
+ "quota.modelFamily.fable",
+ "quota.modelFamily.sonnet",
+ "quota.modelFamily.opus",
+ "quota.modelFamily.haiku",
"claudeDesktop.supports1m",
"claudeDesktop.title",
// Claude fast-mode toggle states (short ON/OFF/Auto labels kept in English)
diff --git a/gui/tests/mirasim-auth-panel-presentation.test.tsx b/gui/tests/mirasim-auth-panel-presentation.test.tsx
new file mode 100644
index 00000000000..398a218b666
--- /dev/null
+++ b/gui/tests/mirasim-auth-panel-presentation.test.tsx
@@ -0,0 +1,108 @@
+import { afterEach, beforeEach, expect, test } from "bun:test";
+import { Window } from "happy-dom";
+import { act } from "react";
+import type { Root } from "react-dom/client";
+import ProviderAuthPanel from "../src/components/provider-workspace/ProviderAuthPanel";
+import { LanguageProvider } from "../src/i18n/provider";
+import type { WorkspaceItem } from "../src/provider-workspace/catalog";
+
+const globals = ["document", "window", "navigator", "fetch", "IS_REACT_ACT_ENVIRONMENT"] as const;
+let previous: Record<(typeof globals)[number], unknown>;
+let testWindow: Window;
+let root: Root | null = null;
+
+const ITEM: WorkspaceItem = {
+ name: "mirasim",
+ adapter: "mirasim",
+ baseUrl: "https://relay.mirasim.ai",
+ authMode: "oauth",
+};
+
+const handlers = {
+ onLogin: async () => {},
+ onLogout: async () => {},
+ onReauth: async () => {},
+ onSwitchAccount: async () => {},
+ onSwitchApiKey: async () => {},
+ onRemoveAccount: async () => {},
+ onRemoveApiKey: async () => {},
+ onAddApiKey: async () => {},
+ onEditAlias: async () => {},
+} as unknown as Parameters[0]["authHandlers"];
+
+beforeEach(() => {
+ previous = Object.fromEntries(globals.map(key => [key, Reflect.get(globalThis, key)])) as typeof previous;
+ testWindow = new Window({ url: "http://localhost/" });
+ Object.defineProperty(testWindow.navigator, "language", { configurable: true, value: "zh-TW" });
+ Object.defineProperties(globalThis, {
+ document: { configurable: true, value: testWindow.document },
+ window: { configurable: true, value: testWindow },
+ navigator: { configurable: true, value: testWindow.navigator },
+ });
+ globalThis.fetch = (async () => new Response("{}", { status: 200 })) as typeof fetch;
+ (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+});
+
+afterEach(async () => {
+ if (root) {
+ await act(async () => { root?.unmount(); });
+ root = null;
+ }
+ for (const key of globals) {
+ Object.defineProperty(globalThis, key, { configurable: true, value: previous[key] });
+ }
+ await testWindow.happyDOM?.close?.();
+});
+
+test("Mirasim account card renders one canonical row per quota window with human labels", async () => {
+ const host = testWindow.document.createElement("div");
+ testWindow.document.body.appendChild(host as never);
+ const { createRoot } = await import("react-dom/client");
+ const account = {
+ id: "account-example-5523",
+ email: "masked-mirasim-account",
+ active: false,
+ quotaMode: "probe",
+ quota: {
+ fiveHourPercent: 0.4,
+ fiveHourResetAt: 1_800_000_000_000,
+ weeklyPercent: 0.1,
+ weeklyResetAt: 1_900_000_000_000,
+ customWindows: [
+ { label: "Model · 7d_claude", percent: 0.5, resetAt: 1_900_000_000_000 },
+ { label: "Model · 7d_fable", percent: 0, resetAt: 1_900_000_000_000 },
+ ],
+ updatedAt: Date.now(),
+ },
+ };
+
+ await act(async () => {
+ root = createRoot(host);
+ root.render(
+
+
+ ,
+ );
+ });
+
+ expect(host.querySelector(".pwi-auth-row-label")?.textContent).toBe("masked-mirasim-account");
+ const secondary = host.querySelector(".pwi-auth-row-secondary")?.textContent ?? "";
+ expect(secondary).not.toContain("masked-mirasim-account");
+ expect(secondary).toContain("ID:");
+
+ const quotaLabels = Array.from(host.querySelectorAll(".quota-stacked-limit"))
+ .map(node => node.textContent);
+ expect(quotaLabels).toEqual([
+ "5 小時限額",
+ "每週限額",
+ "Claude · 每週限額",
+ "Fable · 每週限額",
+ ]);
+ expect(host.textContent).not.toContain("Model · 7d_");
+});
diff --git a/gui/tests/mirasim-oauth-cancel-roster.test.ts b/gui/tests/mirasim-oauth-cancel-roster.test.ts
new file mode 100644
index 00000000000..29baffc5b80
--- /dev/null
+++ b/gui/tests/mirasim-oauth-cancel-roster.test.ts
@@ -0,0 +1,92 @@
+import { describe, expect, test } from "bun:test";
+import { reconcileCancelledOAuthProvider } from "../src/pages/use-providers-oauth";
+import type { OAuthAccount } from "../src/pages/providers-shared";
+
+type AccountSet = { activeAccountId: string | null; accounts: OAuthAccount[] };
+
+describe("Mirasim OAuth cancellation roster reconciliation", () => {
+ test("keeps the committed roster until the authoritative backend refresh replaces it", async () => {
+ let state: Record = {
+ mirasim: {
+ activeAccountId: "ghost",
+ accounts: [{ id: "ghost", email: "masked@example.com", active: true }],
+ },
+ };
+ const setState = (next: Record | ((current: Record) => Record)) => {
+ state = typeof next === "function" ? next(state) : next;
+ };
+ const calls: string[] = [];
+
+ await reconcileCancelledOAuthProvider(
+ "mirasim",
+ setState,
+ async providers => {
+ calls.push(`accounts:${providers.join(",")}`);
+ state = {
+ ...state,
+ mirasim: { activeAccountId: null, accounts: [] },
+ };
+ return true;
+ },
+ async () => { calls.push("oauth"); },
+ );
+
+ expect(state.mirasim).toEqual({ activeAccountId: null, accounts: [] });
+ expect(calls).toEqual(["accounts:mirasim", "oauth"]);
+ });
+
+ test("keeps existing committed accounts when cancellation reconciliation cannot refresh", async () => {
+ let state: Record = {
+ anthropic: {
+ activeAccountId: "real",
+ accounts: [{ id: "real", active: true }],
+ },
+ };
+ const setState = (next: Record | ((current: Record) => Record)) => {
+ state = typeof next === "function" ? next(state) : next;
+ };
+
+ await reconcileCancelledOAuthProvider(
+ "anthropic",
+ setState,
+ async () => { throw new Error("network down"); },
+ async () => { throw new Error("network down"); },
+ );
+
+ expect(state.anthropic).toEqual({
+ activeAccountId: "real",
+ accounts: [{ id: "real", active: true }],
+ });
+ });
+
+ test("authoritative roster can repopulate a credential that committed before cancel won the race", async () => {
+ let state: Record = {
+ mirasim: {
+ activeAccountId: "ghost",
+ accounts: [{ id: "ghost", active: true }],
+ },
+ };
+ const setState = (next: Record | ((current: Record) => Record)) => {
+ state = typeof next === "function" ? next(state) : next;
+ };
+
+ await reconcileCancelledOAuthProvider(
+ "mirasim",
+ setState,
+ async () => {
+ state = {
+ ...state,
+ mirasim: {
+ activeAccountId: "committed",
+ accounts: [{ id: "committed", active: true }],
+ },
+ };
+ return true;
+ },
+ async () => {},
+ );
+
+ expect(state.mirasim.accounts.map(account => account.id)).toEqual(["committed"]);
+ expect(state.mirasim.activeAccountId).toBe("committed");
+ });
+});
diff --git a/gui/tests/mirasim-quota-presentation.test.ts b/gui/tests/mirasim-quota-presentation.test.ts
new file mode 100644
index 00000000000..6e0e52f6969
--- /dev/null
+++ b/gui/tests/mirasim-quota-presentation.test.ts
@@ -0,0 +1,43 @@
+import { expect, test } from "bun:test";
+import { buildQuotaRows } from "../src/components/QuotaBars";
+import { DICTS, type Locale, type TFn, type TKey } from "../src/i18n/shared";
+
+function tFor(locale: Locale): TFn {
+ return ((key: TKey) => DICTS[locale][key]) as TFn;
+}
+
+const CASES = [
+ {
+ locale: "en",
+ labels: ["5-hour limit", "Weekly limit", "Claude · Weekly limit", "Fable · Weekly limit", "Sonnet · Weekly limit"],
+ },
+ {
+ locale: "zh-TW",
+ labels: ["5 小時限額", "每週限額", "Claude · 每週限額", "Fable · 每週限額", "Sonnet · 每週限額"],
+ },
+ {
+ locale: "zh",
+ labels: ["5 小时限额", "每周限额", "Claude · 每周限额", "Fable · 每周限额", "Sonnet · 每周限额"],
+ },
+] as const satisfies readonly { locale: Locale; labels: readonly string[] }[];
+
+for (const { locale, labels } of CASES) {
+ test(`Mirasim quota rows are localized without raw internal labels in ${locale}`, () => {
+ const rows = buildQuotaRows({
+ fiveHourPercent: 0.4,
+ fiveHourResetAt: 1_800_000_000_000,
+ weeklyPercent: 0.1,
+ weeklyResetAt: 1_900_000_000_000,
+ customWindows: [
+ { label: "Model · 7d_claude", percent: 0.5, resetAt: 1_900_000_000_000 },
+ { label: "Model · 7d_fable", percent: 0, resetAt: 1_900_000_000_000 },
+ { label: "Model · 7d-sonnet", percent: 0.25, resetAt: 1_900_000_000_000 },
+ ],
+ updatedAt: Date.now(),
+ }, null, tFor(locale));
+
+ expect(rows.map(row => row.limitLabel)).toEqual(labels);
+ expect(rows.some(row => row.limitLabel === "5h" || row.limitLabel === "7d")).toBe(false);
+ expect(rows.some(row => row.limitLabel.startsWith("Model ·"))).toBe(false);
+ });
+}
diff --git a/gui/tests/models-empty-provider.test.tsx b/gui/tests/models-empty-provider.test.tsx
index 55f01e0d3e8..b6b771fb50a 100644
--- a/gui/tests/models-empty-provider.test.tsx
+++ b/gui/tests/models-empty-provider.test.tsx
@@ -589,6 +589,15 @@ test("failed HTTP discovery renders an amber status badge and reason", () => {
expect(html).toContain('class="link-btn"');
});
+test("HTTP 429 discovery renders a rate-limited badge and reason", () => {
+ const html = renderHint(true, { status: "failed", reason: "http", httpStatus: 429 });
+ expect(html).toContain("Discovery limited");
+ expect(html).toContain("temporarily rate-limited");
+ expect(html).toContain("HTTP 429");
+ expect(html).toContain('class="badge badge-amber"');
+ expect(html).not.toContain(">Discovery failed<");
+});
+
test("failed discovery renders each server-owned reason without provider detail", () => {
const cases: Array<[ProviderDiscoverySummary, string]> = [
[{ status: "failed", reason: "blocked" }, "blocked by the destination policy"],
diff --git a/gui/tests/provider-account-quota-loading.test.tsx b/gui/tests/provider-account-quota-loading.test.tsx
index d13fc9040a2..d7231518569 100644
--- a/gui/tests/provider-account-quota-loading.test.tsx
+++ b/gui/tests/provider-account-quota-loading.test.tsx
@@ -214,6 +214,86 @@ afterEach(async () => {
});
for (const kind of ["oauth", "api-key"] as const) {
+ test(`${kind} selection refresh that wins before the first quota load keeps new probe rows pending`, async () => {
+ const firstBase = deferred();
+ const quota = deferred();
+ const quotaStarted = deferred();
+ const rows = ["a", "b"].map(id => ({ id, masked: id, active: id === "a", quotaMode: "probe" as const }));
+ let baseReads = 0;
+ respond = async url => {
+ if (url.includes("quota=1")) { quotaStarted.resolve(); return quota.promise; }
+ if (++baseReads === 1) return firstBase.promise;
+ return Response.json({ activeAccountId: "a", activeId: "a", accounts: rows, keys: rows });
+ };
+
+ let full!: Promise;
+ await act(async () => {
+ full = kind === "oauth" ? pools.fetchAccountSets(["fixture"], true) : pools.fetchKeyPools(["fixture"], true);
+ });
+ await act(async () => {
+ expect(await pools.refreshAccountRosters({ provider: "fixture", kind })).toBe(true);
+ });
+
+ const current = () => kind === "oauth" ? pools.accountSets.fixture.accounts : pools.keyPools.fixture;
+ expect(current()).toHaveLength(2);
+ expect(current()[0]).toMatchObject({ quotaPending: true, quotaUnavailable: false });
+ expect(current()[0].quota).toBeUndefined();
+
+ await act(async () => {
+ firstBase.resolve(Response.json({ activeAccountId: "a", activeId: "a", accounts: rows, keys: rows }));
+ await quotaStarted.promise;
+ });
+ expect(current()[0]).toMatchObject({ quotaPending: true, quotaUnavailable: false });
+
+ const enriched = rows.map(row => ({ ...row, quota: reading, quotaUnavailable: false }));
+ await act(async () => {
+ quota.resolve(Response.json({ activeAccountId: "a", activeId: "a", accounts: enriched, keys: enriched }));
+ expect(await full).toBe(true);
+ });
+ expect(current()[0]).toMatchObject({ quota: reading, quotaPending: false, quotaUnavailable: false });
+ });
+
+ for (const outcome of ["success", "http-error"] as const) {
+ test(`${kind} membership added after quota probe start is not left pending (${outcome})`, async () => {
+ const quota = deferred();
+ const quotaStarted = deferred();
+ const original = [{ id: "a", masked: "a", active: true, quotaMode: "probe" as const }];
+ const latest = [...original, { id: "new", masked: "new", active: false, quotaMode: "probe" as const }];
+ let baseReads = 0;
+ respond = async url => {
+ if (url.includes("quota=1")) { quotaStarted.resolve(); return quota.promise; }
+ const rows = ++baseReads === 1 ? original : latest;
+ return Response.json({ activeAccountId: "a", activeId: "a", accounts: rows, keys: rows });
+ };
+
+ let full!: Promise;
+ await act(async () => {
+ full = kind === "oauth" ? pools.fetchAccountSets(["fixture"], true) : pools.fetchKeyPools(["fixture"], true);
+ await quotaStarted.promise;
+ });
+ await act(async () => {
+ expect(await pools.refreshAccountRosters({ provider: "fixture", kind })).toBe(true);
+ });
+
+ const current = () => kind === "oauth" ? pools.accountSets.fixture.accounts : pools.keyPools.fixture;
+ expect(current().map(row => row.id)).toEqual(["a", "new"]);
+
+ await act(async () => {
+ quota.resolve(outcome === "success"
+ ? Response.json({ activeAccountId: "a", activeId: "a", accounts: [{ ...original[0], quota: reading }], keys: [{ ...original[0], quota: reading }] })
+ : new Response(null, { status: 503 }));
+ expect(await full).toBe(outcome === "success");
+ });
+
+ expect(current()[1]).toMatchObject({ quotaPending: false, quotaUnavailable: false });
+ expect(current()[1].quota).toBeUndefined();
+
+ respond = async () => Response.json({ activeAccountId: "a", activeId: "a", accounts: latest, keys: latest });
+ await act(async () => { expect(await pools.refreshAccountRosters({ provider: "fixture", kind })).toBe(true); });
+ expect(current()[1]).toMatchObject({ quotaPending: false, quotaUnavailable: false });
+ });
+ }
+
test(`${kind} push refresh overtakes a slow quota probe while late quota cannot change selection or resurrect rows`, async () => {
const quota = deferred();
const started = deferred();
diff --git a/gui/tests/provider-icons.test.ts b/gui/tests/provider-icons.test.ts
index 704c2978d69..58e0c49a470 100644
--- a/gui/tests/provider-icons.test.ts
+++ b/gui/tests/provider-icons.test.ts
@@ -73,6 +73,7 @@ test("the MiniMax and Xiaomi MiMo provider ids resolve to their brand's mark", (
expect(providerIconSrc("mimo")).toBe("/provider-icons/xiaomi-color.svg");
// The precedent that makes the two above consistent rather than novel.
expect(providerIconSrc("mimo-free")).toBe("/provider-icons/xiaomi-color.svg");
+ expect(providerIconSrc("mirasim")).toBe("/provider-icons/mirasim.svg");
});
/*
diff --git a/gui/tests/provider-workspace-auth.test.ts b/gui/tests/provider-workspace-auth.test.ts
new file mode 100644
index 00000000000..55938f29ff3
--- /dev/null
+++ b/gui/tests/provider-workspace-auth.test.ts
@@ -0,0 +1,26 @@
+import { expect, test } from "bun:test";
+import {
+ oauthAccountDisplayLabel,
+ oauthAccountSecondaryIdentity,
+} from "../src/provider-workspace/auth";
+import type { TFn } from "../src/i18n/shared";
+
+const t = ((key: string, vars?: Record) => {
+ if (key === "prov.accountId") return "ID";
+ if (key === "pws.accountOrdinal") return `Account ${vars?.count ?? "1"}`;
+ return key;
+}) as TFn;
+
+test("OAuth account identity does not repeat an email when there is no alias", () => {
+ const account = { id: "account-1234", email: "user@example.test" };
+ expect(oauthAccountDisplayLabel([account], account, t)).toBe("user@example.test");
+ expect(oauthAccountSecondaryIdentity(account, "account-…1234", t)).toBe("ID: account-…1234");
+});
+
+test("OAuth account identity keeps the email below an explicit alias", () => {
+ const account = { id: "account-1234", email: "user@example.test", alias: "Work" };
+ expect(oauthAccountDisplayLabel([account], account, t)).toBe("Work");
+ expect(oauthAccountSecondaryIdentity(account, "account-…1234", t)).toBe(
+ "user@example.test · ID: account-…1234",
+ );
+});
diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json
index 1ce21b6af2a..8e5eba5911b 100644
--- a/scripts/test-layout/layout.json
+++ b/scripts/test-layout/layout.json
@@ -1994,6 +1994,6 @@
"server-combo-cooldown-recording.test.ts": "server",
"injection-routing-drift.test.ts": "codex-integration", "injection-routing-healer.test.ts": "codex-integration",
"injection-routing-heal-apply.test.ts": "codex-integration", "cli-start-routing-heal-wiring.test.ts": "cli",
- "cli-status-codex-routing-drift.test.ts": "cli"
+ "cli-status-codex-routing-drift.test.ts": "cli", "mirasim-control-plane.test.ts": "providers", "mirasim-crypto.test.ts": "providers", "mirasim-endpoints.test.ts": "providers", "mirasim-oauth.test.ts": "providers", "mirasim-provider.test.ts": "providers", "mirasim-recovery.test.ts": "providers", "mirasim-transport.test.ts": "providers", "mirasim-compact-endpoint.test.ts": "responses"
}
}
diff --git a/src/adapters/base.ts b/src/adapters/base.ts
index 431e3fb1309..91066dce964 100644
--- a/src/adapters/base.ts
+++ b/src/adapters/base.ts
@@ -1,6 +1,6 @@
import type { AdapterEvent, OcxParsedRequest } from "../types";
import type { TranslatorBudget } from "../lib/translator-budget";
-import type { RequestExecutionBudget } from "../lib/request-execution-budget";
+import type { RequestExecutionBudget, SendClass } from "../lib/request-execution-budget";
import type { AttemptRecoveryKind, AttemptRecoveryWithheld } from "../usage/log";
import type { AdapterTierMetadata } from "../providers/fastwire";
import type { ProviderRequestSlot } from "../providers/request-pacing";
@@ -54,6 +54,13 @@ export interface IncomingMeta {
export interface ProviderAdapter {
name: string;
+ /**
+ * Native Responses passthrough capability. Fixed-wire adapters set `passthrough`; mixed-wire
+ * adapters may decide from the routed request before any upstream request is built.
+ */
+ passthrough?: boolean;
+ passthroughFor?(parsed: OcxParsedRequest): boolean;
+
/**
* This adapter reports every physical inference send through `IncomingMeta.onPhysicalSend`,
* including its first.
@@ -120,6 +127,13 @@ export interface ProviderAdapter {
tierLogForRunTurn?(parsed: OcxParsedRequest): AdapterTierMetadata | undefined;
}
+export function adapterIsPassthrough(
+ adapter: ProviderAdapter,
+ parsed: OcxParsedRequest,
+): boolean {
+ return adapter.passthrough === true || adapter.passthroughFor?.(parsed) === true;
+}
+
export interface AdapterRequest {
url: string;
method: string;
@@ -190,6 +204,10 @@ export interface AdapterFetchContext {
* adapter entry as one send is how a nested 3x3 ladder stayed invisible to a request cap.
*/
sendBudget?: RequestExecutionBudget;
+ /** Default send class for the first physical send of an adapter-owned recovery leg. */
+ sendClass?: SendClass;
+ /** Default recovery label for physical sends that do not declare a narrower inner recovery. */
+ recovery?: AttemptRecoveryKind;
/**
* Observes every physical upstream send this adapter makes, including its own inner retries.
*
diff --git a/src/adapters/mirasim.ts b/src/adapters/mirasim.ts
new file mode 100644
index 00000000000..7a0112471e9
--- /dev/null
+++ b/src/adapters/mirasim.ts
@@ -0,0 +1,347 @@
+import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../types";
+import type { TranslatorBudget } from "../lib/translator-budget";
+import type { AdapterTierMetadata } from "../providers/fastwire";
+import { createAnthropicAdapter } from "./anthropic";
+import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
+import { createResponsesPassthroughAdapter } from "./openai-responses";
+import {
+ fetchMirasim,
+ MIRASIM_INTERNAL_AGENT_HEADER,
+ MIRASIM_INTERNAL_THREAD_HEADER,
+ MIRASIM_INTERNAL_WIRE_HEADER,
+} from "./mirasim/transport";
+import { cachedMirasimThinkingShape } from "./mirasim/control-plane";
+import {
+ ensureMirasimClaudeAgentSystemMarker,
+ mergeMirasimAnthropicBetaHeaders,
+} from "./mirasim/anthropic";
+
+type MirasimWire = "anthropic" | "responses";
+
+const RESPONSE_WIRE_HEADER = "x-opencodex-mirasim-response-wire";
+const MIRASIM_NATIVE_EXEC_TOOLS = new Set(["exec"]);
+
+function usesNativeMirasimExec(modelId: string): boolean {
+ return /^gpt-6(?:-|$)/i.test(modelId) || /^gpt-5\.6-sol$/i.test(modelId);
+}
+
+function parseMirasimModelSelector(modelId: string): { modelId: string; longContext: boolean } {
+ const trimmed = modelId.trim();
+ const longContext = /\[1m\]$/i.test(trimmed);
+ return {
+ modelId: longContext ? trimmed.replace(/\[1m\]$/i, "").trim() : trimmed,
+ longContext,
+ };
+}
+
+function wireForModel(modelId: string): MirasimWire {
+ const normalized = parseMirasimModelSelector(modelId).modelId.toLowerCase();
+ if (normalized.startsWith("claude-")) return "anthropic";
+ if (normalized.startsWith("gpt-") || normalized === "kimi-k3") return "responses";
+ throw new Error(`Mirasim supports Claude, GPT, and Kimi K3 relay models only (received ${modelId})`);
+}
+
+function requestWire(request: AdapterRequest): MirasimWire {
+ const declared = Object.entries(request.headers)
+ .find(([name]) => name.toLowerCase() === MIRASIM_INTERNAL_WIRE_HEADER)?.[1];
+ if (declared === "anthropic" || declared === "responses") return declared;
+ const path = new URL(request.url).pathname;
+ return path.startsWith("/v1/messages") ? "anthropic" : "responses";
+}
+
+function responseWire(response: Response): MirasimWire {
+ const wire = response.headers.get(RESPONSE_WIRE_HEADER);
+ if (wire === "anthropic" || wire === "responses") return wire;
+ throw new Error("Mirasim response is missing its internal wire marker");
+}
+
+function isMirasimAnthropicTerminal(event: AdapterEvent): boolean {
+ return event.type === "done" || event.type === "incomplete" || event.type === "error";
+}
+
+function markResponseWire(response: Response, wire: MirasimWire): Response {
+ const headers = new Headers(response.headers);
+ headers.set(RESPONSE_WIRE_HEADER, wire);
+ return new Response(response.body, {
+ status: response.status,
+ statusText: response.statusText,
+ headers,
+ });
+}
+
+function threadIdentity(parsed: OcxParsedRequest): string | undefined {
+ return parsed._clientThreadId ?? parsed._codexOwnThreadId;
+}
+
+function relayEffortForBudget(budget: number): string {
+ if (budget <= 1_024) return "low";
+ if (budget <= 8_192) return "medium";
+ if (budget <= 24_576) return "high";
+ return "xhigh";
+}
+
+function relayBudgetForEffort(effort: string): number | undefined {
+ switch (effort) {
+ case "low": return 1_024;
+ case "medium": return 8_192;
+ case "high": return 24_576;
+ case "xhigh": return 32_768;
+ case "max": return 128_000;
+ default: return undefined;
+ }
+}
+
+function normalizedRelayEffort(value: unknown): string | undefined {
+ if (typeof value !== "string") return undefined;
+ const effort = value.trim().toLowerCase();
+ if (effort === "minimal") return "low";
+ if (effort === "ultra") return "max";
+ return ["low", "medium", "high", "xhigh", "max"].includes(effort) ? effort : undefined;
+}
+
+/**
+ * The inspected Mirasim 0.0.336 fallback catalog treats every Claude model as adaptive. OpenCodex's
+ * native Anthropic adapter deliberately uses Anthropic's own per-model shape, where e.g. Haiku 4.5
+ * and Opus 4.6 can serialize a token budget. Normalize that already-translated body at this final
+ * provider boundary so the shared translator remains correct for api.anthropic.com as well.
+ */
+function normalizeMirasimWireBody(
+ bodyText: string,
+ wire: MirasimWire,
+ requestedReasoning: string | undefined,
+ claudeShape: "adaptive" | "budget" | undefined,
+): string {
+ let body: Record;
+ try {
+ const parsed: unknown = JSON.parse(bodyText);
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return bodyText;
+ body = parsed as Record;
+ } catch {
+ return bodyText;
+ }
+
+ if (wire === "responses") {
+ // Mirasim's Codex lane is not an ordinary Responses-compatible endpoint. The inspected
+ // reference client always drives it as a stateless SSE turn, even when the caller requested a
+ // bounded JSON response. Mirror that exact wire contract here; the Responses delivery layer
+ // converts the terminal SSE snapshot back to JSON for non-streaming callers.
+ body.stream = true;
+ body.store = false;
+ body.parallel_tool_calls = true;
+ body.include = ["reasoning.encrypted_content"];
+ const reasoning = body.reasoning;
+ if (reasoning && typeof reasoning === "object" && !Array.isArray(reasoning)) {
+ const record = reasoning as Record;
+ const effort = normalizedRelayEffort(record.effort);
+ if (effort) record.effort = effort;
+ }
+ if (typeof body.reasoning_effort === "string") {
+ const effort = normalizedRelayEffort(body.reasoning_effort);
+ if (effort) {
+ const current = body.reasoning;
+ body.reasoning = {
+ ...(current && typeof current === "object" && !Array.isArray(current)
+ ? current as Record
+ : {}),
+ effort,
+ };
+ delete body.reasoning_effort;
+ }
+ }
+ return JSON.stringify(body);
+ }
+
+ ensureMirasimClaudeAgentSystemMarker(body);
+
+ const requested = requestedReasoning?.trim().toLowerCase();
+ const thinking = body.thinking && typeof body.thinking === "object" && !Array.isArray(body.thinking)
+ ? body.thinking as Record
+ : undefined;
+ const outputConfig = body.output_config && typeof body.output_config === "object" && !Array.isArray(body.output_config)
+ ? body.output_config as Record
+ : {};
+
+ const adaptive = claudeShape !== "budget";
+ const applyBudget = (budget: number): void => {
+ const maxTokens = typeof body.max_tokens === "number" && Number.isFinite(body.max_tokens)
+ ? Math.floor(body.max_tokens)
+ : undefined;
+ let bounded = Math.max(1_024, Math.floor(budget));
+ if (maxTokens !== undefined && bounded >= maxTokens) {
+ bounded = maxTokens - 1;
+ if (bounded < 1_024) {
+ throw new Error("Mirasim Claude thinking budget must be at least 1024 and below max_tokens");
+ }
+ }
+ body.thinking = { type: "enabled", budget_tokens: bounded };
+ delete outputConfig.effort;
+ delete body.temperature;
+ delete body.top_p;
+ };
+
+ if (requested === "none") {
+ body.thinking = { type: "disabled" };
+ delete outputConfig.effort;
+ } else if (requested === "auto") {
+ if (adaptive) {
+ body.thinking = { type: "adaptive" };
+ delete outputConfig.effort;
+ delete body.temperature;
+ delete body.top_p;
+ } else {
+ applyBudget(1_024);
+ }
+ } else {
+ let effort = normalizedRelayEffort(requested);
+ if (!effort && thinking?.type === "enabled" && typeof thinking.budget_tokens === "number") {
+ effort = relayEffortForBudget(thinking.budget_tokens);
+ }
+ if (!effort && thinking?.type === "adaptive") {
+ effort = normalizedRelayEffort(outputConfig.effort);
+ }
+ if (adaptive) {
+ if (effort) {
+ body.thinking = { type: "adaptive" };
+ outputConfig.effort = effort;
+ delete body.temperature;
+ delete body.top_p;
+ }
+ } else {
+ let budget = effort ? relayBudgetForEffort(effort) : undefined;
+ if (budget === undefined && thinking?.type === "enabled" && typeof thinking.budget_tokens === "number") {
+ budget = thinking.budget_tokens;
+ }
+ if (budget !== undefined) applyBudget(budget);
+ }
+ }
+
+ if (Object.keys(outputConfig).length > 0) body.output_config = outputConfig;
+ else delete body.output_config;
+ return JSON.stringify(body);
+}
+
+/**
+ * Mirasim is a transport adapter, not a third protocol translator. Claude requests are serialized
+ * by the existing Anthropic adapter; GPT and Kimi K3 requests are serialized by the existing
+ * Responses adapter. This wrapper owns only model->wire selection and Mirasim's signed transport.
+ */
+export function createMirasimAdapter(provider: OcxProviderConfig): ProviderAdapter {
+ if (typeof provider.apiKey !== "string" || provider.apiKey.trim() === "") {
+ throw new Error("Mirasim OAuth token missing - run ocx login mirasim");
+ }
+
+ // Mirasim authenticates the relay itself. Delegate serializers therefore run as ordinary API-key
+ // destinations so Anthropic subscription-OAuth-only prompt/header mutations are not injected.
+ const anthropic = createAnthropicAdapter({
+ ...provider,
+ adapter: "anthropic",
+ authMode: "key",
+ apiKey: provider.apiKey,
+ });
+ const responsesProvider = {
+ ...provider,
+ adapter: "openai-responses" as const,
+ authMode: "key" as const,
+ apiKey: provider.apiKey,
+ upstreamWebsocket: false,
+ };
+ const responses = createResponsesPassthroughAdapter(responsesProvider);
+ const nativeExecResponses = createResponsesPassthroughAdapter(responsesProvider, {
+ routedCustomToolPassthroughNames: MIRASIM_NATIVE_EXEC_TOOLS,
+ });
+
+ const parser = (wire: MirasimWire, modelId?: string): ProviderAdapter => {
+ if (wire === "anthropic") return anthropic;
+ return modelId && usesNativeMirasimExec(modelId) ? nativeExecResponses : responses;
+ };
+
+ return {
+ name: "mirasim",
+ passthroughFor(parsed) {
+ return wireForModel(parsed.modelId) === "responses";
+ },
+ // fetchMirasim uses createAdapterPhysicalSend, so it owns first-send admission/observation.
+ reportsPhysicalSends: true,
+
+ async buildRequest(parsed, incoming) {
+ const selected = parseMirasimModelSelector(parsed.modelId);
+ const wire = wireForModel(selected.modelId);
+ const delegate = parser(wire, selected.modelId);
+ const wireParsed = selected.modelId === parsed.modelId
+ ? parsed
+ : { ...parsed, modelId: selected.modelId };
+ const request = await delegate.buildRequest(wireParsed, incoming);
+ if (wire === "anthropic") {
+ mergeMirasimAnthropicBetaHeaders(request.headers, incoming?.headers, selected.longContext);
+ }
+ const claudeShape = wire === "anthropic"
+ ? cachedMirasimThinkingShape(provider.apiKey!, selected.modelId)
+ : undefined;
+ request.body = normalizeMirasimWireBody(
+ request.body,
+ wire,
+ parsed.options.reasoning,
+ claudeShape,
+ );
+ request.headers[MIRASIM_INTERNAL_WIRE_HEADER] = wire;
+ // Relay metadata describes the originating client, not the translated target wire.
+ // A Responses caller targeting Claude serializes onto /v1/messages but remains Codex-originated.
+ if (wire === "anthropic" && parsed._rawBody !== undefined) {
+ request.headers[MIRASIM_INTERNAL_AGENT_HEADER] = "codex";
+ }
+ const thread = threadIdentity(parsed);
+ if (thread) request.headers[MIRASIM_INTERNAL_THREAD_HEADER] = thread;
+ return request;
+ },
+
+ async fetchResponse(request: AdapterRequest, ctx?: AdapterFetchContext): Promise {
+ const wire = requestWire(request);
+ const response = await fetchMirasim(request, provider.apiKey!, ctx);
+ // Both wires carry an internal marker because routed compaction deliberately leaves the
+ // native passthrough lane and re-enters the adapter parser. The public passthrough response
+ // boundary strips this private header before client delivery.
+ return markResponseWire(response, wire);
+ },
+
+ async *parseStream(
+ response: Response,
+ budget: TranslatorBudget,
+ tierMetadata?: AdapterTierMetadata,
+ ): AsyncGenerator {
+ const wire = responseWire(response);
+ const delegate = parser(wire);
+ for await (const event of delegate.parseStream(response, budget, tierMetadata)) {
+ yield event;
+ if (wire === "anthropic" && isMirasimAnthropicTerminal(event)) {
+ // Mirasim's relay may keep the HTTP/SSE transport open after Anthropic's
+ // application-level message_stop. The shared Anthropic parser deliberately supports
+ // providers that need EOF fallback semantics, so normalize only this provider boundary:
+ // once a terminal semantic event is visible, closing this wrapper triggers iterator
+ // cleanup and cancels the still-open relay body instead of making web-search wait for
+ // transport EOF and trip its post-terminal drain guard.
+ return;
+ }
+ }
+ },
+
+ async parseResponse(
+ response: Response,
+ budget: TranslatorBudget,
+ tierMetadata?: AdapterTierMetadata,
+ ): Promise {
+ const delegate = parser(responseWire(response));
+ if (!delegate.parseResponse) {
+ const events: AdapterEvent[] = [];
+ for await (const event of delegate.parseStream(response, budget, tierMetadata)) events.push(event);
+ return events;
+ }
+ return delegate.parseResponse(response, budget, tierMetadata);
+ },
+
+ formatErrorBody(status: number, headers: Headers, payloadText: string): string {
+ const wire = headers.get(RESPONSE_WIRE_HEADER);
+ const delegate = wire === "anthropic" ? anthropic : responses;
+ return delegate.formatErrorBody?.(status, headers, payloadText) ?? payloadText;
+ },
+ };
+}
diff --git a/src/adapters/mirasim/anthropic.ts b/src/adapters/mirasim/anthropic.ts
new file mode 100644
index 00000000000..986a8e71a91
--- /dev/null
+++ b/src/adapters/mirasim/anthropic.ts
@@ -0,0 +1,251 @@
+export const MIRASIM_LONG_CONTEXT_BETA = "context-1m-2025-08-07";
+export const MIRASIM_CLAUDE_AGENT_SYSTEM_MARKER =
+ "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
+const MIRASIM_CLAUDE_CACHE_BREAKPOINT_LIMIT = 4;
+
+type AnthropicSystemBlock = {
+ type: "text";
+ text: string;
+ [key: string]: unknown;
+};
+
+type CacheControl = {
+ type: "ephemeral";
+ ttl?: "1h" | "5m";
+};
+
+function isSystemTextBlock(value: unknown): value is AnthropicSystemBlock {
+ return !!value
+ && typeof value === "object"
+ && !Array.isArray(value)
+ && (value as Record).type === "text"
+ && typeof (value as Record).text === "string";
+}
+
+function normalizedCacheControl(value: unknown): CacheControl | undefined {
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
+ const record = value as Record;
+ if (record.type !== "ephemeral") return undefined;
+ const ttl = record.ttl;
+ if (ttl === "1h" || ttl === "5m") return { type: "ephemeral", ttl };
+ return { type: "ephemeral" };
+}
+
+function preferredMirasimCacheControl(body: Record): CacheControl {
+ const candidates: unknown[] = [];
+ const system = body.system;
+ if (Array.isArray(system)) {
+ for (const block of system) {
+ if (block && typeof block === "object" && !Array.isArray(block)) {
+ candidates.push((block as Record).cache_control);
+ }
+ }
+ }
+ const tools = body.tools;
+ if (Array.isArray(tools)) {
+ for (const tool of tools) {
+ if (tool && typeof tool === "object" && !Array.isArray(tool)) {
+ candidates.push((tool as Record).cache_control);
+ }
+ }
+ }
+ const messages = body.messages;
+ if (Array.isArray(messages)) {
+ for (const message of messages) {
+ if (!message || typeof message !== "object" || Array.isArray(message)) continue;
+ const content = (message as Record).content;
+ if (!Array.isArray(content)) continue;
+ for (const block of content) {
+ if (block && typeof block === "object" && !Array.isArray(block)) {
+ candidates.push((block as Record).cache_control);
+ }
+ }
+ }
+ }
+ for (const candidate of candidates) {
+ const normalized = normalizedCacheControl(candidate);
+ if (normalized) return normalized;
+ }
+ return { type: "ephemeral" };
+}
+
+function messageCacheCarriers(body: Record): Array> {
+ const carriers: Array> = [];
+ const messages = body.messages;
+ if (!Array.isArray(messages)) return carriers;
+ for (const message of messages) {
+ if (!message || typeof message !== "object" || Array.isArray(message)) continue;
+ const content = (message as Record).content;
+ if (!Array.isArray(content)) continue;
+ for (const block of content) {
+ if (!block || typeof block !== "object" || Array.isArray(block)) continue;
+ const record = block as Record;
+ if (record.cache_control) carriers.push(record);
+ if (record.type !== "tool_result" || !Array.isArray(record.content)) continue;
+ for (const nested of record.content) {
+ if (!nested || typeof nested !== "object" || Array.isArray(nested)) continue;
+ const nestedRecord = nested as Record;
+ if (nestedRecord.cache_control) carriers.push(nestedRecord);
+ }
+ }
+ }
+ return carriers;
+}
+
+function systemCacheCarriers(
+ body: Record,
+ marker: AnthropicSystemBlock,
+): Array> {
+ const system = body.system;
+ if (!Array.isArray(system)) return [];
+ return system
+ .filter((block): block is Record =>
+ !!block && typeof block === "object" && !Array.isArray(block) && block !== marker
+ && !!(block as Record).cache_control)
+ .map(block => block as Record);
+}
+
+function toolCacheCarriers(body: Record): Array> {
+ const tools = body.tools;
+ if (!Array.isArray(tools)) return [];
+ return tools
+ .filter((tool): tool is Record =>
+ !!tool && typeof tool === "object" && !Array.isArray(tool)
+ && !!(tool as Record).cache_control)
+ .map(tool => tool as Record);
+}
+
+function countMirasimCacheBreakpoints(
+ body: Record,
+ marker: AnthropicSystemBlock,
+): number {
+ return 1
+ + messageCacheCarriers(body).length
+ + systemCacheCarriers(body, marker).length
+ + toolCacheCarriers(body).length;
+}
+
+function enforceMirasimClaudeCacheBreakpointLimit(
+ body: Record,
+ marker: AnthropicSystemBlock,
+): void {
+ let excess = countMirasimCacheBreakpoints(body, marker) - MIRASIM_CLAUDE_CACHE_BREAKPOINT_LIMIT;
+ if (excess <= 0) return;
+
+ // The relay normalizes the Claude Agent marker itself. Keep that marker cacheable and shed
+ // the oldest message breakpoints first, preserving the newest conversation prefix. Only if a
+ // caller already supplied more than four stable breakpoints do we fall back to non-marker
+ // system blocks and finally tools.
+ for (const carrier of messageCacheCarriers(body)) {
+ if (excess <= 0) return;
+ delete carrier.cache_control;
+ excess--;
+ }
+ for (const carrier of systemCacheCarriers(body, marker)) {
+ if (excess <= 0) return;
+ delete carrier.cache_control;
+ excess--;
+ }
+ for (const carrier of toolCacheCarriers(body)) {
+ if (excess <= 0) return;
+ delete carrier.cache_control;
+ excess--;
+ }
+}
+
+/**
+ * Mirasim's Claude relay rejects otherwise-valid generic Messages requests unless the system
+ * prompt identifies the request as a Claude Agent SDK turn. Keep that provider-specific contract
+ * at the Mirasim boundary instead of contaminating the shared Anthropic serializer.
+ *
+ * Preserve the caller's system prompt byte-for-byte as its own block and prepend only the minimum
+ * marker accepted by the relay. Do not spoof Claude Code billing/version headers.
+ */
+export function ensureMirasimClaudeAgentSystemMarker(body: Record): void {
+ const cacheControl = preferredMirasimCacheControl(body);
+ const current = body.system;
+ if (typeof current === "string") {
+ const marker: AnthropicSystemBlock = {
+ type: "text",
+ text: MIRASIM_CLAUDE_AGENT_SYSTEM_MARKER,
+ cache_control: cacheControl,
+ };
+ body.system = current === MIRASIM_CLAUDE_AGENT_SYSTEM_MARKER
+ ? [marker]
+ : [marker, { type: "text", text: current }];
+ enforceMirasimClaudeCacheBreakpointLimit(body, marker);
+ return;
+ }
+
+ if (Array.isArray(current)) {
+ const existing = current.find(
+ block => isSystemTextBlock(block) && block.text === MIRASIM_CLAUDE_AGENT_SYSTEM_MARKER,
+ );
+ let marker: AnthropicSystemBlock;
+ if (isSystemTextBlock(existing)) {
+ marker = existing;
+ marker.cache_control = normalizedCacheControl(marker.cache_control) ?? cacheControl;
+ } else {
+ marker = {
+ type: "text",
+ text: MIRASIM_CLAUDE_AGENT_SYSTEM_MARKER,
+ cache_control: cacheControl,
+ };
+ body.system = [
+ marker,
+ ...current,
+ ];
+ }
+ enforceMirasimClaudeCacheBreakpointLimit(body, marker);
+ return;
+ }
+
+ const marker: AnthropicSystemBlock = {
+ type: "text",
+ text: MIRASIM_CLAUDE_AGENT_SYSTEM_MARKER,
+ cache_control: cacheControl,
+ };
+ body.system = [marker];
+}
+
+function safeBetaValue(value: string | null | undefined): string | undefined {
+ if (!value || value.length > 4096 || /[\0\r\n]/.test(value)) return undefined;
+ return value;
+}
+
+export function mirasimAnthropicBetaValue(
+ values: readonly (string | null | undefined)[],
+ longContext: boolean,
+): string | undefined {
+ const tokens: string[] = [];
+ const seen = new Set();
+ const sources = [
+ ...values.map(safeBetaValue).filter((value): value is string => value !== undefined),
+ ...(longContext ? [MIRASIM_LONG_CONTEXT_BETA] : []),
+ ];
+ for (const value of sources) {
+ for (const token of value.split(",")) {
+ const clean = token.trim();
+ if (!clean || seen.has(clean)) continue;
+ seen.add(clean);
+ tokens.push(clean);
+ }
+ }
+ return tokens.length > 0 ? tokens.join(",") : undefined;
+}
+
+export function mergeMirasimAnthropicBetaHeaders(
+ requestHeaders: Record,
+ incoming: Headers | undefined,
+ longContext: boolean,
+): void {
+ const values: string[] = [];
+ for (const [name, value] of Object.entries(requestHeaders)) {
+ if (name.toLowerCase() !== "anthropic-beta") continue;
+ values.push(value);
+ delete requestHeaders[name];
+ }
+ values.push(incoming?.get("anthropic-beta") ?? "");
+ const merged = mirasimAnthropicBetaValue(values, longContext);
+ if (merged) requestHeaders["anthropic-beta"] = merged;
+}
diff --git a/src/adapters/mirasim/compact.ts b/src/adapters/mirasim/compact.ts
new file mode 100644
index 00000000000..2068b3f6578
--- /dev/null
+++ b/src/adapters/mirasim/compact.ts
@@ -0,0 +1,46 @@
+function plainRecord(value: unknown): Record | undefined {
+ return value !== null && typeof value === "object" && !Array.isArray(value)
+ ? value as Record
+ : undefined;
+}
+
+function normalizeWorkflowEffort(value: unknown): unknown {
+ return typeof value === "string" && value.trim().toLowerCase() === "ultra" ? "max" : value;
+}
+
+/**
+ * Mirasim's native Responses compact route consumes the official compact request shape rather
+ * than OpenCodex's local summary prompt. The official client normalizes its workflow-only
+ * "ultra" selector to the single-request wire effort "max" before signing.
+ */
+export function normalizeMirasimCompactBody(
+ body: Readonly>,
+ wireModelId: string,
+): Record {
+ const model = wireModelId.trim();
+ if (!model.toLowerCase().startsWith("gpt-")) {
+ throw new Error("Mirasim native compact is available only for GPT Responses models");
+ }
+
+ const normalized: Record = { ...body, model };
+ delete normalized.stream;
+
+ const reasoning = plainRecord(normalized.reasoning);
+ if (reasoning) {
+ normalized.reasoning = {
+ ...reasoning,
+ effort: normalizeWorkflowEffort(reasoning.effort),
+ };
+ }
+ if ("reasoning_effort" in normalized) {
+ normalized.reasoning_effort = normalizeWorkflowEffort(normalized.reasoning_effort);
+ }
+ const outputConfig = plainRecord(normalized.output_config);
+ if (outputConfig) {
+ normalized.output_config = {
+ ...outputConfig,
+ effort: normalizeWorkflowEffort(outputConfig.effort),
+ };
+ }
+ return normalized;
+}
diff --git a/src/adapters/mirasim/control-plane.ts b/src/adapters/mirasim/control-plane.ts
new file mode 100644
index 00000000000..a101b18f2f9
--- /dev/null
+++ b/src/adapters/mirasim/control-plane.ts
@@ -0,0 +1,442 @@
+import type { OcxProviderConfig } from "../../types";
+import type { ProviderQuota, ProviderQuotaWindow } from "../../providers/quota-types";
+import {
+ isValidModelDiscoveryModelId,
+ MODEL_DISCOVERY_MAX_MODELS,
+ MODEL_DISCOVERY_MAX_RESPONSE_BYTES,
+ readBoundedDiscoveryJson,
+} from "../../providers/model-discovery";
+import { fetchMirasimControl, mirasimCredentialCacheScope } from "./transport";
+
+const ROSTER_SUCCESS_TTL_MS = 10 * 60_000;
+const ROSTER_FAILURE_TTL_MS = 60_000;
+const CONTROL_TIMEOUT_MS = 8_000;
+const ALLOWED_EFFORTS = new Set(["low", "medium", "high", "xhigh", "max", "ultra"]);
+const DATED_MODEL_SUFFIX = /-20\d{6}$/;
+const RESERVED_MODEL_IDS = new Set(["*", "gpt-4o-mini", "gpt-4o-mini-openrouter"]);
+
+export interface MirasimRosterSpec {
+ id: string;
+ label?: string;
+ contextWindow: number;
+ maxOutput?: number;
+ autoCompactRatio?: number;
+ effort: string[];
+ adaptive: boolean;
+}
+
+export interface MirasimRoster {
+ version: string;
+ agents: {
+ claude: MirasimRosterSpec[];
+ codex: MirasimRosterSpec[];
+ };
+}
+
+export interface MirasimDiscoveredModel {
+ id: string;
+ object?: string;
+ created?: number;
+ ownedBy?: string;
+ contextWindow?: number;
+ maxOutputTokens?: number;
+ displayName?: string;
+ reasoningEfforts?: string[];
+ adaptiveThinking?: boolean;
+ autoCompactRatio?: number;
+}
+
+export type MirasimLiveCatalogResult =
+ | { ok: true; models: MirasimDiscoveredModel[]; roster?: MirasimRoster }
+ | {
+ ok: false;
+ reason: "auth" | "http" | "invalid_response" | "transport";
+ status?: number;
+ retryAfter?: string;
+ };
+
+interface RosterCacheEntry {
+ roster?: MirasimRoster;
+ nextCheckAt: number;
+}
+
+const rosterCache = new Map();
+
+function credentialCacheKey(accessToken: string): string {
+ return mirasimCredentialCacheScope(accessToken);
+}
+
+function plainRecord(value: unknown): Record | undefined {
+ return value !== null && typeof value === "object" && !Array.isArray(value)
+ ? value as Record
+ : undefined;
+}
+
+function finitePositive(value: unknown): number | undefined {
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
+}
+
+function parseEfforts(value: unknown): string[] {
+ if (!Array.isArray(value)) return [];
+ const result: string[] = [];
+ const seen = new Set();
+ for (const item of value) {
+ if (typeof item !== "string") continue;
+ const normalized = item.trim().toLowerCase();
+ if (!ALLOWED_EFFORTS.has(normalized) || seen.has(normalized)) continue;
+ seen.add(normalized);
+ result.push(normalized);
+ }
+ return result;
+}
+
+function parseRosterSpec(value: unknown, family: "claude" | "codex"): MirasimRosterSpec | undefined {
+ const row = plainRecord(value);
+ if (!row || typeof row.id !== "string") return undefined;
+ const id = row.id.trim().toLowerCase();
+ const expectedPrefix = family === "claude" ? "claude-" : "gpt-";
+ const contextWindow = finitePositive(row.contextWindow);
+ if (!id.startsWith(expectedPrefix) || id.endsWith("-paid") || !contextWindow) return undefined;
+ const maxOutput = finitePositive(row.maxOutput);
+ const autoCompactRatio = typeof row.autoCompactRatio === "number"
+ && Number.isFinite(row.autoCompactRatio)
+ && row.autoCompactRatio > 0
+ && row.autoCompactRatio <= 1
+ ? row.autoCompactRatio
+ : undefined;
+ const label = typeof row.label === "string" && row.label.trim() ? row.label.trim() : undefined;
+ return {
+ id,
+ ...(label ? { label } : {}),
+ contextWindow,
+ ...(maxOutput ? { maxOutput } : {}),
+ ...(autoCompactRatio ? { autoCompactRatio } : {}),
+ effort: parseEfforts(row.effort),
+ adaptive: row.adaptive === true,
+ };
+}
+
+export function parseMirasimRoster(value: unknown): MirasimRoster | undefined {
+ const envelope = plainRecord(value);
+ const agents = plainRecord(envelope?.agents);
+ if (!envelope || typeof envelope.version !== "string" || !envelope.version.trim() || !agents) return undefined;
+
+ const parseFamily = (family: "claude" | "codex"): MirasimRosterSpec[] => {
+ const rows = Array.isArray(agents[family]) ? agents[family] as unknown[] : [];
+ const out: MirasimRosterSpec[] = [];
+ const seen = new Set();
+ for (const row of rows) {
+ const spec = parseRosterSpec(row, family);
+ if (!spec || seen.has(spec.id)) continue;
+ seen.add(spec.id);
+ out.push(spec);
+ if (out.length >= MODEL_DISCOVERY_MAX_MODELS) break;
+ }
+ return out;
+ };
+
+ const claude = parseFamily("claude");
+ const codex = parseFamily("codex");
+ if (claude.length === 0 && codex.length === 0) return undefined;
+ return {
+ version: envelope.version.trim(),
+ agents: { claude, codex },
+ };
+}
+
+function rosterSpec(roster: MirasimRoster | undefined, modelId: string): MirasimRosterSpec | undefined {
+ if (!roster) return undefined;
+ const id = modelId.trim().toLowerCase().replace(/\[1m\]$/i, "");
+ return [...roster.agents.claude, ...roster.agents.codex].find(spec => spec.id === id);
+}
+
+export function cachedMirasimThinkingShape(
+ accessToken: string,
+ modelId: string,
+): "adaptive" | "budget" | undefined {
+ const spec = rosterSpec(rosterCache.get(credentialCacheKey(accessToken))?.roster, modelId);
+ return spec ? (spec.adaptive ? "adaptive" : "budget") : undefined;
+}
+
+export function cachedMirasimRoster(accessToken: string): MirasimRoster | undefined {
+ return rosterCache.get(credentialCacheKey(accessToken))?.roster;
+}
+
+async function fetchRoster(
+ providerName: string,
+ provider: OcxProviderConfig,
+ accessToken: string,
+ signal?: AbortSignal,
+): Promise {
+ const key = credentialCacheKey(accessToken);
+ const now = Date.now();
+ const cached = rosterCache.get(key);
+ if (cached && now < cached.nextCheckAt) return cached.roster;
+
+ try {
+ const response = await fetchMirasimControl(providerName, provider, accessToken, "/v1/model-roster", {
+ // The current relay authenticates roster discovery against the login credential itself.
+ // A device-session bearer is valid for inference and /models, but /model-roster rejects it.
+ credentialMode: "access-token",
+ signal,
+ timeoutMs: CONTROL_TIMEOUT_MS,
+ });
+ if (!response.ok) {
+ try { await response.body?.cancel(); } catch { /* already closed */ }
+ rosterCache.set(key, { roster: cached?.roster, nextCheckAt: now + ROSTER_FAILURE_TTL_MS });
+ return cached?.roster;
+ }
+ const bounded = await readBoundedDiscoveryJson(response, MODEL_DISCOVERY_MAX_RESPONSE_BYTES);
+ const roster = bounded.ok ? parseMirasimRoster(bounded.value) : undefined;
+ if (!roster) {
+ rosterCache.set(key, { roster: cached?.roster, nextCheckAt: now + ROSTER_FAILURE_TTL_MS });
+ return cached?.roster;
+ }
+ rosterCache.set(key, { roster, nextCheckAt: now + ROSTER_SUCCESS_TTL_MS });
+ return roster;
+ } catch {
+ rosterCache.set(key, { roster: cached?.roster, nextCheckAt: now + ROSTER_FAILURE_TTL_MS });
+ return cached?.roster;
+ }
+}
+
+interface RawCatalogModel {
+ id: string;
+ object?: string;
+ created?: number;
+ ownedBy?: string;
+ maxInputTokens?: number;
+}
+
+function parseRawCatalog(value: unknown): RawCatalogModel[] | undefined {
+ const envelope = plainRecord(value);
+ const source = Array.isArray(envelope?.data)
+ ? envelope!.data as unknown[]
+ : Array.isArray(envelope?.models)
+ ? envelope!.models as unknown[]
+ : undefined;
+ if (!source || source.length === 0 || source.length > MODEL_DISCOVERY_MAX_MODELS) return undefined;
+
+ const parsed: RawCatalogModel[] = [];
+ for (const item of source) {
+ if (typeof item === "string") {
+ const id = item.trim();
+ if (isValidModelDiscoveryModelId(id)) parsed.push({ id });
+ continue;
+ }
+ const row = plainRecord(item);
+ if (!row || typeof row.id !== "string") continue;
+ const id = row.id.trim();
+ if (!isValidModelDiscoveryModelId(id)) continue;
+ parsed.push({
+ id,
+ ...(typeof row.object === "string" && row.object ? { object: row.object } : {}),
+ ...(typeof row.created === "number" && Number.isFinite(row.created) ? { created: row.created } : {}),
+ ...(typeof row.owned_by === "string" && row.owned_by.trim() ? { ownedBy: row.owned_by.trim() } : {}),
+ ...(finitePositive(row.max_input_tokens) ? { maxInputTokens: finitePositive(row.max_input_tokens) } : {}),
+ });
+ }
+ if (parsed.length === 0) return undefined;
+
+ const undated = new Set(
+ parsed
+ .map(model => model.id)
+ .filter(id => !id.includes("/") && !DATED_MODEL_SUFFIX.test(id)),
+ );
+ const seen = new Set();
+ return parsed.filter(model => {
+ const id = model.id;
+ const normalized = id.toLowerCase();
+ if (seen.has(id) || RESERVED_MODEL_IDS.has(normalized) || id.includes("/") || normalized.endsWith("-paid")) return false;
+ if (DATED_MODEL_SUFFIX.test(id) && undated.has(id.replace(DATED_MODEL_SUFFIX, ""))) return false;
+ if (
+ !normalized.startsWith("claude-")
+ && !normalized.startsWith("gpt-")
+ && normalized !== "kimi-k3"
+ ) return false;
+ seen.add(id);
+ return true;
+ });
+}
+
+function overlayRoster(models: RawCatalogModel[], roster: MirasimRoster | undefined): MirasimDiscoveredModel[] {
+ const overlaid = models.map(model => {
+ const spec = rosterSpec(roster, model.id);
+ return {
+ id: model.id,
+ ...(model.object ? { object: model.object } : {}),
+ ...(model.created !== undefined ? { created: model.created } : {}),
+ ...(model.ownedBy ? { ownedBy: model.ownedBy } : {}),
+ ...(spec?.contextWindow
+ ? { contextWindow: spec.contextWindow }
+ : model.maxInputTokens
+ ? { contextWindow: model.maxInputTokens }
+ : {}),
+ ...(spec?.maxOutput ? { maxOutputTokens: spec.maxOutput } : {}),
+ ...(spec?.label ? { displayName: spec.label } : {}),
+ ...(spec?.effort.length ? { reasoningEfforts: [...spec.effort] } : {}),
+ ...(spec ? { adaptiveThinking: spec.adaptive } : {}),
+ ...(spec?.autoCompactRatio ? { autoCompactRatio: spec.autoCompactRatio } : {}),
+ };
+ });
+ const out = [...overlaid];
+ const seen = new Set(overlaid.map(model => model.id.toLowerCase()));
+ for (const model of overlaid) {
+ if (
+ !model.id.toLowerCase().startsWith("claude-")
+ || model.id.includes("[")
+ || (model.contextWindow ?? 0) < 1_000_000
+ ) continue;
+ const id = `${model.id}[1m]`;
+ if (seen.has(id.toLowerCase())) continue;
+ out.push({
+ ...model,
+ id,
+ ...(model.displayName ? { displayName: `${model.displayName} [1m]` } : {}),
+ });
+ seen.add(id.toLowerCase());
+ }
+ return out;
+}
+
+export async function fetchMirasimLiveCatalog(
+ providerName: string,
+ provider: OcxProviderConfig,
+ accessToken: string,
+ signal?: AbortSignal,
+): Promise {
+ try {
+ const response = await fetchMirasimControl(providerName, provider, accessToken, "/v1/models", {
+ signal,
+ timeoutMs: CONTROL_TIMEOUT_MS,
+ });
+ if (!response.ok) {
+ const status = response.status;
+ const retryAfter = response.headers.get("retry-after")?.trim() || undefined;
+ try { await response.body?.cancel(); } catch { /* already closed */ }
+ return {
+ ok: false,
+ reason: status === 401 || status === 403 ? "auth" : "http",
+ status,
+ ...(retryAfter ? { retryAfter } : {}),
+ };
+ }
+ const bounded = await readBoundedDiscoveryJson(response, MODEL_DISCOVERY_MAX_RESPONSE_BYTES);
+ if (!bounded.ok) return { ok: false, reason: "invalid_response" };
+ const models = parseRawCatalog(bounded.value);
+ if (!models?.length) return { ok: false, reason: "invalid_response" };
+ const roster = await fetchRoster(providerName, provider, accessToken, signal);
+ return { ok: true, models: overlayRoster(models, roster), ...(roster ? { roster } : {}) };
+ } catch {
+ return { ok: false, reason: "transport" };
+ }
+}
+
+function normalizeResetAt(value: unknown): number | undefined {
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
+ const millis = value > 1e12 ? value : value * 1000;
+ return Number.isFinite(new Date(millis).getTime()) ? millis : undefined;
+ }
+ if (typeof value === "string" && value.trim()) {
+ const numeric = Number(value);
+ if (Number.isFinite(numeric) && numeric > 0) return normalizeResetAt(numeric);
+ const parsed = Date.parse(value);
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
+ }
+ return undefined;
+}
+
+function oneDecimalPercent(value: number): number {
+ const rounded = Math.round(value * 10) / 10;
+ return rounded >= 99 ? 100 : Math.max(0, Math.min(100, rounded));
+}
+
+export function parseMirasimLimits(value: unknown): ProviderQuota | null {
+ const envelope = plainRecord(value);
+ if (!envelope || !Array.isArray(envelope.windows)) return null;
+ const customWindows: ProviderQuotaWindow[] = [];
+ let fiveHourPercent: number | undefined;
+ let fiveHourResetAt: number | undefined;
+ let weeklyPercent: number | undefined;
+ let weeklyResetAt: number | undefined;
+
+ for (const item of envelope.windows) {
+ const row = plainRecord(item);
+ if (!row || typeof row.name !== "string" || !row.name.trim()) continue;
+ const budget = typeof row.budget === "number" && Number.isFinite(row.budget) && row.budget >= 0
+ ? row.budget
+ : undefined;
+ const used = typeof row.used === "number" && Number.isFinite(row.used)
+ ? row.used
+ : undefined;
+ if (budget === undefined || used === undefined) continue;
+ const percent = budget > 0 ? oneDecimalPercent((used / budget) * 100) : 0;
+ const resetAt = normalizeResetAt(row.reset_at);
+ const name = row.name.trim();
+ const modelScoped = row.model_scoped === true;
+ const normalized = name.toLowerCase().replace(/[\s_-]+/g, "");
+ if (!modelScoped && (normalized === "5h" || normalized === "5hour" || normalized === "5hours")) {
+ fiveHourPercent = percent;
+ fiveHourResetAt = resetAt;
+ continue;
+ }
+ if (!modelScoped && (normalized === "7d" || normalized === "7day" || normalized === "7days")) {
+ weeklyPercent = percent;
+ weeklyResetAt = resetAt;
+ continue;
+ }
+ customWindows.push({
+ label: modelScoped ? `Model · ${name}` : name,
+ percent,
+ ...(resetAt ? { resetAt } : {}),
+ });
+ }
+
+ if (customWindows.length === 0 && fiveHourPercent === undefined && weeklyPercent === undefined) return null;
+ return {
+ ...(fiveHourPercent !== undefined ? { fiveHourPercent } : {}),
+ ...(fiveHourResetAt !== undefined ? { fiveHourResetAt } : {}),
+ ...(weeklyPercent !== undefined ? { weeklyPercent } : {}),
+ ...(weeklyResetAt !== undefined ? { weeklyResetAt } : {}),
+ customWindows,
+ updatedAt: Date.now(),
+ };
+}
+
+export async function fetchMirasimQuota(
+ providerName: string,
+ provider: OcxProviderConfig,
+ accessToken: string,
+ signal?: AbortSignal,
+): Promise {
+ const response = await fetchMirasimControl(providerName, provider, accessToken, "/v1/limits", {
+ signal,
+ timeoutMs: CONTROL_TIMEOUT_MS,
+ providerHeaders: { "x-mirasim-probe": "usage" },
+ });
+ if (response.status === 404 || response.status === 405) {
+ try { await response.body?.cancel(); } catch { /* already closed */ }
+ return null;
+ }
+ if (!response.ok) {
+ try { await response.body?.cancel(); } catch { /* already closed */ }
+ return null;
+ }
+ const bounded = await readBoundedDiscoveryJson(response, MODEL_DISCOVERY_MAX_RESPONSE_BYTES);
+ return bounded.ok ? parseMirasimLimits(bounded.value) : null;
+}
+
+export function resetMirasimControlPlaneStateForTests(): void {
+ rosterCache.clear();
+}
+
+/** Tests only: seeds one credential-scoped signed-roster observation. */
+export function setCachedMirasimRosterForTests(
+ accessToken: string,
+ roster: MirasimRoster,
+): void {
+ rosterCache.set(credentialCacheKey(accessToken), {
+ roster,
+ nextCheckAt: Date.now() + ROSTER_SUCCESS_TTL_MS,
+ });
+}
diff --git a/src/adapters/mirasim/crypto.ts b/src/adapters/mirasim/crypto.ts
new file mode 100644
index 00000000000..92bb456e4fc
--- /dev/null
+++ b/src/adapters/mirasim/crypto.ts
@@ -0,0 +1,430 @@
+import {
+ createHash,
+ createPrivateKey,
+ createPublicKey,
+ diffieHellman,
+ generateKeyPairSync,
+ hkdfSync,
+ randomBytes,
+ sign,
+ type KeyObject,
+} from "node:crypto";
+
+export const MIRASIM_SIGNATURE_VERSION = "mrs-sig-v2";
+export const MIRASIM_SEAL_VERSION = "mrs-seal-v1";
+export const MIRASIM_DEFAULT_SEAL_PUBLIC_KEY_BASE64 = "HlyNMMeGXryasYLJuYQ/9ksCD4AYVVy1zXKAtJdpJn4=";
+
+export const MIRASIM_HEADERS = {
+ device: "x-mirasim-device",
+ timestamp: "x-mirasim-ts",
+ nonce: "x-mirasim-nonce",
+ signature: "x-mirasim-sig",
+ client: "x-mirasim-client",
+ encryptedMetadata: "x-mirasim-enc",
+ session: "x-mirasim-session",
+ agent: "x-mirasim-agent",
+ call: "x-mirasim-call",
+} as const;
+
+export interface MirasimDeviceIdentity {
+ privateKeyPem: string;
+ publicKeyBase64: string;
+ deviceId: string;
+}
+
+export interface MirasimSigningInput {
+ method: string;
+ path: string;
+ timestamp: string;
+ nonce: string;
+ deviceId: string;
+ clientVersion: string;
+ credential: string;
+ metadata?: Readonly>;
+ body: Uint8Array;
+}
+
+export interface MirasimSignedRequest {
+ canonicalPayload: string;
+ signature: string;
+ headers: Record;
+}
+
+function sha256Hex(value: Uint8Array | string): string {
+ return createHash("sha256").update(value).digest("hex");
+}
+
+function base64UrlNoPadding(value: Uint8Array): string {
+ return Buffer.from(value).toString("base64url");
+}
+
+function rejectNul(label: string, value: string): void {
+ if (value.includes("\0")) throw new Error(`Mirasim ${label} contains NUL`);
+}
+
+export function canonicalMirasimMetadata(metadata: Readonly> | undefined): string {
+ if (!metadata) return "";
+ const normalized = new Map();
+ for (const [rawKey, rawValue] of Object.entries(metadata)) {
+ const key = rawKey.toLowerCase();
+ if (!rawValue) continue;
+ rejectNul("metadata key", key);
+ rejectNul("metadata value", rawValue);
+ normalized.set(key, rawValue);
+ }
+ return [...normalized.entries()]
+ .sort(([left], [right]) => left.localeCompare(right))
+ .map(([key, value]) => `${key}:${value}`)
+ .join("\n");
+}
+
+export function canonicalMirasimSignaturePayload(input: MirasimSigningInput): string {
+ const method = input.method.trim().toUpperCase();
+ const fields = [
+ method,
+ input.path,
+ input.timestamp,
+ input.nonce,
+ input.deviceId,
+ input.clientVersion,
+ input.credential,
+ ];
+ for (const value of fields) rejectNul("signature field", value);
+
+ const metadataCanonical = canonicalMirasimMetadata(input.metadata);
+ const metadataDigest = metadataCanonical ? sha256Hex(metadataCanonical) : "";
+ return [
+ MIRASIM_SIGNATURE_VERSION,
+ method,
+ input.path,
+ input.timestamp,
+ input.nonce,
+ input.deviceId,
+ input.clientVersion,
+ sha256Hex(input.credential),
+ metadataDigest,
+ sha256Hex(input.body),
+ ].join("\n");
+}
+
+function loadEd25519PrivateKey(privateKeyPem: string): KeyObject {
+ const privateKey = createPrivateKey(privateKeyPem);
+ if (privateKey.asymmetricKeyType !== "ed25519") {
+ throw new Error("Mirasim device private key is not Ed25519");
+ }
+ return privateKey;
+}
+
+function publicKeyFromPrivateKey(privateKey: KeyObject): KeyObject {
+ const pkcs8Pem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
+ return createPublicKey(pkcs8Pem);
+}
+
+export function createMirasimDeviceIdentity(privateKeyPem?: string): MirasimDeviceIdentity {
+ let key: KeyObject;
+ if (privateKeyPem?.trim()) {
+ key = loadEd25519PrivateKey(privateKeyPem.trim());
+ } else {
+ key = generateKeyPairSync("ed25519").privateKey;
+ }
+ const pem = key.export({ type: "pkcs8", format: "pem" }).toString().trim();
+ const publicDer = publicKeyFromPrivateKey(key).export({ type: "spki", format: "der" }) as Buffer;
+ const publicKeyBase64 = publicDer.toString("base64");
+ const deviceId = createHash("sha256")
+ .update(publicKeyBase64, "utf8")
+ .digest("base64url")
+ .slice(0, 22);
+ return { privateKeyPem: pem, publicKeyBase64, deviceId };
+}
+
+export function signMirasimRequest(
+ input: Omit & {
+ privateKeyPem: string;
+ timestamp?: string;
+ nonce?: string;
+ },
+): MirasimSignedRequest {
+ const timestamp = input.timestamp ?? String(Date.now());
+ const nonce = input.nonce ?? base64UrlNoPadding(randomBytes(12));
+ const canonicalPayload = canonicalMirasimSignaturePayload({
+ ...input,
+ timestamp,
+ nonce,
+ });
+ const signature = sign(null, Buffer.from(canonicalPayload, "utf8"), loadEd25519PrivateKey(input.privateKeyPem)).toString("base64url");
+ const headers: Record = {
+ ...(input.metadata ?? {}),
+ [MIRASIM_HEADERS.device]: input.deviceId,
+ [MIRASIM_HEADERS.timestamp]: timestamp,
+ [MIRASIM_HEADERS.nonce]: nonce,
+ [MIRASIM_HEADERS.signature]: signature,
+ };
+ if (input.clientVersion) headers[MIRASIM_HEADERS.client] = input.clientVersion;
+ return { canonicalPayload, signature, headers };
+}
+
+const X25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b656e04220420", "hex");
+const X25519_SPKI_PREFIX = Buffer.from("302a300506032b656e032100", "hex");
+
+function x25519PrivateKeyFromRaw(raw: Uint8Array): KeyObject {
+ if (raw.byteLength !== 32) throw new Error(`Mirasim X25519 private key must be 32 bytes, got ${raw.byteLength}`);
+ return createPrivateKey({
+ key: Buffer.concat([X25519_PKCS8_PREFIX, Buffer.from(raw)]),
+ type: "pkcs8",
+ format: "der",
+ });
+}
+
+function x25519PublicKeyFromRaw(raw: Uint8Array): KeyObject {
+ if (raw.byteLength !== 32) throw new Error(`Mirasim X25519 public key must be 32 bytes, got ${raw.byteLength}`);
+ return createPublicKey({
+ key: Buffer.concat([X25519_SPKI_PREFIX, Buffer.from(raw)]),
+ type: "spki",
+ format: "der",
+ });
+}
+
+function rawX25519PublicKey(privateKey: KeyObject): Buffer {
+ const der = publicKeyFromPrivateKey(privateKey).export({ type: "spki", format: "der" }) as Buffer;
+ if (der.length < 32) throw new Error("Mirasim X25519 public key export is malformed");
+ return der.subarray(der.length - 32);
+}
+
+function decodeRelayPublicKey(encoded: string): Buffer {
+ const trimmed = encoded.trim();
+ if (!trimmed) throw new Error("Mirasim relay seal public key is empty");
+ let decoded: Buffer;
+ try {
+ decoded = Buffer.from(trimmed, "base64");
+ } catch {
+ throw new Error("Mirasim relay seal public key is not valid base64");
+ }
+ if (decoded.length !== 32) {
+ throw new Error(`Mirasim relay seal public key must decode to 32 bytes, got ${decoded.length}`);
+ }
+ return decoded;
+}
+
+function stableMetadataJson(metadata: Readonly>): string {
+ const ordered: Record = {};
+ for (const key of Object.keys(metadata).sort()) {
+ const value = metadata[key];
+ if (value !== undefined) ordered[key] = value;
+ }
+ return JSON.stringify(ordered);
+}
+
+function rotateLeft32(value: number, bits: number): number {
+ return ((value << bits) | (value >>> (32 - bits))) >>> 0;
+}
+
+function quarterRound(state: Uint32Array, a: number, b: number, c: number, d: number): void {
+ state[a] = (state[a]! + state[b]!) >>> 0;
+ state[d] = rotateLeft32(state[d]! ^ state[a]!, 16);
+ state[c] = (state[c]! + state[d]!) >>> 0;
+ state[b] = rotateLeft32(state[b]! ^ state[c]!, 12);
+ state[a] = (state[a]! + state[b]!) >>> 0;
+ state[d] = rotateLeft32(state[d]! ^ state[a]!, 8);
+ state[c] = (state[c]! + state[d]!) >>> 0;
+ state[b] = rotateLeft32(state[b]! ^ state[c]!, 7);
+}
+
+function readU32LE(bytes: Uint8Array, offset: number): number {
+ return (
+ bytes[offset]!
+ | (bytes[offset + 1]! << 8)
+ | (bytes[offset + 2]! << 16)
+ | (bytes[offset + 3]! << 24)
+ ) >>> 0;
+}
+
+function chacha20Block(key: Uint8Array, counter: number, nonce: Uint8Array): Buffer {
+ if (key.length !== 32) throw new Error("ChaCha20 key must be 32 bytes");
+ if (nonce.length !== 12) throw new Error("ChaCha20 nonce must be 12 bytes");
+ const initial = new Uint32Array(16);
+ initial[0] = 0x61707865;
+ initial[1] = 0x3320646e;
+ initial[2] = 0x79622d32;
+ initial[3] = 0x6b206574;
+ for (let index = 0; index < 8; index++) initial[4 + index] = readU32LE(key, index * 4);
+ initial[12] = counter >>> 0;
+ initial[13] = readU32LE(nonce, 0);
+ initial[14] = readU32LE(nonce, 4);
+ initial[15] = readU32LE(nonce, 8);
+
+ const state = new Uint32Array(initial);
+ for (let round = 0; round < 10; round++) {
+ quarterRound(state, 0, 4, 8, 12);
+ quarterRound(state, 1, 5, 9, 13);
+ quarterRound(state, 2, 6, 10, 14);
+ quarterRound(state, 3, 7, 11, 15);
+ quarterRound(state, 0, 5, 10, 15);
+ quarterRound(state, 1, 6, 11, 12);
+ quarterRound(state, 2, 7, 8, 13);
+ quarterRound(state, 3, 4, 9, 14);
+ }
+
+ const out = Buffer.allocUnsafe(64);
+ for (let index = 0; index < 16; index++) {
+ out.writeUInt32LE((state[index]! + initial[index]!) >>> 0, index * 4);
+ }
+ return out;
+}
+
+function chacha20Xor(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array): Buffer {
+ const out = Buffer.allocUnsafe(plaintext.length);
+ let counter = 1;
+ for (let offset = 0; offset < plaintext.length; offset += 64, counter++) {
+ const block = chacha20Block(key, counter, nonce);
+ const take = Math.min(64, plaintext.length - offset);
+ for (let index = 0; index < take; index++) {
+ out[offset + index] = plaintext[offset + index]! ^ block[index]!;
+ }
+ }
+ return out;
+}
+
+function littleEndianBigInt(bytes: Uint8Array): bigint {
+ let value = 0n;
+ for (let index = bytes.length - 1; index >= 0; index--) {
+ value = (value << 8n) | BigInt(bytes[index]!);
+ }
+ return value;
+}
+
+function bigIntLittleEndian(value: bigint, length: number): Buffer {
+ const out = Buffer.alloc(length);
+ let remaining = value;
+ for (let index = 0; index < length; index++) {
+ out[index] = Number(remaining & 0xffn);
+ remaining >>= 8n;
+ }
+ return out;
+}
+
+function poly1305(message: Uint8Array, oneTimeKey: Uint8Array): Buffer {
+ if (oneTimeKey.length !== 32) throw new Error("Poly1305 key must be 32 bytes");
+ const rBytes = Buffer.from(oneTimeKey.subarray(0, 16));
+ rBytes[3] &= 15;
+ rBytes[7] &= 15;
+ rBytes[11] &= 15;
+ rBytes[15] &= 15;
+ rBytes[4] &= 252;
+ rBytes[8] &= 252;
+ rBytes[12] &= 252;
+ const r = littleEndianBigInt(rBytes);
+ const s = littleEndianBigInt(oneTimeKey.subarray(16, 32));
+ const prime = (1n << 130n) - 5n;
+ let accumulator = 0n;
+ for (let offset = 0; offset < message.length; offset += 16) {
+ const block = message.subarray(offset, Math.min(offset + 16, message.length));
+ const n = littleEndianBigInt(block) + (1n << BigInt(block.length * 8));
+ accumulator = ((accumulator + n) * r) % prime;
+ }
+ return bigIntLittleEndian((accumulator + s) & ((1n << 128n) - 1n), 16);
+}
+
+function u64le(value: number): Buffer {
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error("AEAD length is out of range");
+ const out = Buffer.alloc(8);
+ out.writeBigUInt64LE(BigInt(value));
+ return out;
+}
+
+function pad16(length: number): Buffer {
+ const remainder = length % 16;
+ return remainder === 0 ? Buffer.alloc(0) : Buffer.alloc(16 - remainder);
+}
+
+/**
+ * RFC 8439 ChaCha20-Poly1305 implemented here rather than via node:crypto:
+ * Bun 1.3/1.4 does not expose the chacha20-poly1305 cipher through createCipheriv.
+ */
+function chacha20Poly1305Seal(
+ key: Uint8Array,
+ nonce: Uint8Array,
+ plaintext: Uint8Array,
+ aad: Uint8Array,
+): Buffer {
+ const oneTimeKey = chacha20Block(key, 0, nonce).subarray(0, 32);
+ const ciphertext = chacha20Xor(key, nonce, plaintext);
+ const macInput = Buffer.concat([
+ Buffer.from(aad),
+ pad16(aad.length),
+ ciphertext,
+ pad16(ciphertext.length),
+ u64le(aad.length),
+ u64le(ciphertext.length),
+ ]);
+ return Buffer.concat([ciphertext, poly1305(macInput, oneTimeKey)]);
+}
+
+export interface MirasimSealOptions {
+ recipientPublicKeyBase64?: string;
+ ephemeralSecret?: Uint8Array;
+ nonce?: Uint8Array;
+}
+
+export function sealMirasimRelayMetadata(
+ metadata: Readonly>,
+ method: string,
+ path: string,
+ options: MirasimSealOptions = {},
+): string {
+ const recipientRaw = decodeRelayPublicKey(
+ options.recipientPublicKeyBase64
+ ?? process.env.MIRASIM_SEAL_PUBKEY
+ ?? MIRASIM_DEFAULT_SEAL_PUBLIC_KEY_BASE64,
+ );
+ const ephemeralPrivate = x25519PrivateKeyFromRaw(options.ephemeralSecret ?? randomBytes(32));
+ const ephemeralPublic = rawX25519PublicKey(ephemeralPrivate);
+ const recipientPublic = x25519PublicKeyFromRaw(recipientRaw);
+ const sharedSecret = diffieHellman({ privateKey: ephemeralPrivate, publicKey: recipientPublic });
+ const key = Buffer.from(hkdfSync(
+ "sha256",
+ sharedSecret,
+ ephemeralPublic,
+ Buffer.from(MIRASIM_SEAL_VERSION, "utf8"),
+ 32,
+ ));
+ const nonce = Buffer.from(options.nonce ?? randomBytes(12));
+ if (nonce.length !== 12) throw new Error(`Mirasim seal nonce must be 12 bytes, got ${nonce.length}`);
+
+ const aad = Buffer.from(
+ [MIRASIM_SEAL_VERSION, method.trim().toUpperCase(), path].join("\n"),
+ "utf8",
+ );
+ const sealed = chacha20Poly1305Seal(
+ key,
+ nonce,
+ Buffer.from(stableMetadataJson(metadata), "utf8"),
+ aad,
+ );
+ return Buffer.concat([ephemeralPublic, nonce, sealed]).toString("base64url");
+}
+
+export function sealedMirasimHeaders(
+ headers: Readonly>,
+ method: string,
+ path: string,
+ options: MirasimSealOptions = {},
+): Record {
+ const out: Record = {};
+ const metadata: Record = {};
+ for (const [name, value] of Object.entries(headers)) {
+ const lower = name.toLowerCase();
+ if (
+ lower.startsWith("x-mirasim-")
+ && lower !== MIRASIM_HEADERS.client
+ && lower !== MIRASIM_HEADERS.encryptedMetadata
+ ) {
+ if (value) metadata[lower] = value;
+ continue;
+ }
+ out[lower] = value;
+ }
+ if (Object.keys(metadata).length > 0) {
+ out[MIRASIM_HEADERS.encryptedMetadata] = sealMirasimRelayMetadata(metadata, method, path, options);
+ }
+ return out;
+}
diff --git a/src/adapters/mirasim/transport.ts b/src/adapters/mirasim/transport.ts
new file mode 100644
index 00000000000..a422a0d33ec
--- /dev/null
+++ b/src/adapters/mirasim/transport.ts
@@ -0,0 +1,841 @@
+import { createHash, randomUUID } from "node:crypto";
+import type { AdapterFetchContext, AdapterRequest } from "../base";
+import { createAdapterPhysicalSend } from "../physical-send";
+import { credentialGeneration, getAccountSet } from "../../oauth/store";
+import { mirasimClientVersion } from "../../oauth/mirasim";
+import type { MirasimOAuthMetadata } from "../../oauth/types";
+import { readBoundedResponseBytes } from "../../lib/bounded-body";
+import {
+ providerOutboundGet,
+ providerOutboundPost,
+ type ProviderOutboundDependencies,
+} from "../../lib/provider-outbound";
+import type { OcxProviderConfig } from "../../types";
+import {
+ createMirasimDeviceIdentity,
+ sealedMirasimHeaders,
+ signMirasimRequest,
+} from "./crypto";
+
+const DEVICE_SESSION_PATH = "/v1/device/session";
+const TICKET_REFRESH_LEAD_MS = 2 * 60 * 1000;
+const TICKET_DEFAULT_TTL_MS = 10 * 60 * 1000;
+const TICKET_404_QUIET_MS = 60 * 1000;
+const TICKET_501_QUIET_MS = 15 * 60 * 1000;
+const MAX_CONTROL_BODY = 64 * 1024;
+const INTERNAL_AGENT_HEADER = "x-opencodex-mirasim-agent";
+const INTERNAL_THREAD_HEADER = "x-opencodex-mirasim-thread";
+const INTERNAL_WIRE_HEADER = "x-opencodex-mirasim-wire";
+const CONTROL_PROVIDER_HEADER_NAMES = new Set(["x-mirasim-probe"]);
+
+class MirasimRelayStatusError extends Error {
+ readonly status: number;
+ readonly retryAfter?: string;
+
+ constructor(status: number, retryAfter?: string | null) {
+ super(`Mirasim relay rejected device-session mint with HTTP ${status}`);
+ this.name = "MirasimRelayStatusError";
+ this.status = status;
+ this.retryAfter = retryAfter?.trim() || undefined;
+ }
+}
+
+function statusErrorResponse(error: MirasimRelayStatusError): Response {
+ const headers = new Headers({ "content-type": "application/json" });
+ if (error.retryAfter) headers.set("retry-after", error.retryAfter);
+ return new Response(JSON.stringify({ error: "Mirasim authentication failed" }), {
+ status: error.status,
+ headers,
+ });
+}
+
+interface StoredMirasimCredential {
+ accountSlotId: string;
+ accountIdentity: string;
+ generation: string;
+ accessToken: string;
+ metadata: MirasimOAuthMetadata;
+}
+
+interface TicketAuth {
+ credential: string;
+ usedTicket: boolean;
+}
+
+interface TicketState {
+ generation: string;
+ ticket?: string;
+ expiresAt?: number;
+ unmintableUntil?: number;
+ retryAt?: number;
+ failures: number;
+ lastStatus?: number;
+ refusedUntil?: number;
+ accessRefreshRequired?: boolean;
+ mintFlight?: Promise;
+ sessionId?: string;
+ lastUsedAt: number;
+}
+
+type PhysicalSend = ReturnType;
+
+const TICKET_BACKOFF_BASE_MS = 1_000;
+const TICKET_BACKOFF_MAX_MS = 30_000;
+const TICKET_RETRY_MAX_MS = 15 * 60 * 1000;
+const TICKET_REFUSAL_FLOOR_MS = 30_000;
+const TRANSPORT_STATE_IDLE_TTL_MS = 24 * 60 * 60 * 1000;
+const TRANSPORT_STATE_MAX_ENTRIES = 128;
+
+const transportStateCache = new Map();
+
+function cleanMetadataValue(value: string | undefined): string | undefined {
+ const trimmed = value?.trim();
+ if (!trimmed || trimmed.length > 512 || /[\0\r\n]/.test(trimmed)) return undefined;
+ return trimmed;
+}
+
+function decodeJwtClaims(token: string): Record | undefined {
+ const parts = token.split(".");
+ if (parts.length !== 3 || !parts[1]) return undefined;
+ try {
+ const parsed = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
+ ? parsed as Record
+ : undefined;
+ } catch {
+ return undefined;
+ }
+}
+
+function relaySubAccount(accessToken: string): string | undefined {
+ const claims = decodeJwtClaims(accessToken);
+ for (const name of ["account_id", "accountId"]) {
+ const value = claims?.[name];
+ if (typeof value === "string") {
+ const clean = cleanMetadataValue(value);
+ if (clean) return clean;
+ }
+ }
+ return undefined;
+}
+
+function matchingCredential(accessToken: string): StoredMirasimCredential {
+ const set = getAccountSet("mirasim");
+ const matches = set?.accounts.filter(account => account.credential.access === accessToken) ?? [];
+ if (matches.length !== 1) {
+ throw new Error(
+ matches.length === 0
+ ? "Mirasim OAuth credential changed before request dispatch; retry the request"
+ : "Mirasim OAuth credential is ambiguous across account slots",
+ );
+ }
+ const account = matches[0]!;
+ const credential = account.credential;
+ if (!credential.mirasim) throw new Error("Mirasim credential is missing device signing metadata");
+ return {
+ accountSlotId: account.id,
+ accountIdentity: credential.accountId ?? account.id,
+ generation: credentialGeneration(credential),
+ accessToken,
+ metadata: {
+ ...credential.mirasim,
+ // Protocol version belongs to this executable, not the persisted account snapshot.
+ // Upgrades must not keep sending an obsolete client version until the user signs in again.
+ clientVersion: mirasimClientVersion(),
+ },
+ };
+}
+
+/**
+ * Stable, non-secret cache scope for one Mirasim account/device pair. Access-token rotation must
+ * not invalidate roster/model capabilities, while a re-login into another account or device
+ * must never inherit them. Tests and pre-dispatch serializers may use synthetic tokens that are
+ * not in the store yet, so fall back to a token fingerprint only when no stored credential exists.
+ */
+export function mirasimCredentialCacheScope(accessToken: string): string {
+ const set = getAccountSet("mirasim");
+ const matches = set?.accounts.filter(account => account.credential.access === accessToken) ?? [];
+ if (matches.length === 0) {
+ return createHash("sha256").update(accessToken).digest("hex");
+ }
+ if (matches.length > 1) {
+ throw new Error("Mirasim OAuth credential is ambiguous across account slots");
+ }
+ const account = matches[0]!;
+ const metadata = account.credential.mirasim;
+ if (!metadata) throw new Error("Mirasim credential is missing device signing metadata");
+ const deviceId = createMirasimDeviceIdentity(metadata.devicePrivateKey).deviceId;
+ return createHash("sha256")
+ .update([account.id, account.credential.accountId ?? account.id, deviceId].join("\0"))
+ .digest("hex");
+}
+
+function stableTransportKey(credential: StoredMirasimCredential, deviceId: string): string {
+ return createHash("sha256")
+ .update([credential.accountSlotId, credential.accountIdentity, deviceId].join("\0"))
+ .digest("hex");
+}
+
+function transportBudgetTargetKey(credential: StoredMirasimCredential): string {
+ return `mirasim:${new URL(credential.metadata.relayUrl).origin}:${credential.accountSlotId}`;
+}
+
+function pruneTransportState(now: number, keepKey: string): void {
+ for (const [key, state] of transportStateCache) {
+ if (key === keepKey || state.mintFlight) continue;
+ if (now - state.lastUsedAt > TRANSPORT_STATE_IDLE_TTL_MS) transportStateCache.delete(key);
+ }
+ if (transportStateCache.size < TRANSPORT_STATE_MAX_ENTRIES) return;
+ const candidates = [...transportStateCache.entries()]
+ .filter(([key, state]) => key !== keepKey && !state.mintFlight)
+ .sort((left, right) => left[1].lastUsedAt - right[1].lastUsedAt);
+ while (transportStateCache.size >= TRANSPORT_STATE_MAX_ENTRIES && candidates.length > 0) {
+ const [key] = candidates.shift()!;
+ transportStateCache.delete(key);
+ }
+}
+
+function transportStateFor(
+ credential: StoredMirasimCredential,
+ deviceId: string,
+ now = Date.now(),
+): { key: string; state: TicketState } {
+ const key = stableTransportKey(credential, deviceId);
+ pruneTransportState(now, key);
+ let state = transportStateCache.get(key);
+ if (!state) {
+ state = {
+ generation: credential.generation,
+ failures: 0,
+ lastUsedAt: now,
+ };
+ transportStateCache.set(key, state);
+ } else {
+ state.lastUsedAt = now;
+ if (state.generation !== credential.generation) {
+ // Access/refresh tokens rotate; account/device identity and session identity do not.
+ // A ticket is credential-generation scoped, so drop only ticket/refusal state.
+ state.generation = credential.generation;
+ state.ticket = undefined;
+ state.expiresAt = undefined;
+ state.retryAt = undefined;
+ state.failures = 0;
+ state.lastStatus = undefined;
+ state.refusedUntil = undefined;
+ state.accessRefreshRequired = false;
+ state.mintFlight = undefined;
+ }
+ }
+ return { key, state };
+}
+
+function resolveTicketExpiry(now: number, payload: Record): number {
+ const expiresIn = payload.expiresIn ?? payload.expires_in;
+ if (typeof expiresIn === "number" && Number.isFinite(expiresIn) && expiresIn > 0) {
+ return now + Math.floor(expiresIn * 1000);
+ }
+ const expiresAt = payload.expiresAt ?? payload.expires_at;
+ if (typeof expiresAt === "number" && Number.isFinite(expiresAt) && expiresAt > 0) {
+ // Mirasim deployments may return absolute epoch time in either seconds or milliseconds.
+ // Match the limits parser so a millisecond timestamp is not accidentally extended 1000x.
+ const candidate = Math.floor(expiresAt > 1e12 ? expiresAt : expiresAt * 1000);
+ if (candidate > now) return candidate;
+ }
+ return now + TICKET_DEFAULT_TTL_MS;
+}
+
+async function boundedControlJson(
+ response: Response,
+ inactivityTimeoutMs?: number,
+): Promise> {
+ const declared = Number(response.headers.get("content-length"));
+ if (Number.isFinite(declared) && declared > MAX_CONTROL_BODY) {
+ try { await response.body?.cancel(); } catch { /* already closed */ }
+ throw new Error("Mirasim device-session response is too large");
+ }
+ const bounded = await readBoundedResponseBytes(response, {
+ maxBytes: MAX_CONTROL_BODY,
+ ...(inactivityTimeoutMs && inactivityTimeoutMs > 0 ? { inactivityTimeoutMs } : {}),
+ });
+ if (bounded.oversized) throw new Error("Mirasim device-session response is too large");
+ try {
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bounded.bytes);
+ const parsed = JSON.parse(text);
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error();
+ return parsed as Record;
+ } catch {
+ throw new Error("Mirasim device-session response is invalid");
+ }
+}
+
+function combineSignal(signal: AbortSignal | undefined, timeoutMs: number | undefined): AbortSignal | undefined {
+ if (!timeoutMs || timeoutMs <= 0) return signal;
+ const timeout = AbortSignal.timeout(timeoutMs);
+ return signal ? AbortSignal.any([signal, timeout]) : timeout;
+}
+
+function responseHeaderDeadline(
+ parent: AbortSignal | undefined,
+ timeoutMs: number | undefined,
+): { signal: AbortSignal | undefined; clear: () => void } {
+ if (!timeoutMs || timeoutMs <= 0) return { signal: parent, clear: () => {} };
+ const deadline = new AbortController();
+ const timer = setTimeout(
+ () => deadline.abort(new DOMException("Mirasim response header timeout", "TimeoutError")),
+ timeoutMs,
+ );
+ return {
+ signal: parent ? AbortSignal.any([parent, deadline.signal]) : deadline.signal,
+ clear: () => clearTimeout(timer),
+ };
+}
+
+function retryAfterMs(value: string | null, now: number): number | undefined {
+ if (!value) return undefined;
+ const seconds = Number(value);
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.floor(seconds * 1000);
+ const timestamp = Date.parse(value);
+ if (!Number.isFinite(timestamp)) return undefined;
+ return Math.max(0, timestamp - now);
+}
+
+function noteTicketFailure(
+ state: TicketState,
+ status: number,
+ retryAfter: string | null,
+ retryable: boolean,
+ now: number,
+): void {
+ let delay = TICKET_BACKOFF_MAX_MS;
+ if (retryable) {
+ delay = Math.min(TICKET_BACKOFF_BASE_MS * (2 ** state.failures), TICKET_BACKOFF_MAX_MS);
+ state.failures += 1;
+ }
+ delay = retryAfterMs(retryAfter, now) ?? delay;
+ delay = Math.max(TICKET_BACKOFF_BASE_MS, Math.min(delay, TICKET_RETRY_MAX_MS));
+ state.retryAt = now + delay;
+ state.lastStatus = status;
+}
+
+function lowercaseHeaders(headers: Readonly>): Record {
+ const out: Record = {};
+ for (const [name, value] of Object.entries(headers)) out[name.toLowerCase()] = value;
+ return out;
+}
+
+async function mintDeviceTicket(
+ credential: StoredMirasimCredential,
+ ctx: AdapterFetchContext,
+ send: PhysicalSend,
+): Promise {
+ const identity = createMirasimDeviceIdentity(credential.metadata.devicePrivateKey);
+ const { state } = transportStateFor(credential, identity.deviceId);
+ const now = Date.now();
+
+ if (state.accessRefreshRequired) {
+ if (state.refusedUntil && now < state.refusedUntil) {
+ throw new MirasimRelayStatusError(401);
+ }
+ // Give a transiently unavailable auth service a bounded escape hatch: after the refusal
+ // floor expires, allow the same access token to mint a fresh ticket on a later request.
+ state.accessRefreshRequired = false;
+ state.refusedUntil = undefined;
+ }
+ if (state.ticket && state.expiresAt && now < state.expiresAt - TICKET_REFRESH_LEAD_MS) {
+ return { credential: state.ticket, usedTicket: true };
+ }
+ if (state.unmintableUntil && now < state.unmintableUntil) {
+ return { credential: credential.accessToken, usedTicket: false };
+ }
+ if (state.retryAt && now < state.retryAt) {
+ if (state.ticket && state.expiresAt && now < state.expiresAt) {
+ return { credential: state.ticket, usedTicket: true };
+ }
+ const retrySeconds = Math.max(1, Math.ceil((state.retryAt - now) / 1_000)).toString();
+ throw new MirasimRelayStatusError(state.lastStatus ?? 503, retrySeconds);
+ }
+ if (state.mintFlight) return state.mintFlight;
+
+ const generation = credential.generation;
+ const flight = (async (): Promise => {
+ const body = JSON.stringify({ publicKey: identity.publicKeyBase64, deviceId: identity.deviceId });
+ const signed = signMirasimRequest({
+ method: "POST",
+ path: DEVICE_SESSION_PATH,
+ deviceId: identity.deviceId,
+ clientVersion: credential.metadata.clientVersion,
+ credential: credential.accessToken,
+ body: Buffer.from(body, "utf8"),
+ privateKeyPem: identity.privateKeyPem,
+ });
+ const url = `${credential.metadata.relayUrl.replace(/\/$/, "")}${DEVICE_SESSION_PATH}`;
+ const providerExecutor = ctx.executor as (typeof globalThis.fetch & {
+ unoverriddenFetch?: typeof globalThis.fetch;
+ }) | undefined;
+ const physicalFetch = providerExecutor?.unoverriddenFetch;
+ const response = await send({
+ url,
+ budgetTargetKey: transportBudgetTargetKey(credential),
+ physicalFetch,
+ dispatch: executor => executor(url, {
+ method: "POST",
+ redirect: "manual",
+ headers: {
+ "content-type": "application/json",
+ accept: "application/json",
+ authorization: `Bearer ${credential.accessToken}`,
+ ...lowercaseHeaders(signed.headers),
+ },
+ body,
+ signal: combineSignal(ctx.abortSignal, ctx.timeoutMs),
+ }),
+ });
+
+ const observedAt = Date.now();
+ if (response.status === 404 || response.status === 501) {
+ try { await response.body?.cancel(); } catch { /* already closed */ }
+ if (state.generation === generation) {
+ state.ticket = undefined;
+ state.expiresAt = undefined;
+ state.retryAt = undefined;
+ state.failures = 0;
+ state.lastStatus = undefined;
+ state.unmintableUntil = observedAt
+ + (response.status === 404 ? TICKET_404_QUIET_MS : TICKET_501_QUIET_MS);
+ }
+ return { credential: credential.accessToken, usedTicket: false };
+ }
+ if (!response.ok) {
+ const status = response.status;
+ const retryAfter = response.headers.get("retry-after");
+ try { await response.body?.cancel(); } catch { /* already closed */ }
+ if (state.generation === generation) {
+ noteTicketFailure(
+ state,
+ status,
+ retryAfter,
+ status === 429 || status >= 500,
+ observedAt,
+ );
+ }
+ if (state.ticket && state.expiresAt && observedAt < state.expiresAt) {
+ return { credential: state.ticket, usedTicket: true };
+ }
+ throw new MirasimRelayStatusError(status, retryAfter);
+ }
+
+ const payload = await boundedControlJson(response, ctx.timeoutMs);
+ const ticket = typeof payload.ticket === "string" ? payload.ticket.trim() : "";
+ if (!ticket || ticket.length > MAX_CONTROL_BODY || /[\r\n\0]/.test(ticket)) {
+ if (state.generation === generation) {
+ noteTicketFailure(state, 502, null, false, observedAt);
+ }
+ throw new Error("Mirasim device-session response contains an invalid ticket");
+ }
+ const expiresAt = resolveTicketExpiry(observedAt, payload);
+ if (state.generation === generation) {
+ state.ticket = ticket;
+ state.expiresAt = expiresAt;
+ state.unmintableUntil = undefined;
+ state.retryAt = undefined;
+ state.failures = 0;
+ state.lastStatus = undefined;
+ state.accessRefreshRequired = false;
+ }
+ return { credential: ticket, usedTicket: true };
+ })();
+
+ state.mintFlight = flight;
+ try {
+ return await flight;
+ } finally {
+ if (state.mintFlight === flight) state.mintFlight = undefined;
+ }
+}
+
+function cleanBaseHeaders(headers: Readonly>): {
+ headers: Record;
+ agent?: "claude" | "codex";
+ threadId?: string;
+} {
+ const out: Record = {};
+ let agent: "claude" | "codex" | undefined;
+ let threadId: string | undefined;
+ for (const [name, value] of Object.entries(headers)) {
+ const lower = name.toLowerCase();
+ if (lower === INTERNAL_AGENT_HEADER) {
+ if (value === "claude" || value === "codex") agent = value;
+ continue;
+ }
+ if (lower === INTERNAL_THREAD_HEADER) {
+ threadId = cleanMetadataValue(value);
+ continue;
+ }
+ if (lower === INTERNAL_WIRE_HEADER) continue;
+ if (
+ lower === "authorization"
+ || lower === "proxy-authorization"
+ || lower === "x-api-key"
+ || lower.startsWith("x-mirasim-")
+ ) continue;
+ out[lower] = value;
+ }
+ return { headers: out, agent, threadId };
+}
+
+function collectEnabled(): boolean {
+ const value = process.env.MIRASIM_COLLECT?.trim().toLowerCase();
+ return value !== "0" && value !== "false" && value !== "off" && value !== "no";
+}
+
+function sessionId(state: TicketState, accountIdentity: string, threadId: string | undefined): string {
+ if (threadId) {
+ return `mirasim_${createHash("sha256")
+ .update(`${accountIdentity}\0${threadId}`)
+ .digest("hex")
+ .slice(0, 32)}`;
+ }
+ if (state.sessionId) return state.sessionId;
+ state.sessionId = `mirasim_${randomUUID()}`;
+ return state.sessionId;
+}
+
+function inferenceMetadata(
+ credential: StoredMirasimCredential,
+ deviceId: string,
+ requestPath: string,
+ agent?: "claude" | "codex",
+ threadId?: string,
+): Record {
+ const { state } = transportStateFor(credential, deviceId);
+ const metadata: Record = {
+ "x-mirasim-session": sessionId(state, credential.accountIdentity, threadId),
+ "x-mirasim-agent": agent ?? (requestPath.startsWith("/v1/responses") || requestPath.startsWith("/v1/alpha/search")
+ ? "codex"
+ : "claude"),
+ "x-mirasim-call": randomUUID(),
+ };
+ const account = relaySubAccount(credential.accessToken);
+ if (account) metadata["x-mirasim-account"] = account;
+ const locale = cleanMetadataValue(process.env.MIRASIM_LOCALE);
+ if (locale) metadata["x-mirasim-locale"] = locale;
+ if (!collectEnabled()) metadata["x-mirasim-collect"] = "off";
+ return metadata;
+}
+
+function assertRelayTarget(url: URL, configuredRelayUrl: string): void {
+ const relay = new URL(configuredRelayUrl);
+ if (url.origin !== relay.origin) {
+ throw new Error("Mirasim request destination does not match the credential relay origin");
+ }
+}
+
+function invalidateTicket(
+ credential: StoredMirasimCredential,
+ deviceId: string,
+): TicketState {
+ const { state } = transportStateFor(credential, deviceId);
+ state.ticket = undefined;
+ state.expiresAt = undefined;
+ return state;
+}
+
+/**
+ * Mirrors the reference client's ticket-refusal floor. The first authenticated relay 401
+ * invalidates the device ticket. A second refusal within the floor is evidence that the
+ * account access credential, not only the ticket, must be refreshed by the outer OAuth owner.
+ */
+function noteTicketRefusal(
+ credential: StoredMirasimCredential,
+ deviceId: string,
+ now = Date.now(),
+): void {
+ const state = invalidateTicket(credential, deviceId);
+ state.accessRefreshRequired = true;
+ state.refusedUntil = now + TICKET_REFUSAL_FLOOR_MS;
+}
+
+
+async function buildPhysicalRequest(
+ request: AdapterRequest,
+ ctx: AdapterFetchContext,
+ credential: StoredMirasimCredential,
+ forceFreshTicket: boolean,
+ controlPlane = false,
+ controlCredentialMode: "device-ticket" | "access-token" = "device-ticket",
+ send: PhysicalSend = createAdapterPhysicalSend(ctx),
+): Promise<{ budgetTargetKey: string; init: RequestInit; usedTicket: boolean; url: string }> {
+ const target = new URL(request.url);
+ assertRelayTarget(target, credential.metadata.relayUrl);
+ const path = target.pathname;
+ const identity = createMirasimDeviceIdentity(credential.metadata.devicePrivateKey);
+ const clean = cleanBaseHeaders(request.headers);
+ if (forceFreshTicket && controlCredentialMode === "device-ticket") {
+ invalidateTicket(credential, identity.deviceId);
+ }
+ const auth = controlPlane && controlCredentialMode === "access-token"
+ ? { credential: credential.accessToken, usedTicket: false }
+ : await mintDeviceTicket(credential, ctx, send);
+ const metadata = controlPlane
+ ? undefined
+ : inferenceMetadata(credential, identity.deviceId, path, clean.agent, clean.threadId);
+ const signed = signMirasimRequest({
+ method: request.method,
+ path,
+ deviceId: identity.deviceId,
+ clientVersion: credential.metadata.clientVersion,
+ credential: auth.credential,
+ metadata,
+ body: Buffer.from(request.body, "utf8"),
+ privateKeyPem: identity.privateKeyPem,
+ });
+ const authenticatedHeaders = { ...clean.headers, ...signed.headers };
+ const signedAndSealed = controlPlane
+ ? lowercaseHeaders(authenticatedHeaders)
+ : sealedMirasimHeaders(authenticatedHeaders, request.method, path);
+ const method = request.method.toUpperCase();
+ return {
+ url: target.toString(),
+ budgetTargetKey: transportBudgetTargetKey(credential),
+ usedTicket: auth.usedTicket,
+ init: {
+ method: request.method,
+ redirect: "manual",
+ headers: { ...signedAndSealed, authorization: `Bearer ${auth.credential}` },
+ ...(method === "GET" || method === "HEAD" ? {} : { body: request.body }),
+ signal: controlPlane ? combineSignal(ctx.abortSignal, ctx.timeoutMs) : ctx.abortSignal,
+ },
+ };
+}
+
+function providerControlExecutor(
+ providerName: string,
+ provider: OcxProviderConfig,
+ dependencies: ProviderOutboundDependencies = {},
+): typeof globalThis.fetch {
+ return (async (input: string | URL | Request, init?: RequestInit): Promise => {
+ const url = input instanceof Request ? input.url : input.toString();
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
+ const headers = init?.headers ?? (input instanceof Request ? input.headers : undefined);
+ const signal = init?.signal ?? (input instanceof Request ? input.signal : undefined);
+ if (method === "GET") {
+ return providerOutboundGet(providerName, provider, url, { headers, signal }, dependencies);
+ }
+ if (method === "POST") {
+ const rawBody = init?.body;
+ if (rawBody != null && typeof rawBody !== "string") {
+ throw new Error("Mirasim control-plane POST body must be a UTF-8 string");
+ }
+ const body = rawBody ?? "";
+ return providerOutboundPost(providerName, provider, url, { headers, body, signal }, dependencies);
+ }
+ throw new Error(`Mirasim control-plane method ${method} is not supported`);
+ }) as typeof globalThis.fetch;
+}
+
+export interface MirasimControlRequestOptions {
+ method?: "GET" | "POST";
+ headers?: Readonly>;
+ /** Provider-owned headers appended after signing; caller-supplied x-mirasim-* remains blocked. */
+ providerHeaders?: Readonly>;
+ /**
+ * Most relay control endpoints accept the device-session bearer used by inference. A small
+ * account-scoped subset (currently /v1/model-roster) authenticates the login access token
+ * directly instead. Both modes still carry the device signature.
+ */
+ credentialMode?: "device-ticket" | "access-token";
+ body?: string;
+ signal?: AbortSignal;
+ timeoutMs?: number;
+ outboundDependencies?: ProviderOutboundDependencies;
+}
+
+function validatedControlProviderHeaders(
+ headers: Readonly> | undefined,
+): Record {
+ const out: Record = {};
+ if (!headers) return out;
+ for (const [rawName, rawValue] of Object.entries(headers)) {
+ const name = rawName.trim().toLowerCase();
+ const value = rawValue.trim();
+ if (!CONTROL_PROVIDER_HEADER_NAMES.has(name)) {
+ throw new Error(`Unsupported Mirasim provider-owned control header: ${rawName}`);
+ }
+ if (!value || value.length > 512 || /[\0\r\n]/.test(value)) {
+ throw new Error(`Invalid Mirasim provider-owned control header: ${rawName}`);
+ }
+ out[name] = value;
+ }
+ return out;
+}
+
+function appendControlProviderHeaders(
+ physical: { init: RequestInit; usedTicket: boolean; url: string },
+ providerHeaders: Readonly>,
+): void {
+ if (Object.keys(providerHeaders).length === 0) return;
+ const headers = new Headers(physical.init.headers);
+ for (const [name, value] of Object.entries(providerHeaders)) headers.set(name, value);
+ physical.init = { ...physical.init, headers };
+}
+
+async function fetchMirasimControlOnce(
+ providerName: string,
+ provider: OcxProviderConfig,
+ credential: StoredMirasimCredential,
+ path: string,
+ options: MirasimControlRequestOptions,
+): Promise {
+ const relayBase = credential.metadata.relayUrl.replace(/\/$/, "");
+ const normalizedPath = `/${path.trim().replace(/^\/+/, "")}`;
+ const executor = providerControlExecutor(providerName, provider, options.outboundDependencies);
+ const ctx: AdapterFetchContext = {
+ executor,
+ ...(options.signal ? { abortSignal: options.signal } : {}),
+ ...(options.timeoutMs ? { timeoutMs: options.timeoutMs } : {}),
+ };
+ const send = createAdapterPhysicalSend(ctx);
+ const request: AdapterRequest = {
+ url: `${relayBase}${normalizedPath}`,
+ method: options.method ?? "GET",
+ headers: { accept: "application/json", ...(options.headers ?? {}) },
+ body: options.body ?? "",
+ };
+ const providerHeaders = validatedControlProviderHeaders(options.providerHeaders);
+ const credentialMode = options.credentialMode ?? "device-ticket";
+ const identity = createMirasimDeviceIdentity(credential.metadata.devicePrivateKey);
+ let physical = await buildPhysicalRequest(
+ request,
+ ctx,
+ credential,
+ false,
+ true,
+ credentialMode,
+ send,
+ );
+ appendControlProviderHeaders(physical, providerHeaders);
+ const budgetTargetKey = transportBudgetTargetKey(credential);
+ const response = await send({
+ url: physical.url,
+ budgetTargetKey,
+ dispatch: physicalExecutor => physicalExecutor(physical.url, physical.init),
+ });
+ if (response.status === 401 && physical.usedTicket) {
+ noteTicketRefusal(credential, identity.deviceId);
+ }
+ return response;
+}
+
+/**
+ * Send a Mirasim control-plane request without inference metadata or the sealed
+ * x-mirasim-enc envelope. Device-ticket authentication remains the default; endpoints whose
+ * control-plane contract is tied to the login identity can opt into the access-token credential
+ * while retaining device signing. One authenticated 401 may force-refresh the same account
+ * generation and replay once, matching the main inference path.
+ */
+export async function fetchMirasimControl(
+ providerName: string,
+ provider: OcxProviderConfig,
+ accessToken: string,
+ path: string,
+ options: MirasimControlRequestOptions = {},
+): Promise {
+ const credential = matchingCredential(accessToken);
+ let response: Response;
+ try {
+ response = await fetchMirasimControlOnce(providerName, provider, credential, path, options);
+ } catch (error) {
+ if (!(error instanceof MirasimRelayStatusError) || error.status !== 401) throw error;
+ response = statusErrorResponse(error);
+ }
+ if (response.status !== 401) return response;
+
+ try {
+ const { forceRefreshOAuthAccessSnapshot } = await import("../../oauth");
+ const refreshed = await forceRefreshOAuthAccessSnapshot({
+ provider: "mirasim",
+ accountId: credential.accountSlotId,
+ generation: credential.generation,
+ accessToken: credential.accessToken,
+ });
+ const replacement = await fetchMirasimControlOnce(
+ providerName,
+ provider,
+ matchingCredential(refreshed.accessToken),
+ path,
+ options,
+ );
+ try { await response.body?.cancel(); } catch { /* already closed */ }
+ return replacement;
+ } catch {
+ return response;
+ }
+}
+
+async function sendInferencePhysical(
+ send: PhysicalSend,
+ physical: Awaited>,
+ ctx: AdapterFetchContext,
+ options: { sendClass?: AdapterFetchContext["sendClass"]; recovery?: AdapterFetchContext["recovery"] } = {},
+): Promise {
+ const deadline = responseHeaderDeadline(ctx.abortSignal, ctx.timeoutMs);
+ try {
+ return await send({
+ url: physical.url,
+ budgetTargetKey: physical.budgetTargetKey,
+ ...(options.sendClass ? { sendClass: options.sendClass } : {}),
+ ...(options.recovery ? { recovery: options.recovery } : {}),
+ dispatch: executor => executor(physical.url, { ...physical.init, signal: deadline.signal }),
+ });
+ } finally {
+ // Header deadline ends when fetch resolves. The returned body keeps only the caller's
+ // abort signal, so a long healthy SSE stream is not killed by the connect/header timeout.
+ deadline.clear();
+ }
+}
+
+export async function fetchMirasim(
+ request: AdapterRequest,
+ accessToken: string,
+ ctx: AdapterFetchContext = {},
+): Promise {
+ const credential = matchingCredential(accessToken);
+ const identity = createMirasimDeviceIdentity(credential.metadata.devicePrivateKey);
+ const send = createAdapterPhysicalSend(ctx);
+ let physical: Awaited>;
+ try {
+ physical = await buildPhysicalRequest(request, ctx, credential, false, false, "device-ticket", send);
+ } catch (error) {
+ if (error instanceof MirasimRelayStatusError && error.status === 401) {
+ return statusErrorResponse(error);
+ }
+ throw error;
+ }
+
+ const response = await sendInferencePhysical(send, physical, ctx);
+ if (response.status === 401 && physical.usedTicket) {
+ // One relay rejection is enough to hand control back to the OAuth owner. A same-token
+ // ticket re-mint plus an OAuth replay would require six physical sends; OpenCodex's shared
+ // request budget intentionally caps the whole logical turn at four. Refreshing access first
+ // remints the ticket as part of the bounded replay and covers both stale-ticket and stale-token
+ // cases in exactly four sends (ticket + inference, refreshed ticket + inference).
+ noteTicketRefusal(credential, identity.deviceId);
+ }
+ return response;
+
+}
+
+export const MIRASIM_INTERNAL_WIRE_HEADER = INTERNAL_WIRE_HEADER;
+export const MIRASIM_INTERNAL_THREAD_HEADER = INTERNAL_THREAD_HEADER;
+export const MIRASIM_INTERNAL_AGENT_HEADER = INTERNAL_AGENT_HEADER;
+
+/** Tests only: observe the stable no-thread session identity for a stored credential. */
+export function mirasimSessionIdForTests(accessToken: string): string | undefined {
+ const credential = matchingCredential(accessToken);
+ const identity = createMirasimDeviceIdentity(credential.metadata.devicePrivateKey);
+ return transportStateFor(credential, identity.deviceId).state.sessionId;
+}
+
+export function resetMirasimTransportStateForTests(): void {
+ transportStateCache.clear();
+}
diff --git a/src/adapters/openai-responses/passthrough.ts b/src/adapters/openai-responses/passthrough.ts
index 3b913b3caec..794eeff718e 100644
--- a/src/adapters/openai-responses/passthrough.ts
+++ b/src/adapters/openai-responses/passthrough.ts
@@ -218,7 +218,15 @@ function appendedUtf8Bytes(previousBytes: number, lastCodeUnit: number, fragment
return previousBytes + Buffer.byteLength(fragment, "utf8") - (joinsSurrogatePair ? 2 : 0);
}
-export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): ProviderAdapter & { passthrough: true } {
+export interface ResponsesPassthroughCompatibility {
+ /** Additional routed custom tool names proven native on this destination/request family. */
+ routedCustomToolPassthroughNames?: ReadonlySet;
+}
+
+export function createResponsesPassthroughAdapter(
+ provider: OcxProviderConfig,
+ compatibility: ResponsesPassthroughCompatibility = {},
+): ProviderAdapter & { passthrough: true } {
return {
name: "openai-responses",
passthrough: true as const,
@@ -422,6 +430,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
const rewritten = rewriteRoutedCustomToolsForUpstream(
outBody,
provider.supportsResponsesCustomTools,
+ compatibility.routedCustomToolPassthroughNames,
);
outBody = rewritten.body;
convertedRoutedCustomToolNames = rewritten.names;
diff --git a/src/adapters/physical-send.ts b/src/adapters/physical-send.ts
index b7cf47c574b..a6654037fd3 100644
--- a/src/adapters/physical-send.ts
+++ b/src/adapters/physical-send.ts
@@ -20,15 +20,25 @@ export function createAdapterPhysicalSend(ctx: AdapterFetchContext = {}, fallbac
let ordinal = 0;
return async (options: {
url: string;
+ /** Logical model-target identity for the shared request budget. Sidecars on the same relay
+ * should reuse the parent target rather than looking like an account/route transition. */
+ budgetTargetKey?: string;
sendClass?: SendClass;
recovery?: AttemptRecoveryKind;
+ /**
+ * Optional physical executor for an adapter-owned credential/control sidecar. Pacing and
+ * admission still come from the request executor above; only the final HTTP function changes.
+ */
+ physicalFetch?: typeof globalThis.fetch;
/** Runs only after admission, e.g. backoff and cancellation of a superseded response. */
beforeDispatch?: () => void | Promise;
dispatch: (executor: typeof globalThis.fetch) => Promise;
}): Promise => {
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
+ const sendClass = options.sendClass ?? ctx.sendClass ?? "transient";
+ const recovery = options.recovery ?? ctx.recovery;
const decision = ctx.sendBudget?.reserveDispatch({
- sendClass: options.sendClass ?? "transient", targetKey: options.url,
+ sendClass, targetKey: options.budgetTargetKey ?? options.url,
});
if (decision && !decision.allowed) throw new SendBudgetExhaustedError(options.url);
const permit = decision?.allowed ? decision.permit : undefined;
@@ -40,8 +50,8 @@ export function createAdapterPhysicalSend(ctx: AdapterFetchContext = {}, fallbac
if (dispatched || (permit && !permit.use())) throw new SendBudgetExhaustedError(options.url);
dispatched = true;
ordinal += 1;
- ctx.onPhysicalSend?.({ ordinal, ...(options.recovery ? { recovery: options.recovery } : {}) });
- return (executor.unpacedFetch ?? executor)(input, init);
+ ctx.onPhysicalSend?.({ ordinal, ...(recovery ? { recovery } : {}) });
+ return (options.physicalFetch ?? executor.unpacedFetch ?? executor)(input, init);
};
return permit?.execute ? permit.execute(run) : run();
}) as typeof globalThis.fetch;
diff --git a/src/adapters/registry.ts b/src/adapters/registry.ts
index a234c60aafb..97bd1245df5 100644
--- a/src/adapters/registry.ts
+++ b/src/adapters/registry.ts
@@ -12,6 +12,7 @@ import { createDevinAdapter } from "./devin";
import { createGoogleAdapter } from "./google";
import { createKiroAdapter } from "./kiro";
import { createMimoFreeAdapter } from "./mimo-free";
+import { createMirasimAdapter } from "./mirasim";
import { createOpenAIChatAdapter } from "./openai-chat";
import { createOllamaNativeAdapter } from "./ollama-native";
import { createResponsesPassthroughAdapter } from "./openai-responses";
@@ -143,6 +144,13 @@ export const ADAPTER_REGISTRY = {
contractParent: "openai-chat",
create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createMimoFreeAdapter(provider),
},
+ mirasim: {
+ // Mirasim can select Anthropic or Responses per model. The Responses contract is the least
+ // restrictive parent (media is not pre-rejected here); the wrapper delegates to the concrete
+ // wire adapter after routing the model.
+ contractParent: "openai-responses",
+ create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createMirasimAdapter(provider),
+ },
qoder: {
contractParent: "codebuddy",
create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createQoderAdapter(provider),
diff --git a/src/cli/dispatch.ts b/src/cli/dispatch.ts
index 6b0d75dfa24..567cc0c4a47 100644
--- a/src/cli/dispatch.ts
+++ b/src/cli/dispatch.ts
@@ -382,7 +382,7 @@ const commandRunners: Record = {
return code ?? 1;
}
const { handleLogin } = await import("../oauth/login-cli");
- await handleLogin(loginArgs[0]);
+ await handleLogin(loginArgs[0], {}, loginArgs.slice(1));
return 0;
},
logout: async deps => {
diff --git a/src/codex/catalog/provider-models.ts b/src/codex/catalog/provider-models.ts
index 7fe2c27c694..c08e8e3fff9 100644
--- a/src/codex/catalog/provider-models.ts
+++ b/src/codex/catalog/provider-models.ts
@@ -68,6 +68,8 @@ import { fetchCodeBuddyModels } from "../../adapters/codebuddy/live-models";
import { resolveProfileByBaseUrl } from "../../adapters/coding-agent/profile";
import { fetchDevinUsableModels } from "../../adapters/devin/live-models";
import { resolveDevinApiBaseUrl } from "../../oauth/devin/api-base";
+import { fetchMirasimLiveCatalog } from "../../adapters/mirasim/control-plane";
+import { mirasimCredentialCacheScope } from "../../adapters/mirasim/transport";
import { resolveZedModels } from "../../providers/zed";
import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
import {
@@ -75,6 +77,7 @@ import {
comboModelId,
getCombo,
listComboIds,
+ parseRetryAfterMs,
quotaInactiveReason,
targetKey,
} from "../../combos";
@@ -116,7 +119,7 @@ import type {
CatalogTrustedOpenAiApiPolicySnapshot,
} from "../convergence-types";
import type { CapturedProviderGather, CatalogGatherProviderAuthOutcome, CatalogGatherProviderModelOutcome, ModelsAuthResolution, ModelsAuthResolver } from "./gather-capture";
-import { QUIET_AUTHORITATIVE_CATALOG_PROVIDERS, applyConfigHintsToCachedModels, applyProviderConfigHints, boundedOwnedBy, catalogHintsFromModelsApiItem, catalogHintsFromProviderConfig } from "./model-hints";
+import { QUIET_AUTHORITATIVE_CATALOG_PROVIDERS, applyConfigHintsToCachedModels, applyProviderConfigHints, boundedOwnedBy, catalogHintsFromModelsApiItem, catalogHintsFromProviderConfig, configuredAutoCompactTokenLimit } from "./model-hints";
import { mergeConfiguredModelsIntoLiveCatalog, shouldExposeProviderModel, warnDroppedConfiguredIdsOnce } from "./model-visibility";
import { captureModelsRequest, captureProviderGather, materializeCapturedHeaders } from "./gather-capture";
@@ -437,6 +440,118 @@ export async function fetchProviderModelsWithAuth(
);
}
}
+ if (prov.adapter === "mirasim") {
+ if (!apiKey) return observed(configured, "degraded");
+ // The Mirasim catalog and signed roster are account/device-scoped. Keep cache authority
+ // stable across access-token rotation without allowing another account/device to inherit it.
+ // Ambiguous/crafted auth rows must degrade discovery, not fail the whole catalog gather.
+ let authorityIdentity: string;
+ try {
+ authorityIdentity = mirasimCredentialCacheScope(apiKey);
+ } catch {
+ return observed(configured, "degraded");
+ }
+ const fresh = getFreshCached(name, ttlMs, Date.now(), authorityIdentity);
+ if (fresh) {
+ return observed(withConfiguredRetention(fresh), "authoritative");
+ }
+ const scopedStale = getStaleCached(name, authorityIdentity);
+ if (isModelsFetchCoolingDown(name, undefined, undefined, authorityIdentity)) {
+ return observed(withConfiguredRetention(scopedStale ?? configured), "degraded");
+ }
+
+ const live = await fetchMirasimLiveCatalog(name, prov, apiKey);
+ if (live.ok) {
+ const discovered = live.models.map(model => {
+ const softCompact = model.contextWindow && model.autoCompactRatio
+ ? Math.floor(model.contextWindow * model.autoCompactRatio)
+ : undefined;
+ const hinted = applyProviderConfigHints(name, prov, {
+ id: model.id,
+ provider: name,
+ ...(model.displayName ? { displayName: model.displayName } : {}),
+ ...(model.ownedBy ? { owned_by: model.ownedBy } : {}),
+ ...(model.contextWindow ? {
+ contextWindow: model.contextWindow,
+ maxInputTokens: model.contextWindow,
+ } : {}),
+ ...(model.maxOutputTokens ? { maxOutputTokens: model.maxOutputTokens } : {}),
+ ...(model.reasoningEfforts?.length ? { reasoningEfforts: model.reasoningEfforts } : {}),
+ ...(softCompact ? { autoCompactTokenLimit: softCompact } : {}),
+ }, contextCap, metadataModelIdCaseFold, captured.effectiveAlias);
+ // The signed account roster is Mirasim's wire authority. Generic provider hints are
+ // still useful for local-only presentation/capability policy, but registry fallback
+ // tables must not overwrite a successfully observed account context/output/effort.
+ const liveWindow = model.contextWindow
+ ? applyProviderContextCap(model.contextWindow, contextCap)
+ : undefined;
+ const configuredSoftCompact = configuredAutoCompactTokenLimit(prov, model.id);
+ const effectiveSoftCompact = liveWindow
+ ? clampAutoCompactTokenLimit(
+ liveWindow,
+ liveWindow,
+ [softCompact, configuredSoftCompact]
+ .filter((value): value is number => typeof value === "number" && value > 0)
+ .sort((left, right) => left - right)[0],
+ )
+ : undefined;
+ return {
+ // `hinted` already carries the live roster label as its fallback; an operator's
+ // modelDisplayNames entry (what the dashboard shows and edits) must win over it.
+ ...hinted,
+ ...(liveWindow ? {
+ contextWindow: liveWindow,
+ maxInputTokens: liveWindow,
+ ...(contextCap !== undefined ? {
+ contextCap,
+ contextCapped: liveWindow < model.contextWindow!,
+ } : {}),
+ } : {}),
+ ...(model.maxOutputTokens ? { maxOutputTokens: model.maxOutputTokens } : {}),
+ ...(model.reasoningEfforts?.length
+ ? { reasoningEfforts: [...model.reasoningEfforts] }
+ : {}),
+ ...(effectiveSoftCompact ? { autoCompactTokenLimit: effectiveSoftCompact } : {}),
+ } as CatalogModel;
+ });
+ const forCache = withConfiguredRetention(discovered, { retainComboTargets: false });
+ if (!setCached(name, forCache, Date.now(), cacheGeneration, authorityIdentity)) {
+ return observed(withConfiguredRetention(configured), "degraded");
+ }
+ markProviderDiscoveryOk(name, live.models.length);
+ return observed(withConfiguredRetention(forCache, { warnDrops: true }), "authoritative");
+ }
+
+ if (isCurrentCacheGeneration()) {
+ const failedAt = Date.now();
+ const retryDelayMs = live.retryAfter
+ ? parseRetryAfterMs(live.retryAfter, failedAt, { preserveServerDelay: true })
+ : undefined;
+ markModelsFetchFailure(
+ name,
+ failedAt,
+ authorityIdentity,
+ retryDelayMs === undefined ? undefined : failedAt + retryDelayMs,
+ );
+ if (live.reason === "http" && live.status !== undefined) {
+ markProviderDiscoveryFailed(name, { reason: "http", httpStatus: live.status });
+ } else {
+ markProviderDiscoveryFailed(name, {
+ reason: live.reason === "auth"
+ ? "provider"
+ : live.reason === "invalid_response"
+ ? "invalid_response"
+ : "network",
+ });
+ }
+ }
+ return observed(
+ withConfiguredRetention(
+ scopedStale ?? configured,
+ ),
+ "degraded",
+ );
+ }
if (prov.adapter === "qoder") {
if (!apiKey) return observed(configured, "degraded");
const profile = resolveQoderProfile(prov.baseUrl);
diff --git a/src/codex/model-cache.ts b/src/codex/model-cache.ts
index abd9ab2f315..989ae26387c 100644
--- a/src/codex/model-cache.ts
+++ b/src/codex/model-cache.ts
@@ -83,6 +83,8 @@ export const MODELS_FETCH_FAILURE_COOLDOWN_MS = 30_000;
interface DiscoveryFailure {
at: number;
+ /** Upstream-advised absolute retry deadline (for example, Retry-After on HTTP 429). */
+ retryAt?: number;
/**
* Credential the failure was observed under, for entitlement-specific rosters. Absent means
* the failure is credential-agnostic (a plain `/models` endpoint) and suppresses every
@@ -106,8 +108,16 @@ export function markModelsFetchFailure(
provider: string,
now = Date.now(),
authorityIdentity?: string,
+ retryAt?: number,
): void {
- failureAt.set(provider, { at: now, ...(authorityIdentity ? { authorityIdentity } : {}) });
+ const advisedRetryAt = typeof retryAt === "number" && Number.isFinite(retryAt) && retryAt > now
+ ? retryAt
+ : undefined;
+ failureAt.set(provider, {
+ at: now,
+ ...(advisedRetryAt !== undefined ? { retryAt: advisedRetryAt } : {}),
+ ...(authorityIdentity ? { authorityIdentity } : {}),
+ });
}
/** `liveModelCount` is required so a caller that forgets to pass it fails typecheck instead of
@@ -178,7 +188,9 @@ export function isModelsFetchCoolingDown(
authorityIdentity?: string,
): boolean {
const failure = failureAt.get(provider);
- if (failure === undefined || now - failure.at >= cooldownMs) return false;
+ if (failure === undefined) return false;
+ const retryAt = Math.max(failure.at + cooldownMs, failure.retryAt ?? 0);
+ if (now >= retryAt) return false;
if (failure.authorityIdentity === undefined || authorityIdentity === undefined) return true;
return failure.authorityIdentity === authorityIdentity;
}
diff --git a/src/oauth/index.ts b/src/oauth/index.ts
index 29acaf210dd..fa756750ccb 100644
--- a/src/oauth/index.ts
+++ b/src/oauth/index.ts
@@ -48,6 +48,7 @@ import { validateDevinApiBaseUrl } from "./devin/api-base";
import { loginGithubCopilot, refreshGithubCopilotToken, validateCopilotApiBaseUrl } from "./github-copilot";
import { loginCommandCode, refreshCommandCodeToken } from "./command-code";
import { loginMetaMuse, refreshMetaMuseToken } from "./meta-muse";
+import { mirasimCredentialNeedsMigration, mirasimOAuthProviderDefinition, MirasimTokenRefreshError, type MirasimProviderLoginOptions } from "./mirasim";
import { loginOrcaRouter, orcaRouterInferenceBaseUrl, refreshOrcaRouterKey } from "./orcarouter";
import { ANTIGRAVITY_REQUEST_UA } from "../adapters/google-antigravity-wire";
import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive";
@@ -181,8 +182,7 @@ interface GenericRefreshDeps { intentLock?:ReturnTypenumber){const k=verdictKey(p,a,c),u=permanentRefreshFailures.get(k);if(u===undefined)return false;if(u<=now()){permanentRefreshFailures.delete(k);return false;}return true;}
export function sweepExpiredXaiPermanentFailureVerdicts(now=Date.now()):number{let removed=0;for(const[key,until]of permanentRefreshFailures){if(until>now)continue;permanentRefreshFailures.delete(key);removed+=1;}return removed;}
-
-export interface LoginOpts {
+export interface LoginOpts extends MirasimProviderLoginOptions {
forceLogin?: boolean;
/** When set, persist into this account slot and require matching identity. */
reauthAccountId?: string;
@@ -193,7 +193,6 @@ export interface LoginOpts {
*/
flow?: ChatGPTLoginFlow;
}
-
export interface LoginFlowLifecycle {
flowId?: string;
/** Runs after background credential/config persistence settles, before status becomes done. */
@@ -233,8 +232,8 @@ function oauthDefaultModel(id: string): string {
if (!model) throw new Error(`OAuth provider missing default model in registry: ${id}`);
return model;
}
-
export const OAUTH_PROVIDERS: Record = {
+ mirasim: mirasimOAuthProviderDefinition(() => oauthConfig("mirasim"), oauthDefaultModel("mirasim")),
"command-code": {
// Add-account/reauth must not reimport the current local CLI credential.
login: (ctrl, opts) => loginCommandCode(ctrl, { importLocal: opts?.forceLogin ? "off" : "fallback" }),
@@ -603,8 +602,9 @@ async function resolveAccessSnapshotForAccount(
const cred = row.credential;
const current = accessSnapshot(provider, accountId, cred, oauthProvider);
if (rejectedGeneration !== undefined && current.generation !== rejectedGeneration) return current;
- if (rejectedGeneration === undefined && cred.expires > Date.now() + REFRESH_SKEW_MS) return current;
-
+ // A fresh legacy Mirasim token is unusable until its device signing metadata is migrated.
+ const requiresCredentialMigration = oauthProvider === "mirasim" && mirasimCredentialNeedsMigration(cred);
+ if (rejectedGeneration === undefined && !requiresCredentialMigration && cred.expires > Date.now() + REFRESH_SKEW_MS) return current;
const key = `${provider}\u0000${accountId}`;
let existing = tokenRefreshes.get(key);
let replacedStaleFlight: OAuthRefreshFlightEvidence | undefined;
@@ -661,6 +661,7 @@ export async function getValidAccessTokenSnapshot(
/** Providers whose upstream-401 replay path may force a snapshot refresh. */
const FORCE_REFRESH_PROVIDERS = new Set([
"xai",
+ "mirasim",
"github-copilot",
"kiro",
"google-antigravity",
@@ -720,6 +721,7 @@ function terminal(error:unknown):boolean{
if (error instanceof ChatGptTokenRequestError) return false;
if(error instanceof XaiTokenRequestError)return ["invalid_grant","refresh_token_reused","revoked_token"].includes(error.oauthError??"");
if(error instanceof AnthropicTokenError)return (error.httpStatus===400||error.httpStatus===401)&&["invalid_grant","refresh_token_reused","revoked","revoked_token","refresh_token_revoked"].includes(error.oauthError??"");
+ if(error instanceof MirasimTokenRefreshError)return (error.httpStatus===400||error.httpStatus===401)&&["invalid_grant","refresh_token_reused","revoked","revoked_token","refresh_token_revoked","expired_token"].includes(error.oauthError??"");
if(error instanceof KiroTokenRefreshError)return (error.httpStatus===400||error.httpStatus===401)&&error.oauthError!==undefined;
if(error instanceof NousTokenError)return error.terminal===true||["invalid_grant","refresh_token_reused","revoked","revoked_token","expired_token"].includes(error.oauthError??"");
// ChatGPT carries the pool classifier's verdict on the error itself: revoked and
@@ -954,9 +956,10 @@ export async function refreshGenericAccountWithLock(
logOAuthEvent("OAuth refresh started", { provider, accountId });
const guard = await (deps.intentLock ?? createOAuthRefreshIntentLock(provider, accountId)).acquire();
try {
- // Re-read under the lock: a pause committed while this caller waited must stop the refresh.
+ // Re-read under the lock: a pause committed while this caller waited must stop the refresh,
+ // while a terminal reauth marker still requires a fresh login.
const row = getAccountCredentialWithStatus(provider, accountId);
- if (!row) throw new OAuthLoginRequiredError(provider);
+ if (!row || row.needsReauth) throw new OAuthLoginRequiredError(provider);
if (row.paused) throw new OAuthAccountPausedError();
const stored = row.credential;
if (
diff --git a/src/oauth/login-cli.ts b/src/oauth/login-cli.ts
index 6197047b996..67ab85c2cfb 100644
--- a/src/oauth/login-cli.ts
+++ b/src/oauth/login-cli.ts
@@ -9,7 +9,7 @@ import {
requestBoundLocalProviderReload,
type LocalProviderReloadResult,
} from "../server/local-provider-reload-client";
-import { DEPRECATED_OAUTH_PROVIDER_ALIASES, isPublicOAuthProvider, listOAuthProviders, runLogin } from "./index";
+import { DEPRECATED_OAUTH_PROVIDER_ALIASES, isPublicOAuthProvider, listOAuthProviders, runLogin, type LoginOpts } from "./index";
import { KEY_LOGIN_PROVIDERS, isKeyLoginProvider, validateApiKey, type KeyLoginProvider } from "./key-providers";
import type { OcxConfig, OcxProviderConfig } from "../types";
import { configuredAdminToken } from "../lib/admin-secrets";
@@ -116,11 +116,50 @@ export function loginUsageMessage(): string {
return `Usage: ocx login \n`
+ ` Codex / ChatGPT: ocx login codex (account pool, needs a running proxy; 'chatgpt' and\n`
+ ` 'openai' are the same route. An OpenAI platform key is 'openai-apikey'.)\n`
+ + ` Mirasim email: ocx login mirasim --email [--code -]\n`
+ ` OAuth login: ${listOAuthProviders().join(", ")}\n`
+ ` API-key login: ${Object.keys(KEY_LOGIN_PROVIDERS).join(", ")}`;
}
-export async function handleLogin(provider?: string, deps: LoginCliDeps = {}): Promise {
+const MIRASIM_LOGIN_USAGE = "Usage: ocx login mirasim --email [--code -]";
+
+export function parseMirasimLoginOpts(args: readonly string[]): LoginOpts | undefined {
+ if (args.length === 0) return undefined;
+ let email: string | undefined;
+ let code: string | undefined;
+ for (let index = 0; index < args.length; index += 1) {
+ const arg = args[index]!;
+ if (arg === "--email") {
+ const value = args[++index]?.trim();
+ if (!value) throw new Error(MIRASIM_LOGIN_USAGE);
+ email = value;
+ continue;
+ }
+ if (arg === "--code") {
+ const value = args[++index]?.trim();
+ if (!value) throw new Error(MIRASIM_LOGIN_USAGE);
+ code = value;
+ continue;
+ }
+ // A typo can be an email address or one-time code accidentally passed positionally.
+ // Never reflect the raw value into stderr/CI logs.
+ throw new Error(`Unknown Mirasim login option at position ${index + 1}. ${MIRASIM_LOGIN_USAGE}`);
+ }
+ if (!email) throw new Error("--code requires --email for Mirasim login");
+ // Verification codes are short-lived secrets. CLI argv is visible to shell history and
+ // process inspection, so non-interactive input must come through stdin via "--code -".
+ if (code && code !== "-") throw new Error(MIRASIM_LOGIN_USAGE);
+ return {
+ mirasimEmail: email,
+ ...(code ? { mirasimCode: code } : {}),
+ };
+}
+
+export async function handleLogin(
+ provider?: string,
+ deps: LoginCliDeps = {},
+ providerArgs: readonly string[] = [],
+): Promise {
const name = (provider ?? "").trim().toLowerCase();
// A removed provider id reached through its alias still logs in — the merged
// successor owns the flow. Warn rather than silently reroute so scripts and
@@ -130,18 +169,31 @@ export async function handleLogin(provider?: string, deps: LoginCliDeps = {}): P
console.error(`${name} is deprecated; logging in as ${alias}`);
return handleOAuthLogin(alias, deps);
}
- if (isPublicOAuthProvider(name)) return handleOAuthLogin(name, deps);
+ if (isPublicOAuthProvider(name)) {
+ const opts = name === "mirasim" ? parseMirasimLoginOpts(providerArgs) : undefined;
+ return handleOAuthLogin(name, deps, opts);
+ }
if (isKeyLoginProvider(name)) return handleKeyLogin(name, deps);
console.error(loginUsageMessage());
process.exit(1);
}
-export async function handleOAuthLogin(name: string, deps: LoginCliDeps = {}): Promise {
+export async function handleOAuthLogin(
+ name: string,
+ deps: LoginCliDeps = {},
+ opts?: LoginOpts,
+): Promise {
if (name === "anthropic2") assertAnthropicInstanceLoginConfig(loadConfig(), name);
const login = deps.runLogin ?? runLogin;
const launch = deps.openUrl ?? openUrl;
const browser = createBrowserLaunchReport(deps.warn);
await withPrompt(deps.ask, async (ask) => {
+ let effectiveOpts = opts;
+ if (name === "mirasim" && opts?.mirasimCode === "-") {
+ const code = (await ask("Enter the Mirasim sign-in code: ")).trim();
+ if (!code) throw new Error("Mirasim sign-in code cannot be empty");
+ effectiveOpts = { ...opts, mirasimCode: code };
+ }
await login(name, {
onAuth: ({ url, instructions }) => {
console.log(`\n🔐 Opening browser for ${name} login...\n${url}\n`);
@@ -152,13 +204,13 @@ export async function handleOAuthLogin(name: string, deps: LoginCliDeps = {}): P
browser.track(launch(url));
},
onProgress: (m) => console.log(` ${m}`),
- onManualCodeInput: async () => {
+ onManualCodeInput: async (_expectedState, prompt) => {
// "or wait for browser" is a lie if nothing opened, and a warning printed after readline
// has drawn the prompt lands on the line the user is typing on.
await browser.settled();
- return await ask("Paste redirect URL or code (or wait for browser): ");
+ return await ask(prompt ?? "Paste redirect URL or code (or wait for browser): ");
},
- });
+ }, effectiveOpts);
});
// A device or polling provider never prompts, so nothing above waited on the launcher. It is
// still owed an answer before this claims the login worked.
diff --git a/src/oauth/mirasim.ts b/src/oauth/mirasim.ts
new file mode 100644
index 00000000000..fdeaf3561c2
--- /dev/null
+++ b/src/oauth/mirasim.ts
@@ -0,0 +1,1001 @@
+import { randomBytes } from "node:crypto";
+import { createMirasimDeviceIdentity } from "../adapters/mirasim/crypto";
+import { readBoundedResponseBytes } from "../lib/bounded-body";
+import type { OcxProviderConfig } from "../types";
+import type { MirasimOAuthMetadata, OAuthController, OAuthCredentials } from "./types";
+
+export const MIRASIM_RELAY_URL = "https://relay.mirasim.ai";
+export const MIRASIM_ADMIN_URL = "https://auth.mirasim.ai";
+export const MIRASIM_CLIENT_VERSION = "0.0.336";
+
+const MAX_AUTH_BODY = 64 * 1024;
+const LOGIN_TIMEOUT_MS = 3 * 60 * 1000;
+const AUTH_REQUEST_TIMEOUT_MS = 20_000;
+const PROFILE_REQUEST_TIMEOUT_MS = 5_000;
+const EMAIL_ADDRESS = /^[^@\s]+@[^@\s.]+(?:\.[^@\s.]+)+$/;
+const OAUTH_ERROR_CODE = /^[a-z0-9][a-z0-9_.-]{0,63}$/i;
+
+export class MirasimTokenRefreshError extends Error {
+ readonly httpStatus: number;
+ readonly oauthError?: string;
+ readonly retryAfterMs?: number;
+ readonly retryable: boolean;
+
+ constructor(httpStatus: number, oauthError?: string, retryAfterMs?: number) {
+ super(`Mirasim token refresh failed with HTTP ${httpStatus}`);
+ this.name = "MirasimTokenRefreshError";
+ this.httpStatus = httpStatus;
+ this.oauthError = oauthError;
+ this.retryAfterMs = retryAfterMs;
+ this.retryable = httpStatus === 429 || httpStatus >= 500;
+ }
+}
+
+export interface MirasimLoginOptions {
+ /** Management-owned browser origin used by the dashboard OAuth flow. */
+ browserBaseUrl?: string;
+ /** Dashboard locale used by the management-owned OAuth pages. */
+ browserLocale?: string;
+ /** Optional account email. Browser and default CLI login both use Mirasim email verification. */
+ email?: string;
+ /** Optional code from a previous /auth/code request. */
+ code?: string;
+}
+
+type MirasimBrowserLocale = "en" | "zh-TW" | "zh-CN";
+
+type MirasimBrowserCopy = {
+ htmlLang: string;
+ signInTitle: string;
+ chooseProvider: string;
+ continueWith: (provider: string) => string;
+ expiredTitle: string;
+ expiredBody: string;
+ failedTitle: string;
+ mismatchBody: string;
+ failedBody: string;
+ unavailableTitle: string;
+ unavailableBody: string;
+ unsupportedTitle: string;
+ unsupportedBody: string;
+ completeTitle: string;
+ completeBody: string;
+ incompleteTitle: string;
+ incompleteBody: string;
+ methodNotAllowed: string;
+ callbackAlreadyUsed: string;
+ notFound: string;
+ chooseProviderInstruction: string;
+ waitingForBrowser: string;
+ continueInstruction: (provider: string) => string;
+};
+
+const MIRASIM_BROWSER_COPY: Record = {
+ en: {
+ htmlLang: "en",
+ signInTitle: "Sign in to Mirasim",
+ chooseProvider: "Enter the email address for your Mirasim account. A short-lived verification code will be sent to that address.",
+ continueWith: () => "Send verification code",
+ expiredTitle: "Mirasim sign-in expired",
+ expiredBody: "This sign-in link is invalid or has expired. Return to OpenCodex and start again.",
+ failedTitle: "Mirasim sign-in failed",
+ mismatchBody: "The sign-in response did not match this OpenCodex login attempt.",
+ failedBody: "Mirasim did not complete the sign-in. Return to OpenCodex and try again.",
+ unavailableTitle: "Mirasim sign-in unavailable",
+ unavailableBody: "OpenCodex could not load the currently enabled Mirasim sign-in providers.",
+ unsupportedTitle: "Unsupported Mirasim sign-in provider",
+ unsupportedBody: "Choose one of the providers offered by Mirasim.",
+ completeTitle: "Mirasim sign-in complete",
+ completeBody: "Return to OpenCodex. You may close this tab.",
+ incompleteTitle: "Mirasim sign-in incomplete",
+ incompleteBody: "No renewable credential was received.",
+ methodNotAllowed: "Method Not Allowed",
+ callbackAlreadyUsed: "Mirasim login callback already used.",
+ notFound: "Not Found",
+ chooseProviderInstruction: "Complete Mirasim sign-in in the browser using the emailed verification code.",
+ waitingForBrowser: "Waiting for Mirasim browser authentication...",
+ continueInstruction: provider => `Continue with ${provider}.`,
+ },
+ "zh-TW": {
+ htmlLang: "zh-TW",
+ signInTitle: "登入 Mirasim",
+ chooseProvider: "輸入 Mirasim 帳號的電子郵件地址。我們會寄送一組短效驗證碼到該地址。",
+ continueWith: () => "傳送驗證碼",
+ expiredTitle: "Mirasim 登入連結已過期",
+ expiredBody: "此登入連結無效或已過期。請返回 OpenCodex 重新開始。",
+ failedTitle: "Mirasim 登入失敗",
+ mismatchBody: "登入回應與這次 OpenCodex 登入要求不相符。",
+ failedBody: "Mirasim 未完成登入。請返回 OpenCodex 後重試。",
+ unavailableTitle: "Mirasim 登入暫時無法使用",
+ unavailableBody: "OpenCodex 無法載入 Mirasim 目前啟用的登入供應商。",
+ unsupportedTitle: "不支援的 Mirasim 登入供應商",
+ unsupportedBody: "請選擇 Mirasim 提供的登入方式。",
+ completeTitle: "Mirasim 登入完成",
+ completeBody: "請返回 OpenCodex。現在可以關閉此分頁。",
+ incompleteTitle: "Mirasim 登入未完成",
+ incompleteBody: "未收到可續期的憑證。",
+ methodNotAllowed: "不允許此方法",
+ callbackAlreadyUsed: "Mirasim 登入回呼已使用。",
+ notFound: "找不到頁面",
+ chooseProviderInstruction: "請在瀏覽器中使用電子郵件驗證碼完成 Mirasim 登入。",
+ waitingForBrowser: "正在等待 Mirasim 瀏覽器驗證…",
+ continueInstruction: provider => `使用 ${provider} 繼續。`,
+ },
+ "zh-CN": {
+ htmlLang: "zh-CN",
+ signInTitle: "登录 Mirasim",
+ chooseProvider: "输入 Mirasim 账户的电子邮件地址。我们会向该地址发送一组短效验证码。",
+ continueWith: () => "发送验证码",
+ expiredTitle: "Mirasim 登录链接已过期",
+ expiredBody: "此登录链接无效或已过期。请返回 OpenCodex 重新开始。",
+ failedTitle: "Mirasim 登录失败",
+ mismatchBody: "登录响应与本次 OpenCodex 登录请求不匹配。",
+ failedBody: "Mirasim 未完成登录。请返回 OpenCodex 后重试。",
+ unavailableTitle: "Mirasim 登录暂不可用",
+ unavailableBody: "OpenCodex 无法加载 Mirasim 当前启用的登录提供商。",
+ unsupportedTitle: "不支持的 Mirasim 登录提供商",
+ unsupportedBody: "请选择 Mirasim 提供的登录方式。",
+ completeTitle: "Mirasim 登录完成",
+ completeBody: "请返回 OpenCodex。现在可以关闭此标签页。",
+ incompleteTitle: "Mirasim 登录未完成",
+ incompleteBody: "未收到可续期凭证。",
+ methodNotAllowed: "不允许此方法",
+ callbackAlreadyUsed: "Mirasim 登录回调已使用。",
+ notFound: "找不到页面",
+ chooseProviderInstruction: "请在浏览器中使用电子邮件验证码完成 Mirasim 登录。",
+ waitingForBrowser: "正在等待 Mirasim 浏览器验证…",
+ continueInstruction: provider => `使用 ${provider} 继续。`,
+ },
+};
+
+function normalizeMirasimBrowserLocale(value: string | null | undefined): MirasimBrowserLocale {
+ const normalized = (value ?? "").trim().toLowerCase();
+ if (!normalized) return "en";
+ if (
+ normalized.startsWith("zh-tw")
+ || normalized.startsWith("zh-hk")
+ || normalized.startsWith("zh-mo")
+ || normalized.startsWith("zh-hant")
+ ) {
+ return "zh-TW";
+ }
+ if (normalized === "zh" || normalized.startsWith("zh-")) return "zh-CN";
+ return "en";
+}
+
+interface MirasimJwtClaims {
+ exp?: unknown;
+ email?: unknown;
+ account_id?: unknown;
+ accountId?: unknown;
+ user_id?: unknown;
+ userId?: unknown;
+ sub?: unknown;
+}
+
+function isLoopbackHost(hostname: string): boolean {
+ const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
+ return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1";
+}
+
+function validatedServiceUrl(raw: string, label: string): string {
+ let parsed: URL;
+ try {
+ parsed = new URL(raw.trim());
+ } catch {
+ throw new Error(`Invalid Mirasim ${label} URL`);
+ }
+ if (parsed.username || parsed.password || parsed.hash) {
+ throw new Error(`Invalid Mirasim ${label} URL`);
+ }
+ if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && isLoopbackHost(parsed.hostname))) {
+ throw new Error(`Mirasim ${label} URL must use HTTPS unless it is loopback`);
+ }
+ return parsed.toString().replace(/\/$/, "");
+}
+
+export function mirasimRelayUrl(): string {
+ return validatedServiceUrl(process.env.MIRASIM_RELAY_URL ?? MIRASIM_RELAY_URL, "relay");
+}
+
+export function mirasimAdminUrl(): string {
+ return validatedServiceUrl(
+ process.env.MIRASIM_ADMIN_URL ?? MIRASIM_ADMIN_URL,
+ "authentication service",
+ );
+}
+
+export function mirasimCredentialNeedsMigration(credential: OAuthCredentials): boolean {
+ return credential.mirasim === undefined;
+}
+
+export function mirasimClientVersion(): string {
+ const value = (process.env.MIRASIM_CLIENT_VERSION ?? MIRASIM_CLIENT_VERSION).trim();
+ if (!value || value.length > 128 || /[\x00-\x1f\x7f]/.test(value)) {
+ throw new Error("Invalid Mirasim client version");
+ }
+ return value;
+}
+
+function defaultMirasimMetadata(existingPrivateKey?: string): MirasimOAuthMetadata {
+ const identity = createMirasimDeviceIdentity(existingPrivateKey);
+ return {
+ devicePrivateKey: identity.privateKeyPem,
+ relayUrl: mirasimRelayUrl(),
+ adminUrl: mirasimAdminUrl(),
+ clientVersion: mirasimClientVersion(),
+ };
+}
+
+function decodeJwtClaims(token: string): MirasimJwtClaims | undefined {
+ const parts = token.split(".");
+ if (parts.length !== 3 || !parts[1]) return undefined;
+ try {
+ const decoded = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
+ return decoded && typeof decoded === "object" && !Array.isArray(decoded)
+ ? decoded as MirasimJwtClaims
+ : undefined;
+ } catch {
+ return undefined;
+ }
+}
+
+function claimString(claims: MirasimJwtClaims | undefined, ...names: Array): string | undefined {
+ for (const name of names) {
+ const value = claims?.[name];
+ if (typeof value === "string" && value.trim()) return value.trim();
+ }
+ return undefined;
+}
+
+function accessTokenExpiry(accessToken: string, expiresInSeconds?: number): number {
+ const now = Date.now();
+ if (typeof expiresInSeconds === "number" && Number.isFinite(expiresInSeconds) && expiresInSeconds > 0) {
+ return now + Math.floor(expiresInSeconds * 1000);
+ }
+ const exp = decodeJwtClaims(accessToken)?.exp;
+ if (typeof exp === "number" && Number.isFinite(exp) && exp > 0) return Math.floor(exp * 1000);
+ return now + 30 * 60 * 1000;
+}
+
+function normalizeCredentialSecret(label: string, value: string): string {
+ const normalized = value.trim();
+ if (!normalized || normalized.length > MAX_AUTH_BODY || /[\r\n\0]/.test(normalized)) {
+ throw new Error(`Mirasim ${label} is invalid`);
+ }
+ return normalized;
+}
+
+function normalizeLoginEmail(value: string): string {
+ const normalized = value.trim();
+ if (!normalized || normalized.length > 254 || !EMAIL_ADDRESS.test(normalized)) {
+ throw new Error("Invalid Mirasim account email address");
+ }
+ return normalized;
+}
+
+function normalizeLoginCode(value: string): string {
+ const normalized = value.trim();
+ if (!normalized || normalized.length > 64 || /[\r\n\0]/.test(normalized)) {
+ throw new Error("Invalid Mirasim sign-in code");
+ }
+ return normalized;
+}
+
+function credentialsFromTokens(
+ accessToken: string,
+ refreshToken: string,
+ mirasim: MirasimOAuthMetadata,
+ expiresInSeconds?: number,
+): OAuthCredentials {
+ const access = normalizeCredentialSecret("access token", accessToken);
+ const refresh = normalizeCredentialSecret("refresh token", refreshToken);
+ const claims = decodeJwtClaims(access);
+ const accountId = claimString(claims, "account_id", "accountId", "user_id", "userId", "sub");
+ const email = claimString(claims, "email");
+ return {
+ access,
+ refresh,
+ expires: accessTokenExpiry(access, expiresInSeconds),
+ source: "oauth",
+ ...(accountId ? { accountId } : {}),
+ ...(email ? { email } : {}),
+ mirasim,
+ };
+}
+
+function requestSignal(parent?: AbortSignal): AbortSignal {
+ const timeout = AbortSignal.timeout(AUTH_REQUEST_TIMEOUT_MS);
+ return parent ? AbortSignal.any([parent, timeout]) : timeout;
+}
+
+function profileSignal(parent?: AbortSignal): AbortSignal {
+ const timeout = AbortSignal.timeout(PROFILE_REQUEST_TIMEOUT_MS);
+ return parent ? AbortSignal.any([parent, timeout]) : timeout;
+}
+
+async function boundedJson(response: Response): Promise> {
+ const declared = Number(response.headers.get("content-length"));
+ if (Number.isFinite(declared) && declared > MAX_AUTH_BODY) {
+ try { await response.body?.cancel(); } catch { /* already closed */ }
+ throw new Error("Mirasim authentication response is too large");
+ }
+ const bounded = await readBoundedResponseBytes(response, { maxBytes: MAX_AUTH_BODY });
+ if (bounded.oversized) throw new Error("Mirasim authentication response is too large");
+ let parsed: unknown;
+ try {
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bounded.bytes);
+ parsed = JSON.parse(text);
+ } catch {
+ throw new Error("Mirasim authentication response is invalid JSON");
+ }
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
+ throw new Error("Mirasim authentication response is invalid");
+ }
+ return parsed as Record;
+}
+
+function safeOAuthCode(value: unknown): string | undefined {
+ if (typeof value !== "string") return undefined;
+ const normalized = value.trim().toLowerCase();
+ return OAUTH_ERROR_CODE.test(normalized) ? normalized : undefined;
+}
+
+function safeOAuthErrorCode(payload: Record | undefined): string | undefined {
+ if (!payload) return undefined;
+ for (const field of ["code", "type"]) {
+ const code = safeOAuthCode(payload[field]);
+ if (code) return code;
+ }
+ const nested = payload.error;
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
+ const record = nested as Record;
+ for (const field of ["code", "type"]) {
+ const code = safeOAuthCode(record[field]);
+ if (code) return code;
+ }
+ }
+ return safeOAuthCode(payload.error);
+}
+
+function retryAfterMs(value: string | null, now = Date.now()): number | undefined {
+ if (!value) return undefined;
+ const seconds = Number(value);
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.floor(seconds * 1_000);
+ const timestamp = Date.parse(value);
+ if (!Number.isFinite(timestamp)) return undefined;
+ return Math.max(0, timestamp - now);
+}
+
+function safeProfileEmail(value: unknown): string | undefined {
+ if (typeof value !== "string") return undefined;
+ const email = value.trim();
+ if (!email || email.length > 320 || /[\r\n\0]/.test(email)) return undefined;
+ return email;
+}
+
+async function enrichMirasimCredentialFromProfile(
+ credential: OAuthCredentials,
+ signal?: AbortSignal,
+): Promise {
+ const metadata = credential.mirasim;
+ if (!metadata) return credential;
+ try {
+ const response = await fetch(`${metadata.adminUrl}/auth/me`, {
+ method: "GET",
+ headers: {
+ Accept: "application/json",
+ Authorization: `Bearer ${credential.access}`,
+ },
+ redirect: "error",
+ signal: profileSignal(signal),
+ });
+ if (!response.ok) {
+ try { await response.body?.cancel(); } catch { /* already closed */ }
+ return credential;
+ }
+ const profile = await boundedJson(response);
+ const email = safeProfileEmail(profile.email);
+ return email ? { ...credential, email } : credential;
+ } catch {
+ // The official client treats /auth/me as best-effort during login. The renewable credential
+ // remains usable even when the profile service is temporarily unavailable.
+ return credential;
+ }
+}
+
+async function postMirasimAuthJson(
+ adminUrl: string,
+ path: "/auth/code" | "/auth/verify",
+ body: Record,
+ signal?: AbortSignal,
+): Promise {
+ const response = await fetch(`${adminUrl}${path}`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
+ body: JSON.stringify(body),
+ redirect: "error",
+ signal: requestSignal(signal),
+ });
+ if (!response.ok) {
+ // These requests carry account/login secrets. Do not reflect an upstream body into
+ // CLI/dashboard logs; the status is enough to diagnose the public failure.
+ try { await response.body?.cancel(); } catch { /* already closed */ }
+ throw new Error(`Mirasim email sign-in failed with HTTP ${response.status}`);
+ }
+ return response;
+}
+
+async function loginMirasimWithEmail(
+ ctrl: OAuthController,
+ mirasim: MirasimOAuthMetadata,
+ options: Required> & Pick,
+): Promise {
+ const email = normalizeLoginEmail(options.email);
+ let code = options.code?.trim();
+ if (!code) {
+ const request = await postMirasimAuthJson(
+ mirasim.adminUrl,
+ "/auth/code",
+ { email },
+ ctrl.signal,
+ );
+ // A development auth service may echo the code. Deliberately discard every success body.
+ try { await request.body?.cancel(); } catch { /* already closed */ }
+ ctrl.onProgress?.(`Mirasim sent a sign-in code to ${email}.`);
+ if (!ctrl.onManualCodeInput) {
+ throw new Error("Mirasim email sign-in requires an interactive verification code");
+ }
+ code = await ctrl.onManualCodeInput(undefined, "Enter the Mirasim sign-in code: ");
+ }
+ code = normalizeLoginCode(code);
+
+ const response = await postMirasimAuthJson(
+ mirasim.adminUrl,
+ "/auth/verify",
+ { email, code },
+ ctrl.signal,
+ );
+ const payload = await boundedJson(response);
+ const accessToken = typeof payload.access_token === "string" ? payload.access_token : "";
+ const refreshToken = typeof payload.refresh_token === "string" ? payload.refresh_token : "";
+ if (!accessToken.trim()) throw new Error("Mirasim email sign-in returned no access token");
+ if (!refreshToken.trim()) throw new Error("Mirasim email sign-in returned no renewable credential");
+ const expiresIn = typeof payload.expires_in === "number" ? payload.expires_in : undefined;
+ const credential = credentialsFromTokens(accessToken, refreshToken, mirasim, expiresIn);
+ return enrichMirasimCredentialFromProfile(
+ credential.email ? credential : { ...credential, email },
+ ctrl.signal,
+ );
+}
+
+interface CallbackTokens {
+ accessToken: string;
+ refreshToken: string;
+}
+
+const MIRASIM_BROWSER_START_PATH = "/oauth/mirasim/start";
+const MIRASIM_BROWSER_FORM_MAX_BYTES = 4 * 1024;
+const MIRASIM_BROWSER_MAX_VERIFY_ATTEMPTS = 5;
+
+interface MirasimBrowserSession {
+ state: string;
+ locale: MirasimBrowserLocale;
+ adminUrl: string;
+ baseUrl: string;
+ expiresAt: number;
+ email?: string;
+ codeSent: boolean;
+ verifyAttempts: number;
+ verifyInFlight: number;
+ completed: boolean;
+ promise: Promise;
+ resolve: (tokens: CallbackTokens) => void;
+ reject: (error: Error) => void;
+ timeout: ReturnType;
+ abort?: () => void;
+}
+
+const mirasimBrowserSessions = new Map();
+
+function validatedBrowserBaseUrl(raw: string): string {
+ let parsed: URL;
+ try {
+ parsed = new URL(raw.trim());
+ } catch {
+ throw new Error("Invalid Mirasim browser callback origin");
+ }
+ if (
+ parsed.username
+ || parsed.password
+ || parsed.search
+ || parsed.hash
+ || (parsed.pathname && parsed.pathname !== "/")
+ ) {
+ throw new Error("Invalid Mirasim browser callback origin");
+ }
+ if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && isLoopbackHost(parsed.hostname))) {
+ throw new Error("Mirasim browser callback origin must use HTTPS unless it is loopback");
+ }
+ return parsed.origin;
+}
+
+function deleteMirasimBrowserSession(state: string): void {
+ const session = mirasimBrowserSessions.get(state);
+ if (!session) return;
+ clearTimeout(session.timeout);
+ mirasimBrowserSessions.delete(state);
+}
+
+function createMirasimBrowserSession(
+ ctrl: OAuthController,
+ state: string,
+ adminUrl: string,
+ rawBaseUrl: string,
+ rawLocale?: string,
+): { startUrl: string; tokens: Promise; discard: () => void } {
+ const baseUrl = validatedBrowserBaseUrl(rawBaseUrl);
+ const locale = normalizeMirasimBrowserLocale(rawLocale);
+ let resolveTokens!: (tokens: CallbackTokens) => void;
+ let rejectTokens!: (error: Error) => void;
+ const promise = new Promise((resolve, reject) => {
+ resolveTokens = resolve;
+ rejectTokens = reject;
+ });
+ const timeout = setTimeout(() => {
+ const current = mirasimBrowserSessions.get(state);
+ if (!current) return;
+ deleteMirasimBrowserSession(state);
+ current.reject(new Error("Mirasim OAuth login timed out"));
+ }, LOGIN_TIMEOUT_MS);
+ timeout.unref?.();
+ const session: MirasimBrowserSession = {
+ state,
+ locale,
+ adminUrl,
+ baseUrl,
+ expiresAt: Date.now() + LOGIN_TIMEOUT_MS,
+ codeSent: false,
+ verifyAttempts: 0,
+ verifyInFlight: 0,
+ completed: false,
+ promise,
+ resolve: resolveTokens,
+ reject: rejectTokens,
+ timeout,
+ };
+ const onAbort = () => {
+ const current = mirasimBrowserSessions.get(state);
+ if (!current) return;
+ deleteMirasimBrowserSession(state);
+ current.reject(new Error("Mirasim OAuth login cancelled"));
+ };
+ ctrl.signal?.addEventListener("abort", onAbort, { once: true });
+ session.abort = () => ctrl.signal?.removeEventListener("abort", onAbort);
+ mirasimBrowserSessions.set(state, session);
+
+ const startUrl = new URL(MIRASIM_BROWSER_START_PATH, `${baseUrl}/`);
+ startUrl.searchParams.set("state", state);
+ startUrl.searchParams.set("lang", locale);
+ return {
+ startUrl: startUrl.toString(),
+ tokens: promise,
+ discard: () => {
+ session.abort?.();
+ session.abort = undefined;
+ deleteMirasimBrowserSession(state);
+ },
+ };
+}
+
+function currentMirasimBrowserSession(state: string): MirasimBrowserSession | undefined {
+ const session = mirasimBrowserSessions.get(state);
+ if (!session) return undefined;
+ if (Date.now() < session.expiresAt) return session;
+ deleteMirasimBrowserSession(state);
+ session.reject(new Error("Mirasim OAuth login timed out"));
+ return undefined;
+}
+
+function reserveMirasimBrowserVerifyAttempt(session: MirasimBrowserSession): boolean {
+ if (mirasimBrowserSessions.get(session.state) !== session || session.completed) return false;
+ if (session.verifyAttempts >= MIRASIM_BROWSER_MAX_VERIFY_ATTEMPTS) return false;
+ session.verifyAttempts += 1;
+ session.verifyInFlight += 1;
+ return true;
+}
+
+function finishMirasimBrowserVerifyAttempt(session: MirasimBrowserSession): boolean {
+ session.verifyInFlight = Math.max(0, session.verifyInFlight - 1);
+ return mirasimBrowserSessions.get(session.state) === session && !session.completed;
+}
+
+function oauthBrowserHeaders(contentType?: string): Headers {
+ const headers = new Headers({
+ "Cache-Control": "no-store",
+ "Pragma": "no-cache",
+ "X-Frame-Options": "DENY",
+ "Content-Security-Policy": "default-src 'none'; img-src 'self'; style-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'",
+ "Referrer-Policy": "no-referrer",
+ "X-Content-Type-Options": "nosniff",
+ });
+ if (contentType) headers.set("Content-Type", contentType);
+ return headers;
+}
+
+function escapeHtml(value: string): string {
+ return value
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .replaceAll("'", "'");
+}
+
+function browserHtml(
+ locale: MirasimBrowserLocale,
+ title: string,
+ body: string,
+ status = 200,
+): Response {
+ const copy = MIRASIM_BROWSER_COPY[locale];
+ return new Response(
+ `${escapeHtml(title)}