From 4f3d2594fb1605ad5e5918054046c4cf6d3f3192 Mon Sep 17 00:00:00 2001 From: letr1n1ty Date: Mon, 21 Sep 2026 15:31:37 +0800 Subject: [PATCH 01/33] feat(mirasim): add native provider and signed relay support --- .../2026-09-21-mirasim-provider-design.md | 85 ++ src/adapters/base.ts | 14 + src/adapters/mirasim.ts | 312 ++++++ src/adapters/mirasim/anthropic.ts | 251 +++++ src/adapters/mirasim/compact.ts | 46 + src/adapters/mirasim/control-plane.ts | 427 ++++++++ src/adapters/mirasim/crypto.ts | 430 ++++++++ src/adapters/mirasim/transport.ts | 525 +++++++++ src/adapters/registry.ts | 8 + src/cli/dispatch.ts | 2 +- src/codex/catalog/provider-models.ts | 100 +- src/oauth/index.ts | 38 + src/oauth/login-cli.ts | 53 +- src/oauth/mirasim.ts | 996 ++++++++++++++++++ src/oauth/store.ts | 23 +- src/oauth/types.ts | 17 +- src/providers/mirasim-models.ts | 73 ++ src/providers/openai-tiers-destination.ts | 4 + src/providers/quota.ts | 8 + src/providers/quota/account-cache.ts | 18 +- src/providers/registry/entries-extended.ts | 33 + src/server/claude-messages.ts | 137 ++- src/server/index/serve-options.ts | 14 +- src/server/management/oauth-account-routes.ts | 18 +- src/server/management/provider-routes.ts | 21 + src/server/responses/compact.ts | 50 +- src/server/responses/core.ts | 6 +- src/server/responses/passthrough-delivery.ts | 125 ++- src/server/responses/passthrough-dispatch.ts | 137 ++- src/server/responses/request-transport.ts | 4 +- src/server/responses/sidecar-execution.ts | 4 +- src/server/search.ts | 129 +++ .../adapter-registry-authority.test.ts | 3 + tests/mirasim-crypto.test.ts | 105 ++ tests/providers/mirasim-control-plane.test.ts | 428 ++++++++ tests/providers/mirasim-endpoints.test.ts | 276 +++++ tests/providers/mirasim-oauth.test.ts | 332 ++++++ tests/providers/mirasim-provider.test.ts | 309 ++++++ .../responses-compaction-routing.test.ts | 14 + 39 files changed, 5502 insertions(+), 73 deletions(-) create mode 100644 docs/plans/2026-09-21-mirasim-provider-design.md create mode 100644 src/adapters/mirasim.ts create mode 100644 src/adapters/mirasim/anthropic.ts create mode 100644 src/adapters/mirasim/compact.ts create mode 100644 src/adapters/mirasim/control-plane.ts create mode 100644 src/adapters/mirasim/crypto.ts create mode 100644 src/adapters/mirasim/transport.ts create mode 100644 src/oauth/mirasim.ts create mode 100644 src/providers/mirasim-models.ts create mode 100644 tests/mirasim-crypto.test.ts create mode 100644 tests/providers/mirasim-control-plane.test.ts create mode 100644 tests/providers/mirasim-endpoints.test.ts create mode 100644 tests/providers/mirasim-oauth.test.ts create mode 100644 tests/providers/mirasim-provider.test.ts diff --git a/docs/plans/2026-09-21-mirasim-provider-design.md b/docs/plans/2026-09-21-mirasim-provider-design.md new file mode 100644 index 00000000000..22dc767da8b --- /dev/null +++ b/docs/plans/2026-09-21-mirasim-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/src/adapters/base.ts b/src/adapters/base.ts index 431e3fb1309..c73ce354559 100644 --- a/src/adapters/base.ts +++ b/src/adapters/base.ts @@ -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; diff --git a/src/adapters/mirasim.ts b/src/adapters/mirasim.ts new file mode 100644 index 00000000000..455436df545 --- /dev/null +++ b/src/adapters/mirasim.ts @@ -0,0 +1,312 @@ +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_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"; + +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-")) return "responses"; + throw new Error(`Mirasim supports Claude and GPT 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 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 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 responses = createResponsesPassthroughAdapter({ + ...provider, + adapter: "openai-responses", + authMode: "key", + apiKey: provider.apiKey, + upstreamWebsocket: false, + }); + + const parser = (wire: MirasimWire): ProviderAdapter => wire === "anthropic" ? anthropic : 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); + 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; + 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); + // GPT Responses is delivered as native passthrough, so its private adapter marker would + // otherwise become a client-visible response header. Only the translated Anthropic path + // needs the marker so parseStream/parseResponse can choose its delegate. + return wire === "anthropic" ? markResponseWire(response, wire) : response; + }, + + parseStream( + response: Response, + budget: TranslatorBudget, + tierMetadata?: AdapterTierMetadata, + ): AsyncGenerator { + return parser(responseWire(response)).parseStream(response, budget, tierMetadata); + }, + + 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..e3cc06d40a1 --- /dev/null +++ b/src/adapters/mirasim/control-plane.ts @@ -0,0 +1,427 @@ +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 }; + +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-")) 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; + try { await response.body?.cancel(); } catch { /* already closed */ } + return { ok: false, reason: status === 401 || status === 403 ? "auth" : "http", status }; + } + 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..3ca23a5044f --- /dev/null +++ b/src/adapters/mirasim/transport.ts @@ -0,0 +1,525 @@ +import { createHash, randomUUID } from "node:crypto"; +import type { AdapterFetchContext, AdapterRequest } from "../base"; +import { createAdapterPhysicalSend } from "../physical-send"; +import { credentialGeneration, getAccountSet } from "../../oauth/store"; +import type { MirasimOAuthMetadata } from "../../oauth/types"; +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_THREAD_HEADER = "x-opencodex-mirasim-thread"; +const INTERNAL_WIRE_HEADER = "x-opencodex-mirasim-wire"; +const CONTROL_PROVIDER_HEADER_NAMES = new Set(["x-mirasim-probe"]); + +interface StoredMirasimCredential { + accountSlotId: string; + accountIdentity: string; + generation: string; + accessToken: string; + metadata: MirasimOAuthMetadata; +} + +interface TicketState { + ticket?: string; + expiresAt?: number; + unmintableUntil?: number; +} + +const ticketCache = new Map(); +const sessionCache = 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, + }; +} + +/** + * 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 ticketKey(credential: StoredMirasimCredential, deviceId: string): string { + return createHash("sha256") + .update([credential.accountSlotId, credential.generation, deviceId].join("\0")) + .digest("hex"); +} + +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) { + const candidate = Math.floor(expiresAt * 1000); + if (candidate > now) return candidate; + } + return now + TICKET_DEFAULT_TTL_MS; +} + +async function boundedControlJson(response: Response): Promise> { + const text = await response.text(); + if (Buffer.byteLength(text, "utf8") > MAX_CONTROL_BODY) { + throw new Error("Mirasim device-session response is too large"); + } + try { + 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 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, +): Promise<{ credential: string; usedTicket: boolean }> { + const identity = createMirasimDeviceIdentity(credential.metadata.devicePrivateKey); + const key = ticketKey(credential, identity.deviceId); + const state = ticketCache.get(key) ?? {}; + const now = Date.now(); + + 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 }; + } + + 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 executor = ctx.executor ?? globalThis.fetch; + const response = await executor( + `${credential.metadata.relayUrl.replace(/\/$/, "")}${DEVICE_SESSION_PATH}`, + { + 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), + }, + ); + + if (response.status === 404 || response.status === 501) { + try { await response.body?.cancel(); } catch { /* already closed */ } + ticketCache.set(key, { + unmintableUntil: now + (response.status === 404 ? TICKET_404_QUIET_MS : TICKET_501_QUIET_MS), + }); + return { credential: credential.accessToken, usedTicket: false }; + } + if (!response.ok) { + try { await response.body?.cancel(); } catch { /* already closed */ } + if (state.ticket && state.expiresAt && now < state.expiresAt) { + return { credential: state.ticket, usedTicket: true }; + } + throw new Error(`Mirasim device-session mint failed with HTTP ${response.status}`); + } + + const payload = await boundedControlJson(response); + const ticket = typeof payload.ticket === "string" ? payload.ticket.trim() : ""; + if (!ticket || ticket.length > MAX_CONTROL_BODY || /[\r\n\0]/.test(ticket)) { + throw new Error("Mirasim device-session response contains an invalid ticket"); + } + const expiresAt = resolveTicketExpiry(now, payload); + ticketCache.set(key, { ticket, expiresAt }); + return { credential: ticket, usedTicket: true }; +} + +function cleanBaseHeaders(headers: Readonly>): { + headers: Record; + threadId?: string; +} { + const out: Record = {}; + let threadId: string | undefined; + for (const [name, value] of Object.entries(headers)) { + const lower = name.toLowerCase(); + 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, threadId }; +} + +function collectEnabled(): boolean { + const value = process.env.MIRASIM_COLLECT?.trim().toLowerCase(); + return value !== "0" && value !== "false" && value !== "off" && value !== "no"; +} + +function sessionId(cacheKey: string, accountIdentity: string, threadId: string | undefined): string { + if (threadId) { + return `mirasim_${createHash("sha256") + .update(`${accountIdentity}\0${threadId}`) + .digest("hex") + .slice(0, 32)}`; + } + const existing = sessionCache.get(cacheKey); + if (existing) return existing; + const created = `mirasim_${randomUUID()}`; + sessionCache.set(cacheKey, created); + return created; +} + +function inferenceMetadata( + credential: StoredMirasimCredential, + deviceId: string, + requestPath: string, + threadId?: string, +): Record { + const key = ticketKey(credential, deviceId); + const metadata: Record = { + "x-mirasim-session": sessionId(key, credential.accountIdentity, threadId), + "x-mirasim-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"); + } +} + +async function buildPhysicalRequest( + request: AdapterRequest, + ctx: AdapterFetchContext, + credential: StoredMirasimCredential, + forceFreshTicket: boolean, + controlPlane = false, + controlCredentialMode: "device-ticket" | "access-token" = "device-ticket", +): Promise<{ 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") { + ticketCache.delete(ticketKey(credential, identity.deviceId)); + } + const auth = controlPlane && controlCredentialMode === "access-token" + ? { credential: credential.accessToken, usedTicket: false } + : await mintDeviceTicket(credential, ctx); + const metadata = controlPlane + ? undefined + : inferenceMetadata(credential, identity.deviceId, path, 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(), + usedTicket: auth.usedTicket, + init: { + method: request.method, + redirect: "manual", + headers: { ...signedAndSealed, authorization: `Bearer ${auth.credential}` }, + ...(method === "GET" || method === "HEAD" ? {} : { body: request.body }), + signal: 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 }; +} + +/** + * 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. + */ +export async function fetchMirasimControl( + providerName: string, + provider: OcxProviderConfig, + accessToken: string, + path: string, + options: MirasimControlRequestOptions = {}, +): Promise { + const credential = matchingCredential(accessToken); + 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 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"; + let physical = await buildPhysicalRequest( + request, + ctx, + credential, + false, + true, + credentialMode, + ); + appendControlProviderHeaders(physical, providerHeaders); + let response = await executor(physical.url, physical.init); + if (response.status !== 401 || !physical.usedTicket) return response; + + const replacement = await buildPhysicalRequest( + request, + ctx, + credential, + true, + true, + credentialMode, + ); + appendControlProviderHeaders(replacement, providerHeaders); + try { await response.body?.cancel(); } catch { /* already closed */ } + physical = replacement; + response = await executor(physical.url, physical.init); + return response; +} + +export async function fetchMirasim( + request: AdapterRequest, + accessToken: string, + ctx: AdapterFetchContext = {}, +): Promise { + const credential = matchingCredential(accessToken); + const send = createAdapterPhysicalSend(ctx); + let physical = await buildPhysicalRequest(request, ctx, credential, false); + const response = await send({ + url: physical.url, + dispatch: executor => executor(physical.url, physical.init), + }); + if (response.status !== 401 || !physical.usedTicket) return response; + + try { + return await send({ + url: physical.url, + sendClass: "auth-recovery", + recovery: "oauth-401", + beforeDispatch: async () => { + // Build the replacement first. If re-minting fails the original 401 remains readable + // rather than returning a Response whose body we already cancelled. + const replacement = await buildPhysicalRequest(request, ctx, credential, true); + try { await response.body?.cancel(); } catch { /* already closed */ } + physical = replacement; + }, + dispatch: executor => executor(physical.url, physical.init), + }); + } catch (error) { + if (!response.bodyUsed) return response; + throw error; + } +} + +export const MIRASIM_INTERNAL_WIRE_HEADER = INTERNAL_WIRE_HEADER; +export const MIRASIM_INTERNAL_THREAD_HEADER = INTERNAL_THREAD_HEADER; + +export function resetMirasimTransportStateForTests(): void { + ticketCache.clear(); + sessionCache.clear(); +} 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..59c634d1198 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 { @@ -116,7 +118,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 +439,102 @@ 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. + const authorityIdentity = mirasimCredentialCacheScope(apiKey); + 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) && scopedStale) { + return observed(withConfiguredRetention(scopedStale), "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, + ...(model.displayName ? { displayName: model.displayName } : {}), + ...(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()) { + markModelsFetchFailure(name, undefined, authorityIdentity); + 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/oauth/index.ts b/src/oauth/index.ts index 29acaf210dd..469053abe6d 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 { loginMirasim, mirasimRelayUrl, refreshMirasimToken } from "./mirasim"; import { loginOrcaRouter, orcaRouterInferenceBaseUrl, refreshOrcaRouterKey } from "./orcarouter"; import { ANTIGRAVITY_REQUEST_UA } from "../adapters/google-antigravity-wire"; import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive"; @@ -186,6 +187,18 @@ export interface LoginOpts { forceLogin?: boolean; /** When set, persist into this account slot and require matching identity. */ reauthAccountId?: string; + /** + * Management-owned browser origin for Mirasim OAuth. + * The GUI supplies this so Mirasim can return to the long-lived OpenCodex server + * instead of a random ephemeral loopback listener. + */ + mirasimBrowserBaseUrl?: string; + /** Dashboard/browser locale forwarded to the Mirasim management-owned OAuth pages. */ + mirasimBrowserLocale?: string; + /** Mirasim CLI-only email-code login. Browser/GUI login leaves these unset. */ + mirasimEmail?: string; + /** Optional already-received Mirasim email verification code. */ + mirasimCode?: string; /** * ChatGPT only: `device` selects the deviceauth grant instead of the * localhost:1455 callback flow, for hosts with no browser or no loopback @@ -235,6 +248,30 @@ function oauthDefaultModel(id: string): string { } export const OAUTH_PROVIDERS: Record = { + mirasim: { + login: (ctrl, opts) => loginMirasim(ctrl, { + ...(opts?.mirasimBrowserBaseUrl ? { browserBaseUrl: opts.mirasimBrowserBaseUrl } : {}), + ...(opts?.mirasimBrowserLocale ? { browserLocale: opts.mirasimBrowserLocale } : {}), + ...(opts?.mirasimEmail ? { email: opts.mirasimEmail } : {}), + ...(opts?.mirasimCode ? { code: opts.mirasimCode } : {}), + }), + refresh: (refreshToken, signal, credential) => + refreshMirasimToken(refreshToken, signal, credential), + providerConfig: { + ...oauthConfig("mirasim"), + baseUrl: mirasimRelayUrl(), + upstreamHttpVersion: "http1.1", + }, + resolveProviderConfig: () => ({ + ...oauthConfig("mirasim"), + baseUrl: mirasimRelayUrl(), + upstreamHttpVersion: "http1.1", + }), + defaultModel: oauthDefaultModel("mirasim"), + // Mirasim refresh tokens are used only when a request needs a fresh bearer. Avoid creating + // unattended auth traffic on behalf of a relay account. + defaultRefreshPolicy: "lazy-only", + }, "command-code": { // Add-account/reauth must not reimport the current local CLI credential. login: (ctrl, opts) => loginCommandCode(ctrl, { importLocal: opts?.forceLogin ? "off" : "fallback" }), @@ -661,6 +698,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", diff --git a/src/oauth/login-cli.ts b/src/oauth/login-cli.ts index 6197047b996..16497fb8f37 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,43 @@ 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 { +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("Usage: ocx login mirasim --email
[--code ]"); + email = value; + continue; + } + if (arg === "--code") { + const value = args[++index]?.trim(); + if (!value) throw new Error("Usage: ocx login mirasim --email
[--code ]"); + code = value; + continue; + } + throw new Error(`Unknown Mirasim login option: ${arg}`); + } + if (!email) throw new Error("--code requires --email for Mirasim login"); + 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,13 +162,20 @@ 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; @@ -152,13 +191,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): "); }, - }); + }, opts); }); // 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..339a8d61cd6 --- /dev/null +++ b/src/oauth/mirasim.ts @@ -0,0 +1,996 @@ +import { randomBytes } from "node:crypto"; +import { createMirasimDeviceIdentity } from "../adapters/mirasim/crypto"; +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 PROVIDER_SLUG = /^[a-z][a-z0-9_-]{0,63}$/; +const EMAIL_ADDRESS = /^[^@\s]+@[^@\s.]+(?:\.[^@\s.]+)+$/; + +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; + /** CLI-only alternative for accounts that are not bound to GitHub/Google OAuth. */ + 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: "Choose the account provider you want to use.", + continueWith: provider => `Continue with ${provider}`, + 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: "Choose GitHub or Google on the Mirasim sign-in page.", + waitingForBrowser: "Waiting for Mirasim browser authentication...", + continueInstruction: provider => `Continue with ${provider}.`, + }, + "zh-TW": { + htmlLang: "zh-TW", + signInTitle: "登入 Mirasim", + chooseProvider: "選擇要用於登入的帳號供應商。", + continueWith: provider => `使用 ${provider} 繼續`, + 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 登入頁面選擇 GitHub 或 Google。", + waitingForBrowser: "正在等待 Mirasim 瀏覽器驗證…", + continueInstruction: provider => `使用 ${provider} 繼續。`, + }, + "zh-CN": { + htmlLang: "zh-CN", + signInTitle: "登录 Mirasim", + chooseProvider: "选择用于登录的账户提供商。", + continueWith: provider => `使用 ${provider} 继续`, + 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 登录页面选择 GitHub 或 Google。", + 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", + ); +} + +function defaultMirasimMetadata(existingPrivateKey?: string): MirasimOAuthMetadata { + const identity = createMirasimDeviceIdentity(existingPrivateKey); + return { + devicePrivateKey: identity.privateKeyPem, + relayUrl: mirasimRelayUrl(), + adminUrl: mirasimAdminUrl(), + clientVersion: (process.env.MIRASIM_CLIENT_VERSION ?? MIRASIM_CLIENT_VERSION).trim() || MIRASIM_CLIENT_VERSION, + }; +} + +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) throw new Error("Mirasim authentication response is too large"); + const text = await response.text(); + if (Buffer.byteLength(text, "utf8") > MAX_AUTH_BODY) throw new Error("Mirasim authentication response is too large"); + let parsed: unknown; + try { + 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 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, + ); +} + +async function discoverLoginProviders(adminUrl: string, signal?: AbortSignal): Promise { + const response = await fetch(`${adminUrl}/auth/oauth/providers`, { + headers: { Accept: "application/json" }, + redirect: "error", + signal: requestSignal(signal), + }); + if (!response.ok) throw new Error(`Mirasim sign-in provider discovery failed with HTTP ${response.status}`); + const payload = await boundedJson(response); + const rows = Array.isArray(payload.providers) ? payload.providers : []; + const providers: string[] = []; + const seen = new Set(); + for (const value of rows) { + if (typeof value !== "string") continue; + const id = value.trim().toLowerCase(); + if (!PROVIDER_SLUG.test(id) || seen.has(id)) continue; + seen.add(id); + providers.push(id); + } + if (providers.length === 0) throw new Error("Mirasim has no enabled sign-in provider"); + return providers; +} + +function chooseLoginProvider(providers: string[]): string { + const configured = process.env.MIRASIM_OAUTH_PROVIDER?.trim().toLowerCase(); + if (configured) { + if (!PROVIDER_SLUG.test(configured) || !providers.includes(configured)) { + throw new Error(`Mirasim sign-in provider "${configured}" is not currently offered`); + } + return configured; + } + return providers.includes("github") ? "github" : providers[0]!; +} + +interface CallbackTokens { + accessToken: string; + refreshToken: string; +} + +function parseCallbackTokens(url: URL, expectedState: string): CallbackTokens { + const returnedState = url.searchParams.get("state")?.trim(); + // Some Mirasim deployments omit state. The unguessable callback path is then the channel + // binding; a present state must still match exactly. + if (returnedState && returnedState !== expectedState) throw new Error("Mirasim OAuth state mismatch"); + const error = url.searchParams.get("error")?.trim(); + if (error) throw new Error("Mirasim OAuth login was cancelled or rejected"); + const accessToken = url.searchParams.get("access_token")?.trim() || url.searchParams.get("token")?.trim() || ""; + const refreshToken = url.searchParams.get("refresh_token")?.trim() || ""; + return { + accessToken: normalizeCredentialSecret("access token", accessToken), + refreshToken: normalizeCredentialSecret("refresh token", refreshToken), + }; +} + +function parseManualCallback(input: string, expectedState: string): CallbackTokens { + const trimmed = input.trim(); + if (!trimmed) throw new Error("Mirasim OAuth callback is empty"); + try { + return parseCallbackTokens(new URL(trimmed), expectedState); + } catch (error) { + if (!trimmed.includes("=")) throw error; + return parseCallbackTokens(new URL(`http://localhost/?${trimmed.replace(/^\?/, "")}`), expectedState); + } +} + +const MIRASIM_BROWSER_START_PATH = "/oauth/mirasim/start"; +const MIRASIM_BROWSER_CALLBACK_PREFIX = "/oauth/mirasim/callback/"; +const MIRASIM_BROWSER_COMPLETED_TTL_MS = 30_000; + +interface MirasimBrowserSession { + state: string; + locale: MirasimBrowserLocale; + callbackToken: string; + callbackPath: string; + adminUrl: string; + baseUrl: string; + expiresAt: number; + pendingTokens?: CallbackTokens; + completed: boolean; + promise: Promise; + resolve: (tokens: CallbackTokens) => void; + reject: (error: Error) => void; + timeout: ReturnType; + abort?: () => void; +} + +const mirasimBrowserSessions = new Map(); +const mirasimBrowserCallbackStates = 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); + mirasimBrowserCallbackStates.delete(session.callbackToken); + 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 callbackToken = randomBytes(18).toString("base64url"); + const callbackPath = `${MIRASIM_BROWSER_CALLBACK_PREFIX}${callbackToken}`; + 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, + callbackToken, + callbackPath, + adminUrl, + baseUrl, + expiresAt: Date.now() + LOGIN_TIMEOUT_MS, + 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); + mirasimBrowserCallbackStates.set(callbackToken, state); + + 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 callbackTokenFromPath(pathname: string): string | undefined { + if (!pathname.startsWith(MIRASIM_BROWSER_CALLBACK_PREFIX)) return undefined; + const token = pathname.slice(MIRASIM_BROWSER_CALLBACK_PREFIX.length); + if (!/^[A-Za-z0-9_-]{20,}$/.test(token) || token.includes("/")) return undefined; + return token; +} + +function currentMirasimBrowserSessionByCallback(pathname: string): MirasimBrowserSession | undefined { + const callbackToken = callbackTokenFromPath(pathname); + if (!callbackToken) return undefined; + const state = mirasimBrowserCallbackStates.get(callbackToken); + if (!state) return undefined; + const session = currentMirasimBrowserSession(state); + return session?.callbackToken === callbackToken ? session : undefined; +} + +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'", + "Referrer-Policy": "no-referrer", + }); + 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)}

${escapeHtml(title)}

${body}`, + { status, headers: oauthBrowserHeaders("text/html; charset=utf-8") }, + ); +} + +function renderMirasimProviderSelection( + state: string, + providers: string[], + locale: MirasimBrowserLocale, +): Response { + const copy = MIRASIM_BROWSER_COPY[locale]; + const links = providers.map(provider => { + const href = new URL(MIRASIM_BROWSER_START_PATH, "http://localhost"); + href.searchParams.set("state", state); + href.searchParams.set("provider", provider); + href.searchParams.set("lang", locale); + const label = provider === "github" ? "GitHub" : provider === "google" ? "Google" : provider; + return `
  • ${escapeHtml(copy.continueWith(label))}
  • `; + }).join(""); + return browserHtml( + locale, + copy.signInTitle, + `Mirasim

    ${escapeHtml(copy.chooseProvider)}

      ${links}
    `, + ); +} + +function settleMirasimBrowserSession(session: MirasimBrowserSession, tokens: CallbackTokens): void { + if (session.completed) return; + session.completed = true; + session.pendingTokens = undefined; + clearTimeout(session.timeout); + session.abort?.(); + session.abort = undefined; + session.resolve(tokens); + const cleanup = setTimeout(() => { + if (mirasimBrowserSessions.get(session.state) === session) { + mirasimBrowserCallbackStates.delete(session.callbackToken); + mirasimBrowserSessions.delete(session.state); + } + }, MIRASIM_BROWSER_COMPLETED_TTL_MS); + cleanup.unref?.(); +} + +function rejectMirasimBrowserSession(session: MirasimBrowserSession, error: Error): void { + if (mirasimBrowserSessions.get(session.state) !== session) return; + mirasimBrowserCallbackStates.delete(session.callbackToken); + mirasimBrowserSessions.delete(session.state); + clearTimeout(session.timeout); + session.abort?.(); + session.abort = undefined; + session.pendingTokens = undefined; + session.reject(error); +} + +/** + * Public browser resources for the management-owned Mirasim OAuth flow. + * + * These routes deliberately sit outside /api management auth. The 256-bit pending state is the + * capability, matching the upstream CPA design: the start route only chooses an offered provider, + * while the callback accepts bounded renewable credentials only for that one live state. + */ +export async function handleMirasimBrowserOAuthRequest( + req: Request, + url = new URL(req.url), +): Promise { + const isStart = url.pathname === MIRASIM_BROWSER_START_PATH; + const isCallback = callbackTokenFromPath(url.pathname) !== undefined; + if (!isStart && !isCallback) { + return null; + } + const requestLocale = normalizeMirasimBrowserLocale( + url.searchParams.get("lang") ?? req.headers.get("accept-language"), + ); + if (req.method !== "GET") { + return new Response(MIRASIM_BROWSER_COPY[requestLocale].methodNotAllowed, { + status: 405, + headers: oauthBrowserHeaders("text/plain; charset=utf-8"), + }); + } + + const returnedState = url.searchParams.get("state")?.trim() ?? ""; + const session = isStart + ? (returnedState ? currentMirasimBrowserSession(returnedState) : undefined) + : currentMirasimBrowserSessionByCallback(url.pathname); + if (!session) { + const copy = MIRASIM_BROWSER_COPY[requestLocale]; + return browserHtml( + requestLocale, + copy.expiredTitle, + `

    ${escapeHtml(copy.expiredBody)}

    `, + 400, + ); + } + const locale = session.locale; + const copy = MIRASIM_BROWSER_COPY[locale]; + const state = session.state; + if (returnedState && returnedState !== state) { + rejectMirasimBrowserSession(session, new Error("Mirasim OAuth state mismatch")); + return browserHtml( + locale, + copy.failedTitle, + `

    ${escapeHtml(copy.mismatchBody)}

    `, + 400, + ); + } + + if (isStart) { + let providers: string[]; + try { + providers = await discoverLoginProviders(session.adminUrl, req.signal); + } catch { + return browserHtml( + locale, + copy.unavailableTitle, + `

    ${escapeHtml(copy.unavailableBody)}

    `, + 503, + ); + } + const provider = url.searchParams.get("provider")?.trim().toLowerCase() ?? ""; + if (!provider) return renderMirasimProviderSelection(state, providers, locale); + if (!PROVIDER_SLUG.test(provider) || !providers.includes(provider)) { + return browserHtml( + locale, + copy.unsupportedTitle, + `

    ${escapeHtml(copy.unsupportedBody)}

    `, + 400, + ); + } + + const callbackUrl = new URL(session.callbackPath, `${session.baseUrl}/`); + const authUrl = new URL(`${session.adminUrl}/auth/oauth/${encodeURIComponent(provider)}/login`); + authUrl.searchParams.set("redirect_uri", callbackUrl.toString()); + authUrl.searchParams.set("state", state); + const headers = oauthBrowserHeaders(); + headers.set("Location", authUrl.toString()); + return new Response(null, { status: 302, headers }); + } + + if (url.searchParams.get("result") === "complete") { + if (session.completed) { + return browserHtml( + locale, + copy.completeTitle, + `

    ${escapeHtml(copy.completeBody)}

    `, + ); + } + const tokens = session.pendingTokens; + if (!tokens) { + return browserHtml( + locale, + copy.incompleteTitle, + `

    ${escapeHtml(copy.incompleteBody)}

    `, + 400, + ); + } + settleMirasimBrowserSession(session, tokens); + return browserHtml( + locale, + copy.completeTitle, + `

    ${escapeHtml(copy.completeBody)}

    `, + ); + } + + try { + const tokens = parseCallbackTokens(url, state); + session.pendingTokens = tokens; + const clean = new URL(session.callbackPath, `${session.baseUrl}/`); + clean.searchParams.set("state", state); + clean.searchParams.set("result", "complete"); + const headers = oauthBrowserHeaders(); + headers.set("Location", clean.toString()); + return new Response(null, { status: 303, headers }); + } catch (error) { + rejectMirasimBrowserSession( + session, + error instanceof Error ? error : new Error("Mirasim OAuth callback failed"), + ); + return browserHtml( + locale, + copy.failedTitle, + `

    ${escapeHtml(copy.failedBody)}

    `, + 400, + ); + } +} + +async function waitForCallback( + ctrl: OAuthController, + expectedState: string, + callbackPath: string, +): Promise<{ callbackUrl: string; tokens: Promise; stop: () => void }> { + let resolveTokens!: (tokens: CallbackTokens) => void; + let rejectTokens!: (error: Error) => void; + const tokens = new Promise((resolve, reject) => { + resolveTokens = resolve; + rejectTokens = reject; + }); + + let consumed = false; + const server = Bun.serve({ + hostname: "127.0.0.1", + port: 0, + reusePort: false, + fetch(request) { + const url = new URL(request.url); + const locale = normalizeMirasimBrowserLocale(request.headers.get("accept-language")); + const copy = MIRASIM_BROWSER_COPY[locale]; + if (url.pathname !== callbackPath) return new Response(copy.notFound, { status: 404 }); + if (consumed) return new Response(copy.callbackAlreadyUsed, { status: 409 }); + consumed = true; + try { + resolveTokens(parseCallbackTokens(url, expectedState)); + return new Response( + `

    ${escapeHtml(copy.completeTitle)}

    ${escapeHtml(copy.completeBody)}

    `, + { headers: { "Content-Type": "text/html; charset=utf-8", "Connection": "close" } }, + ); + } catch (error) { + const failure = error instanceof Error ? error : new Error("Mirasim OAuth callback failed"); + rejectTokens(failure); + return new Response(copy.failedBody, { + status: 400, + headers: { "Content-Type": "text/plain; charset=utf-8", "Connection": "close" }, + }); + } + }, + }); + + const timeout = setTimeout(() => rejectTokens(new Error("Mirasim OAuth login timed out")), LOGIN_TIMEOUT_MS); + timeout.unref?.(); + const abort = () => rejectTokens(new Error("Mirasim OAuth login cancelled")); + ctrl.signal?.addEventListener("abort", abort, { once: true }); + + const stop = () => { + clearTimeout(timeout); + ctrl.signal?.removeEventListener("abort", abort); + server.stop(true); + }; + return { + callbackUrl: `http://127.0.0.1:${server.port}${callbackPath}`, + tokens: tokens.finally(stop), + stop, + }; +} + +export async function loginMirasim( + ctrl: OAuthController, + options: MirasimLoginOptions = {}, +): Promise { + const mirasim = defaultMirasimMetadata(); + const browserLocale = normalizeMirasimBrowserLocale(options.browserLocale); + const browserCopy = MIRASIM_BROWSER_COPY[browserLocale]; + if (options.email) { + return loginMirasimWithEmail(ctrl, mirasim, { + email: options.email, + ...(options.code ? { code: options.code } : {}), + }); + } + if (options.code) throw new Error("Mirasim --code requires --email"); + const state = randomBytes(32).toString("base64url"); + if (options.browserBaseUrl) { + const pending = createMirasimBrowserSession( + ctrl, + state, + mirasim.adminUrl, + options.browserBaseUrl, + browserLocale, + ); + ctrl.onAuth?.({ + url: pending.startUrl, + instructions: browserCopy.chooseProviderInstruction, + }); + ctrl.onProgress?.(browserCopy.waitingForBrowser); + + const browserResult = pending.tokens.then(tokens => ({ source: "browser" as const, tokens })); + const result = ctrl.onManualCodeInput + ? await Promise.race([ + browserResult, + ctrl.onManualCodeInput(state).then(input => ({ + source: "manual" as const, + tokens: parseManualCallback(input, state), + })), + ]) + : await browserResult; + if (result.source === "manual") pending.discard(); + return enrichMirasimCredentialFromProfile( + credentialsFromTokens(result.tokens.accessToken, result.tokens.refreshToken, mirasim), + ctrl.signal, + ); + } + + const providers = await discoverLoginProviders(mirasim.adminUrl, ctrl.signal); + const provider = chooseLoginProvider(providers); + const callbackPath = `/mirasim/oauth/${randomBytes(24).toString("base64url")}`; + const pending = await waitForCallback(ctrl, state, callbackPath); + + const authUrl = new URL(`${mirasim.adminUrl}/auth/oauth/${encodeURIComponent(provider)}/login`); + authUrl.searchParams.set("redirect_uri", pending.callbackUrl); + authUrl.searchParams.set("state", state); + ctrl.onAuth?.({ + url: authUrl.toString(), + instructions: browserCopy.continueInstruction( + provider === "github" ? "GitHub" : provider === "google" ? "Google" : provider, + ), + }); + ctrl.onProgress?.(browserCopy.waitingForBrowser); + + try { + const callbackPromise = pending.tokens; + const tokens = ctrl.onManualCodeInput + ? await Promise.race([ + callbackPromise, + ctrl.onManualCodeInput(state).then(input => parseManualCallback(input, state)), + ]) + : await callbackPromise; + return enrichMirasimCredentialFromProfile( + credentialsFromTokens(tokens.accessToken, tokens.refreshToken, mirasim), + ctrl.signal, + ); + } finally { + pending.stop(); + } +} + +export async function refreshMirasimToken( + refreshToken: string, + signal?: AbortSignal, + credential?: OAuthCredentials, +): Promise { + const priorMetadata = credential?.mirasim; + const mirasim = priorMetadata + ? { + ...priorMetadata, + relayUrl: validatedServiceUrl(priorMetadata.relayUrl, "relay"), + adminUrl: validatedServiceUrl(priorMetadata.adminUrl, "authentication service"), + } + : defaultMirasimMetadata(); + + const response = await fetch(`${mirasim.adminUrl}/auth/refresh`, { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + body: JSON.stringify({ refresh_token: normalizeCredentialSecret("refresh token", refreshToken) }), + redirect: "error", + signal: requestSignal(signal), + }); + if (!response.ok) { + // The request body contains a long-lived secret. Do not reflect the upstream response body. + throw new Error(`Mirasim token refresh failed with HTTP ${response.status}`); + } + const payload = await boundedJson(response); + const accessToken = typeof payload.access_token === "string" ? payload.access_token : ""; + const rotatedRefresh = typeof payload.refresh_token === "string" && payload.refresh_token.trim() + ? payload.refresh_token + : refreshToken; + const expiresIn = typeof payload.expires_in === "number" ? payload.expires_in : undefined; + return credentialsFromTokens(accessToken, rotatedRefresh, mirasim, expiresIn); +} diff --git a/src/oauth/store.ts b/src/oauth/store.ts index 25c3e7dfda4..34193722466 100644 --- a/src/oauth/store.ts +++ b/src/oauth/store.ts @@ -597,6 +597,22 @@ function normalizeCredential(cred: unknown): OAuthCredentials | null { }; } } + if (candidate.mirasim && typeof candidate.mirasim === "object") { + const mirasim = candidate.mirasim; + const cleanMirasim = (value: unknown, max: number): string | undefined => { + if (typeof value !== "string") return undefined; + const trimmed = value.trim(); + return trimmed && trimmed.length <= max && !value.includes("\0") ? trimmed : undefined; + }; + const devicePrivateKey = cleanMirasim(mirasim.devicePrivateKey, 16_384); + const relayUrl = cleanMirasim(mirasim.relayUrl, 2048); + const adminUrl = cleanMirasim(mirasim.adminUrl, 2048); + const clientVersion = cleanMirasim(mirasim.clientVersion, 128); + // The private key is load-bearing. Metadata-only legacy/crafted rows are discarded. + if (devicePrivateKey && relayUrl && adminUrl && clientVersion) { + normalized.mirasim = { devicePrivateKey, relayUrl, adminUrl, clientVersion }; + } + } return normalized; } @@ -1473,7 +1489,12 @@ export async function replaceProviderAccountSet( activeAccountId: set.activeAccountId, accounts: set.accounts.map(account => ({ id: account.id, - credential: { ...account.credential, ...(account.credential.kiro ? { kiro: { ...account.credential.kiro } } : {}) }, + credential: { + ...account.credential, + ...(account.credential.kiro ? { kiro: { ...account.credential.kiro } } : {}), + ...(account.credential.muse ? { muse: { ...account.credential.muse } } : {}), + ...(account.credential.mirasim ? { mirasim: { ...account.credential.mirasim } } : {}), + }, ...(account.alias ? { alias: account.alias } : {}), ...(account.needsReauth ? { needsReauth: true } : {}), ...(account.refreshAttentionGeneration ? { refreshAttentionGeneration: account.refreshAttentionGeneration } : {}), diff --git a/src/oauth/types.ts b/src/oauth/types.ts index 05852753f38..9bf6134d5ae 100644 --- a/src/oauth/types.ts +++ b/src/oauth/types.ts @@ -54,6 +54,19 @@ export interface MuseOAuthMetadata { tierName?: string; } +/** + * Account-scoped Mirasim device material. + * + * The Ed25519 private key is a request-signing secret. It stays inside the protected OAuth + * credential store and must never be projected into management/status responses. + */ +export interface MirasimOAuthMetadata { + devicePrivateKey: string; + relayUrl: string; + adminUrl: string; + clientVersion: string; +} + export type OAuthCredentials = { refresh: string; access: string; @@ -77,6 +90,8 @@ export type OAuthCredentials = { kiro?: KiroOAuthMetadata; /** Never returned by management APIs; persisted only inside the protected auth-store boundary. */ muse?: MuseOAuthMetadata; + /** Never returned by management APIs; contains the Mirasim device signing private key. */ + mirasim?: MirasimOAuthMetadata; }; /** One logged-in account inside a provider's account set (multiauth). */ @@ -125,7 +140,7 @@ export interface OAuthAccountSelection { export interface OAuthController { onAuth?(info: { url: string; instructions?: string; deviceCode?: string }): void; onProgress?(message: string): void; - onManualCodeInput?(expectedState?: string): Promise; + onManualCodeInput?(expectedState?: string, prompt?: string): Promise; signal?: AbortSignal; } diff --git a/src/providers/mirasim-models.ts b/src/providers/mirasim-models.ts new file mode 100644 index 00000000000..6355812be06 --- /dev/null +++ b/src/providers/mirasim-models.ts @@ -0,0 +1,73 @@ +export const MIRASIM_MODELS = [ + "claude-fable-5", + "claude-fable-5-1", + "claude-haiku-4-5", + "claude-opus-4-6", + "claude-opus-4-8", + "claude-opus-5", + "claude-sonnet-5", + "gpt-6-astra", + "gpt-5.6-luna", + "gpt-5.6-sol", + "gpt-5.6-terra", +] as const; + +export const MIRASIM_MODEL_CONTEXT_WINDOWS: Record = { + "claude-fable-5": 1_000_000, + "claude-fable-5-1": 1_000_000, + "claude-haiku-4-5": 200_000, + "claude-opus-4-6": 1_000_000, + "claude-opus-4-8": 1_000_000, + "claude-opus-5": 1_000_000, + "claude-sonnet-5": 1_000_000, + "gpt-6-astra": 872_000, + "gpt-5.6-luna": 372_000, + "gpt-5.6-sol": 372_000, + "gpt-5.6-terra": 372_000, +}; + +export const MIRASIM_MODEL_MAX_OUTPUT_TOKENS: Record = { + "claude-fable-5": 128_000, + "claude-fable-5-1": 128_000, + "claude-haiku-4-5": 64_000, + "claude-opus-4-6": 128_000, + "claude-opus-4-8": 128_000, + "claude-opus-5": 128_000, + "claude-sonnet-5": 128_000, + "gpt-6-astra": 128_000, + "gpt-5.6-luna": 128_000, + "gpt-5.6-sol": 128_000, + "gpt-5.6-terra": 128_000, +}; + +export const MIRASIM_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max", "ultra"]; + +export const MIRASIM_MODEL_DISPLAY_NAMES: Record = { + "claude-fable-5": "Claude Fable 5", + "claude-fable-5-1": "Claude Fable 5.1", + "claude-haiku-4-5": "Claude 4.5 Haiku", + "claude-opus-4-6": "Claude 4.6 Opus", + "claude-opus-4-8": "Claude Opus 4.8", + "claude-opus-5": "Claude Opus 5", + "claude-sonnet-5": "Claude Sonnet 5", + "gpt-6-astra": "GPT 6 Astra", + "gpt-5.6-luna": "GPT 5.6 Luna", + "gpt-5.6-sol": "GPT 5.6 Sol", + "gpt-5.6-terra": "GPT 5.6 Terra", +}; + +const MIRASIM_LONG_CONTEXT_BASE_MODELS = MIRASIM_MODELS.filter(model => + model.startsWith("claude-") && (MIRASIM_MODEL_CONTEXT_WINDOWS[model] ?? 0) >= 1_000_000 +); + +export const MIRASIM_SELECTABLE_MODELS = [ + ...MIRASIM_MODELS, + ...MIRASIM_LONG_CONTEXT_BASE_MODELS.map(model => `${model}[1m]`), +]; + +for (const model of MIRASIM_LONG_CONTEXT_BASE_MODELS) { + const alias = `${model}[1m]`; + MIRASIM_MODEL_CONTEXT_WINDOWS[alias] = MIRASIM_MODEL_CONTEXT_WINDOWS[model]!; + MIRASIM_MODEL_MAX_OUTPUT_TOKENS[alias] = MIRASIM_MODEL_MAX_OUTPUT_TOKENS[model]!; + MIRASIM_MODEL_DISPLAY_NAMES[alias] = `${MIRASIM_MODEL_DISPLAY_NAMES[model] ?? model} [1m]`; +} diff --git a/src/providers/openai-tiers-destination.ts b/src/providers/openai-tiers-destination.ts index a4a8e3f7f97..b9e83eee8f5 100644 --- a/src/providers/openai-tiers-destination.ts +++ b/src/providers/openai-tiers-destination.ts @@ -75,6 +75,9 @@ export function supportsNativeResponsesCompactEndpoint( provider: OcxProviderConfig, ): boolean { if (isCanonicalOpenAiForwardProvider(provider)) return true; + if (providerName === "mirasim" && provider.adapter === "mirasim") { + return normalizedBaseUrl(provider.baseUrl) === "https://relay.mirasim.ai"; + } return providerName === OPENAI_API_PROVIDER_ID && provider.adapter === "openai-responses" && normalizedBaseUrl(provider.baseUrl) === OPENAI_API_BASE_URL; @@ -107,5 +110,6 @@ export function isOpenAiOperatedResponsesDestination(provider: OcxProviderConfig */ export function destinationDecodesNativeCompactionBlob(provider: OcxProviderConfig): boolean { return isOpenAiOperatedResponsesDestination(provider) + || (provider.adapter === "mirasim" && normalizedBaseUrl(provider.baseUrl) === "https://relay.mirasim.ai") || provider.decodesNativeCompactionBlobs === true; } diff --git a/src/providers/quota.ts b/src/providers/quota.ts index a6403fbfdcd..1745a5e7c42 100644 --- a/src/providers/quota.ts +++ b/src/providers/quota.ts @@ -30,6 +30,7 @@ import { isProviderQuotaReportCurrent, LAST_GOOD_MAX_AGE_MS, providerQuotaBeforePublishForTests, + report, routingEvidence, setProviderQuotaReportCache, TERMINAL_QUOTA_FAILURE, @@ -70,6 +71,8 @@ import { fetchCommandCodeQuota, fetchKimiQuota, keyQuotaReaderForProvider } from import { antigravityQuotaDiagnosticIdentity, fetchAntigravityQuota, probeAntigravityUsageQuota } from "./quota/antigravity"; import { persistKiroAccountState } from "./kiro-account-state-disk"; import { kiroProbeCurrent, kiroProbeIdentity } from "./quota/kiro-account-probe"; +import { AnthropicQuotaProbeOwnershipError, anthropicCooldownFlightKey, assertAnthropicQuotaSendAllowed, probeAnthropicQuotaWithRecovery } from "./quota/anthropic-cooldown-recovery"; +import { fetchMirasimQuota } from "../adapters/mirasim/control-plane"; export type { ProviderQuota, ProviderQuotaCreditsUsd, ProviderQuotaWindow } from "./quota-types"; export { QUOTA_RESPONSE_MAX_BYTES } from "./quota-wire"; export { @@ -364,6 +367,11 @@ async function readExplicitAccountQuota(provider: string, accountId: string, con case "kimi": result = await fetchKimiQuota(provider, config, accessToken); break; case "command-code": result = await fetchCommandCodeQuota(provider, config, accessToken); break; case "devin": result = await (await import("./quota/devin")).fetchDevinQuota(provider, accessToken, credential.apiBaseUrl, config.baseUrl); break; + case "mirasim": { + const quota = await fetchMirasimQuota(provider, config, accessToken); + result = quota ? report(provider, "mirasim:/v1/limits", quota) : null; + break; + } default: return null; } return { result, identity, isCurrent }; diff --git a/src/providers/quota/account-cache.ts b/src/providers/quota/account-cache.ts index 97a41836fe9..423d10686f2 100644 --- a/src/providers/quota/account-cache.ts +++ b/src/providers/quota/account-cache.ts @@ -199,8 +199,8 @@ export function supportsPerAccountQuota(provider: string): boolean { } export function explicitAccountReader(provider: string): boolean { - return provider === "xai" || provider === "cursor" || provider === "kimi" || provider === "command-code" - || provider === "devin"; + return provider === "xai" || provider === "cursor" || provider === "kimi" + || provider === "command-code" || provider === "devin" || provider === "mirasim"; } export function providerOAuthAccountQuotaMode(provider: string): AccountQuotaMode { @@ -539,6 +539,9 @@ export function quotaCredentialIdentity(provider: string, accountId: string, cre return createHash("sha256").update(JSON.stringify([ provider, accountId, credential.access, credential.refresh, credential.expires, credential.accountId, credential.projectId, credential.source, + credential.mirasim?.devicePrivateKey, + credential.mirasim?.relayUrl, + credential.mirasim?.clientVersion, target.adapter, target.baseUrl, target.authMode, target.disabled === true, // Only credentials that carry their own endpoint (Devin tenants) extend the identity, so // every other provider's existing cache keys stay valid. @@ -550,6 +553,17 @@ export function explicitQuotaDestination(provider: string, config: OcxProviderCo if (config.disabled === true || config.authMode !== "oauth") return false; if (provider === "kimi") return isCanonicalKimiCodeBaseUrl(config.baseUrl); if (provider === "command-code") return isCanonicalCommandCodeBaseUrl(config.baseUrl); + if (provider === "mirasim") { + try { + const normalized = new URL(config.baseUrl); + return config.adapter === "mirasim" + && normalized.protocol === "https:" + && normalized.origin === "https://relay.mirasim.ai" + && normalized.pathname.replace(/\/+$/, "") === ""; + } catch { + return false; + } + } // These readers use fixed canonical billing origins, never config.baseUrl. Devin reads // the credential's own allowlisted api-server host instead (fetchDevinQuota revalidates it). return provider === "xai" || provider === "cursor" || provider === "devin"; diff --git a/src/providers/registry/entries-extended.ts b/src/providers/registry/entries-extended.ts index 2be40c761df..07e777089f6 100644 --- a/src/providers/registry/entries-extended.ts +++ b/src/providers/registry/entries-extended.ts @@ -9,6 +9,13 @@ import { MOONSHOT_INTL_BASE_URL, } from "../base-url-choices"; import { COMMAND_CODE_MODEL_REASONING_EFFORTS } from "../command-code-efforts"; +import { + MIRASIM_MODEL_CONTEXT_WINDOWS, + MIRASIM_MODEL_DISPLAY_NAMES, + MIRASIM_MODEL_MAX_OUTPUT_TOKENS, + MIRASIM_REASONING_EFFORTS, + MIRASIM_SELECTABLE_MODELS, +} from "../mirasim-models"; import { CODEBUDDY_CN_MODELS, CODEBUDDY_CN_MODEL_CONTEXT_WINDOWS, @@ -137,6 +144,32 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [ preserveCustomDestination: true, note: "TypeSafe JEV decision service for the optional JEV Combo strategy. This credential-only preset does not publish a directly routable model.", }, + { + id: "mirasim", + label: "Mirasim", + adapter: "mirasim", + baseUrl: "https://relay.mirasim.ai", + authKind: "oauth", + oauthId: "mirasim", + dashboardPreset: true, + defaultModel: "gpt-5.6-sol", + models: [...MIRASIM_SELECTABLE_MODELS], + liveModels: true, + modelDiscovery: { + path: "/v1/models", + }, + modelContextWindows: { ...MIRASIM_MODEL_CONTEXT_WINDOWS }, + modelDisplayNames: { ...MIRASIM_MODEL_DISPLAY_NAMES }, + modelMaxOutputTokens: { ...MIRASIM_MODEL_MAX_OUTPUT_TOKENS }, + defaultMaxOutputTokens: 128_000, + reasoningEfforts: [...MIRASIM_REASONING_EFFORTS], + modelReasoningEfforts: Object.fromEntries( + MIRASIM_SELECTABLE_MODELS.map(model => [model, [...MIRASIM_REASONING_EFFORTS]]), + ), + // The official client maps its workflow-only "ultra" rung to max on the single inference + // request; the surrounding multi-turn workflow is client orchestration, not a relay effort. + reasoningEffortMap: { ultra: "max" }, + }, { id: "baseten", label: "Baseten Model APIs", diff --git a/src/server/claude-messages.ts b/src/server/claude-messages.ts index e9787d79b82..de9f25f443b 100644 --- a/src/server/claude-messages.ts +++ b/src/server/claude-messages.ts @@ -7,6 +7,8 @@ * unchanged. The Responses output (SSE or JSON) is converted back to Anthropic shape. */ import { FORWARD_HEADERS } from "../adapters/openai-responses"; +import { fetchMirasim } from "../adapters/mirasim/transport"; +import { mirasimAnthropicBetaValue } from "../adapters/mirasim/anthropic"; import { admissionModelDeniedResponse, AdmissionModelDeniedError, @@ -51,6 +53,7 @@ import { responsesSseToAnthropicSse, } from "../claude/outbound"; import { clearableDeadline, idleDeadline } from "../lib/abort"; +import { readBoundedResponseBytes } from "../lib/bounded-body"; import { estimateTokens } from "../lib/token-estimate"; import { CLAUDE_NATIVE_THINKING, @@ -92,6 +95,12 @@ import { markProtocolBlocked, markProtocolEntry } from "../protocols/trace"; import { recordProtocolShadowPlan } from "../protocols/shadow-plan"; import { captureAnthropicClientIdentity } from "../adapters/anthropic/client-identity"; import { nativeMessagesDeclineReason, type NativeMessagesSelector } from "./messages-native-eligibility"; +import { providerFetch } from "./responses/fetch-helpers"; +import { + forceRefreshOAuthAccessSnapshot, + getValidAccessTokenSnapshot, + publicOAuthAuthenticationErrorMessage, +} from "../oauth"; import { isApiAuthRequired, isDataPlaneAdmissionSecret, @@ -1685,11 +1694,126 @@ function thinkingProjectionForPreview(config: OcxConfig, modelId: string): Claud } } +const MIRASIM_COUNT_TOKENS_MAX_BYTES = 1024 * 1024; + +async function handleMirasimClaudeCountTokens( + req: Request, + config: OcxConfig, + raw: Rec, + routedModel: string, + longContext: boolean, + admission?: DataPlaneAdmission, +): Promise { + let route: ReturnType; + try { + route = routeModel(config, routedModel); + } catch { + return undefined; + } + if (route.provider.adapter !== "mirasim") return undefined; + if (!route.modelId.trim().toLowerCase().startsWith("claude-")) { + return anthropicErrorResponse( + 400, + "Mirasim /v1/messages/count_tokens requires a Claude Messages model", + "invalid_request_error", + ); + } + try { + assertRouteAllowedByScope( + resolveAdmissionModelScope(config, admission), + routedModel, + route, + ); + } catch (error) { + if (error instanceof AdmissionModelDeniedError) return admissionModelDeniedResponse(error); + throw error; + } + + let snapshot; + try { + snapshot = await getValidAccessTokenSnapshot(route.providerName); + } catch (error) { + return anthropicErrorResponse( + 401, + publicOAuthAuthenticationErrorMessage(error), + "authentication_error", + ); + } + + const beta = mirasimAnthropicBetaValue( + [req.headers.get("anthropic-beta")], + longContext, + ); + const headers: Record = { + "content-type": "application/json", + "accept": "application/json", + "anthropic-version": req.headers.get("anthropic-version")?.trim() || "2023-06-01", + ...(beta ? { "anthropic-beta": beta } : {}), + }; + const url = `${route.provider.baseUrl.replace(/\/+$/, "")}/v1/messages/count_tokens`; + const body = JSON.stringify({ ...raw, model: route.modelId }); + const executor = providerFetch(route.provider, undefined, { + providerName: route.providerName, + modelId: route.modelId, + }); + const outbound = { url, method: "POST", headers, body } as const; + let upstream: Response | undefined; + try { + upstream = await fetchMirasim(outbound, snapshot.accessToken, { + abortSignal: req.signal, + executor, + }); + if (upstream.status === 401) { + try { + const refreshed = await forceRefreshOAuthAccessSnapshot(snapshot); + try { await upstream.body?.cancel(); } catch { /* already closed */ } + upstream = await fetchMirasim(outbound, refreshed.accessToken, { + abortSignal: req.signal, + executor, + }); + } catch { + // Preserve the relay's authenticated rejection below. + } + } + const observed = await readBoundedResponseBytes(upstream, { + maxBytes: MIRASIM_COUNT_TOKENS_MAX_BYTES, + signal: req.signal, + }); + if (observed.oversized) { + return anthropicErrorResponse(502, "Mirasim count_tokens response exceeded the safe size limit", "api_error"); + } + const responseHeaders = new Headers({ + "content-type": upstream.headers.get("content-type") ?? "application/json", + }); + return new Response(observed.bytes, { + status: upstream.status, + headers: responseHeaders, + }); + } catch (error) { + if (req.signal.aborted) { + return anthropicErrorResponse(499, "request canceled by client", "api_error"); + } + return anthropicErrorResponse( + 502, + error instanceof Error && error.name === "TimeoutError" + ? "Mirasim count_tokens request timed out" + : "Mirasim count_tokens relay failed", + "api_error", + ); + } finally { + const pending = upstream?.body; + if (pending && !pending.locked) { + try { void pending.cancel().catch(() => undefined); } catch { /* already closed */ } + } + } +} + export async function handleClaudeCountTokens( req: Request, config: OcxConfig, requestPolicy: RequestPolicyView = config, ingress: ClaudeIngressOptions = {}, + admission?: DataPlaneAdmission, ): Promise { const disabled = claudeInboundDisabled(config); if (disabled) return disabled; @@ -1713,6 +1837,7 @@ export async function handleClaudeCountTokens( } try { let model = raw.model; + const longContext = /\[1m\]/i.test(model); // Case-insensitive [1m] strip (audit 021 #7 — the CLI matches /\[1m\]/i). const stripped = stripOneMillionMarker(model); if (stripped !== model) { @@ -1737,7 +1862,17 @@ export async function handleClaudeCountTokens( model = countFastRow.baseId; raw.model = model; } - captureClaudeInbound("count_tokens", raw, resolveInboundModel(model, cc), req.headers.get("anthropic-beta") ?? undefined); + const routedModel = resolveInboundModel(model, cc); + captureClaudeInbound("count_tokens", raw, routedModel, req.headers.get("anthropic-beta") ?? undefined); + const mirasim = await handleMirasimClaudeCountTokens( + req, + config, + raw, + routedModel, + longContext, + admission, + ); + if (mirasim) return mirasim; if (wantsNativePassthrough(req, config, requestPolicy, model, cc)) { return await anthropicNativePassthrough(req, config, { model, provider: "anthropic-native", surface: "claude" }, undefined, raw, "/v1/messages/count_tokens"); } diff --git a/src/server/index/serve-options.ts b/src/server/index/serve-options.ts index f8a2f32ed99..ec676e83552 100644 --- a/src/server/index/serve-options.ts +++ b/src/server/index/serve-options.ts @@ -671,6 +671,12 @@ export function createServeOptions(ctx: ServeOptionsContext) { return new Response(resp.body, { status: 503, headers }); } + if (url.pathname === "/oauth/mirasim/start" || url.pathname.startsWith("/oauth/mirasim/callback/")) { + const { handleMirasimBrowserOAuthRequest } = await import("../../oauth/mirasim"); + const oauthResponse = await handleMirasimBrowserOAuthRequest(req, url); + if (oauthResponse) return oauthResponse; + } + if (url.pathname.startsWith("/api/")) { const localManagementAuth = { attestationSecret: localAttestationSecret, @@ -1534,7 +1540,13 @@ export function createServeOptions(ctx: ServeOptionsContext) { return withCors(anthropicErrorResponse(403, "cross-origin data-plane request blocked", "permission_error"), req, policy); } return runAdmittedHttpTurn(req, policy, async () => withCors( - await handleClaudeCountTokens(req, config, policy, { claudeIntercept: ingress === "claude-intercept" }), + await handleClaudeCountTokens( + req, + config, + policy, + { claudeIntercept: ingress === "claude-intercept" }, + admission, + ), req, policy, )); diff --git a/src/server/management/oauth-account-routes.ts b/src/server/management/oauth-account-routes.ts index 50c2cabb3c2..f8ee59dfff2 100644 --- a/src/server/management/oauth-account-routes.ts +++ b/src/server/management/oauth-account-routes.ts @@ -253,7 +253,15 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise< // the provider's loopback callback server (inside this process) captures the redirect in the // background, then the credential is persisted. The GUI opens the URL and polls /api/oauth/status. if (url.pathname === "/api/oauth/login" && req.method === "POST") { - const body = await readManagementJsonBodyOr(req, {}) as { provider?: string; addAccount?: boolean; accountId?: string; reauth?: boolean; openBrowser?: unknown; method?: unknown }; + const body = await readManagementJsonBodyOr(req, {}) as { + provider?: string; + addAccount?: boolean; + accountId?: string; + reauth?: boolean; + openBrowser?: unknown; + method?: unknown; + locale?: unknown; + }; const provider = (body.provider ?? "").trim().toLowerCase(); if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400); // Muse may import a local Keychain credential or start a device grant; add-account @@ -295,6 +303,14 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise< const { url: authUrl, instructions, deviceCode } = await startLoginFlow(provider, { forceLogin: body.addAccount === true || reauth, ...(accountId ? { reauthAccountId: accountId } : {}), + ...(provider === "mirasim" + ? { + mirasimBrowserBaseUrl: url.origin, + ...(typeof body.locale === "string" && body.locale.length <= 32 + ? { mirasimBrowserLocale: body.locale } + : {}), + } + : {}), }, { // startLoginFlow returns the authorization URL before background persistence completes. // Three-way reconcile settled disk changes so a failed login cannot leave a provider diff --git a/src/server/management/provider-routes.ts b/src/server/management/provider-routes.ts index ee1733d2b2b..e36935b2224 100644 --- a/src/server/management/provider-routes.ts +++ b/src/server/management/provider-routes.ts @@ -1712,6 +1712,27 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise = compactProvider.adapter === "mirasim" + ? { ...(raw as Record) } + : withoutReasoning; // The regular /v1/responses path applies sanitizeReasoningInputContent via the adapter's // buildRequest, but the compact endpoint forwards directly. Apply the same sanitizer here // so routed-model reasoning items (reasoning_text content) don't 400 the ChatGPT backend. // #5095: the compact endpoint forwards `raw` directly, so it needs the same legacy dotted // call-name repair the adapter applies. A damaged item here refuses the compaction itself, // which is the request a long task depends on to keep going. - const compactBody = repairLegacyDottedToolCallNames( + const sanitizedCompactBody = repairLegacyDottedToolCallNames( sanitizeReasoningInputContent(compactBodyRaw), ) as typeof compactBodyRaw; + const compactBody = compactProvider.adapter === "mirasim" + ? normalizeMirasimCompactBody(sanitizedCompactBody, route.modelId) + : sanitizedCompactBody; { const binding = conversationStateBindingFromAuth(authCtx, codexPoolAffinityKey(req.headers)); if (binding) { @@ -944,7 +963,9 @@ export async function handleResponsesCompact( }); } } - const compactUrl = `${base}/responses/compact`; + const compactUrl = compactProvider.adapter === "mirasim" + ? `${base}/v1/responses/compact` + : `${base}/responses/compact`; const compactTargetKey = `${route.providerName}|${route.modelId}|compact`; const actualCompactHostKey = upstreamHostHealthKey( route.providerName, @@ -1057,6 +1078,29 @@ export async function handleResponsesCompact( recovery: "normal" | "single", sendAuthCtx: CodexAuthContext, ): Promise => { + if (sendProvider.adapter === "mirasim") { + const accessToken = sendProvider.apiKey; + if (!accessToken) { + return Promise.reject(new Error("Mirasim compact access token is unavailable")); + } + return fetchMirasim({ + url: compactUrl, + method: "POST", + headers: Object.fromEntries(sendHeaders.entries()), + body: JSON.stringify(compactBody), + }, accessToken, { + abortSignal: req.signal, + timeoutMs: connectMs, + sendBudget, + executor: providerFetch(sendProvider, undefined, { + providerName: route.providerName, + modelId: route.modelId, + }), + }).then(res => { + settleObservedCompactHostResponse(); + return res; + }); + } const doFetch = (upstreamRecovery?: UpstreamSendRecovery) => fetchWithHeaderTimeout( compactUrl, applyUpstreamRecoveryInit({ diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 3ac69ba454f..481d4e73ad5 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -28,6 +28,10 @@ import { deliverAdapterResponse } from "./adapter-delivery"; import { releaseUpstreamHostAdmission } from "../../codex/upstream-host-health"; import { releaseCodexAuthContextProbeLease } from "../../codex/auth-context"; import { runWithCompactionRecovery } from "./compaction-recovery"; +import { adapterIsPassthrough } from "../../adapters/base"; + +/** Public Responses entry and compatibility exports. Implementations live with their owners. */ + /** * Route one `/v1/responses` request through the adapter pipeline: recovery loop, passthrough * wire, image/web-search bridges, and the terminal-guard continuation. @@ -121,7 +125,7 @@ async function handleResponsesInner( ); const sendBudgetState = createResponsesSendBudget(requestContext); if (sendBudgetState instanceof Response) return sendBudgetState; - if ("passthrough" in transportState.adapter && transportState.adapter.passthrough && !sidecarState.routedCompaction) { + if (adapterIsPassthrough(transportState.adapter, requestState.parsed) && !sidecarState.routedCompaction) { const passthroughResult = await executePassthroughResponse( requestContext, admissionState, diff --git a/src/server/responses/passthrough-delivery.ts b/src/server/responses/passthrough-delivery.ts index 184e2b1eabb..a3e374b1e7a 100644 --- a/src/server/responses/passthrough-delivery.ts +++ b/src/server/responses/passthrough-delivery.ts @@ -1,5 +1,6 @@ import { isNativeControlResponse } from "./native-response-control"; import { createOutboundCredentialMask, createTerminalErrorRedactionBlockRewrite } from "./terminal-error-redaction"; +import { Buffer } from "node:buffer"; import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options"; import type { PreparedResponsesRequest } from "./request-prepare"; import type { ResponsesTransport } from "./request-transport"; @@ -154,6 +155,102 @@ import { type BufferedResponsesSseFailure, type BufferedResponsesSseResult, } from "./buffered-sse-json"; +import { decodeServerSentEvents } from "../../lib/sse-decoder"; + +function requestBodyForcesResponsesStream(bodyText: string): boolean { + try { + const body = JSON.parse(bodyText) as { stream?: unknown }; + return body?.stream === true; + } catch { + return false; + } +} + +async function collectForcedResponsesStream( + response: Response, + translatorBudget: PreparedResponsesRequest["translatorBudget"], + signal: AbortSignal, +): Promise { + if (!response.body) throw new Error("upstream streaming response has no body"); + const indexedItems = new Map; bytes: number }>(); + const fallbackItems: Array<{ item: Record; bytes: number }> = []; + let retainedBytes = 0; + const retain = (bytes: number): void => { + if (bytes <= 0) return; + translatorBudget.chargeRetained(bytes, { kind: "retained_collectors" }); + retainedBytes += bytes; + }; + const release = (bytes: number): void => { + if (bytes <= 0) return; + translatorBudget.releaseRetained(bytes, { kind: "retained_collectors" }); + retainedBytes -= bytes; + }; + try { + for await (const event of decodeServerSentEvents(response.body, { signal, translatorBudget })) { + let payload: unknown; + try { payload = JSON.parse(event.data); } catch { continue; } + if (!payload || typeof payload !== "object" || Array.isArray(payload)) continue; + const record = payload as { + type?: unknown; + output_index?: unknown; + item?: unknown; + response?: unknown; + }; + const type = String(record.type ?? ""); + if (type === "response.output_item.done") { + if (!record.item || typeof record.item !== "object" || Array.isArray(record.item)) continue; + // Match the reference client's non-stream collector: retain authoritative completed + // output items and patch them into a terminal snapshot whose output[] is empty. + const item = record.item as Record; + const bytes = Buffer.byteLength(JSON.stringify(item), "utf8"); + const index = typeof record.output_index === "number" + && Number.isSafeInteger(record.output_index) + && record.output_index >= 0 + ? record.output_index + : undefined; + retain(bytes); + if (index === undefined) { + fallbackItems.push({ item, bytes }); + } else { + const previous = indexedItems.get(index); + if (previous) release(previous.bytes); + indexedItems.set(index, { item, bytes }); + } + continue; + } + if (type === "error" || type === "response.failed") { + throw new Error("upstream streaming response failed before completion"); + } + if (type !== "response.completed" && type !== "response.incomplete") continue; + if (!record.response || typeof record.response !== "object" || Array.isArray(record.response)) { + throw new Error("upstream streaming terminal omitted its response object"); + } + const terminal = record.response as Record; + if ((!Array.isArray(terminal.output) || terminal.output.length === 0) + && (indexedItems.size > 0 || fallbackItems.length > 0)) { + terminal.output = [ + ...[...indexedItems.entries()] + .sort(([left], [right]) => left - right) + .map(([, entry]) => entry.item), + ...fallbackItems.map(entry => entry.item), + ]; + } + const headers = new Headers(response.headers); + headers.set("content-type", "application/json"); + headers.set("cache-control", "no-store"); + return new Response(JSON.stringify(terminal), { + status: response.status, + statusText: response.statusText, + headers, + }); + } + throw new Error("upstream streaming response ended before a terminal response"); + } finally { + if (retainedBytes > 0) { + translatorBudget.releaseRetained(retainedBytes, { kind: "retained_collectors" }); + } + } +} const PLAINTEXT_V2_SSE_PREFIX_LIMIT = 4096; @@ -458,7 +555,7 @@ export async function deliverPassthroughResponse( }); } - const headers = sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions); + let headers = sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions); if (!upstreamResponse.ok) noteUpstreamRequestId(logCtx, headers); const resolvedModel = headers.get("openai-model")?.trim(); if (resolvedModel) { @@ -468,12 +565,36 @@ export async function deliverPassthroughResponse( // ChatGPT may omit Content-Type on SSE responses. Plaintext V2 responses // reach this fallback only after their first Responses event is confirmed. const passthroughCt = headers.get("content-type")?.toLowerCase(); - const isEventStream = passthroughCt?.includes("text/event-stream") + let isEventStream = passthroughCt?.includes("text/event-stream") || (responseEffects.plaintextV2AgentMessageToolNames.size === 0 && upstreamResponse.ok && !!upstreamResponse.body && !passthroughCt && (parsed.stream || canonicalBufferedJson)); + if ( + route.provider.adapter === "mirasim" + && clientRequestedStream === false + && isEventStream + && upstreamResponse.ok + && requestBodyForcesResponsesStream(nativeExchange.request.body) + ) { + try { + upstreamResponse = await collectForcedResponsesStream( + upstreamResponse, + translatorBudget, + upstream.signal, + ); + headers = sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions); + isEventStream = false; + } catch { + upstream.abort(); + return formatErrorResponse( + 502, + "upstream_error", + "upstream streaming response ended before a bounded non-streaming response could be collected", + ); + } + } const recordTerminalOutcome = codexForwardTerminalOutcomeRecorder( config, admissionState.authCtx, diff --git a/src/server/responses/passthrough-dispatch.ts b/src/server/responses/passthrough-dispatch.ts index f17b98dfbf6..a8a1ab77a2f 100644 --- a/src/server/responses/passthrough-dispatch.ts +++ b/src/server/responses/passthrough-dispatch.ts @@ -57,7 +57,7 @@ import { import type { RoutedNamespaceToolAliases } from "../../responses/namespace-tool-compat"; import { hasResponsesSnapshotRepair, repairResponsesSnapshotJson } from "../responses-snapshot-repair"; import { backfillResponsesFieldsJson } from "./responses-field-backfill"; -import type { AdapterRequest } from "../../adapters/base"; +import { adapterIsPassthrough, type AdapterRequest } from "../../adapters/base"; import { isXaiResponsesDestination, resolveProviderTransport } from "../../providers/xai-transport"; import { CODE_MODE_EXEC_TOOL_NAME } from "../../types"; import type { ResponsesTerminalStatus } from "../../bridge"; @@ -166,6 +166,7 @@ import { upstreamErrorMessageFromPayload, ENCRYPTED_FUNCTION_OUTPUT_REJECTION } import { isTransientConsoleGoUploadRejection } from "../../providers/opencode-zen-rate-limit"; import { planReasoningEffortDowngrade } from "../../providers/reasoning-metadata"; import { unboundPoolSpendRefusalResponse } from "../workflow-refusal"; +import { waitForProviderRequestSlot } from "../../providers/request-pacing"; /** Transfer an uncommitted Copilot exchange to the existing translated pipeline without new admission. */ export interface PassthroughAdapterHandoff { kind: "adapter-handoff" } @@ -240,6 +241,9 @@ export async function preparePassthroughExchange( | "initialSendAllowance" | "noteInitialDispatch" | "adapterSendBudget" + | "adapterDispatchBudget" + | "noteAdapterPhysicalSend" + | "noteAdapterRecoveryWithheld" >, ) { const { config, logCtx, options, req } = requestContext; @@ -275,6 +279,9 @@ export async function preparePassthroughExchange( claimAmbiguousResend, reserveCredentialHop, workflowRootId, + adapterDispatchBudget, + noteAdapterPhysicalSend, + noteAdapterRecoveryWithheld, } = sendBudgetState; const codexSafetyBufferingOptions = isCanonicalOpenAiForwardProvider(route.provider) @@ -1006,49 +1013,85 @@ export async function preparePassthroughExchange( // policy, so `spendEnforced` cannot change before dispatch even though no reporter starts it here. const receiptMode = !sendBudgetState.adapterSendBudget?.spendEnforced && Boolean(options.comboInitialSend); try { - // Transient-5xx pre-stream retry (devlog/_plan/260716_claudecode_hardening/010): - // the ChatGPT backend emits transient 502/520s that an immediate retry absorbs. - // Body is a replayable string; nothing has streamed to the client yet. - upstreamResponse = await fetchWithTransientRetry( - recovery => { - // The pool-wide recovery window measures recovery traffic against observed demand, - // and this is where demand is observed: `recovery === undefined` is a new request's - // first send, everything after it is the same request trying again. Without this the - // ratio has no denominator and the window collapses to its quiet-pool floor, which - // would throttle recovery on a busy proxy exactly as hard as on an idle one (#4701). - if (recovery === undefined) classifyPoolRecoveryDispatch("initial"); - transportState.noteRoutedAttemptSend(passthroughEstimate, recovery); - return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({ - method: request.method, - headers: request.headers, - body: request.body, - }, recovery), upstream.signal, connectMs, upstreamRequestsStream, - providerFetch(route.provider, options.codexWsRuntimeIdentity, { - httpOnly: canonicalBufferedJson, - nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt - && responseEffects.plaintextV2AgentMessageToolNames.size === 0 - ? options.nativeControl : undefined, - dispatchOverride: oauthDispatch(request, parsed, "passthrough"), - providerName: route.providerName, - modelId: route.modelId, - onPhysicalDispatch: receiptMode ? sendBudgetState.noteInitialDispatch : undefined, - onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId), - beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider) - ? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined, - }), - route.provider.authMode === "forward") - // Every real attempt response — including an intermediate 5xx the - // retry wrapper replaces — proves the host was reached (#914 review). - .then(adoptObservedResponse); - }, - { abortSignal: upstream.signal, label: safeHostLabel(request.url), - attempts: sendBudgetState.initialSendAllowance(transientSendAttempts()), - // Reporter path: direct requests and enforced spend. Receipt-mode sends are settled - // by `onPhysicalDispatch`; a reporter as well would count them twice. - ...(receiptMode ? {} : { onSendsConsumed: transientSendReporter() }), - claimAmbiguousResend: claimPreHeaderResend, - }, - ); + if (transportState.adapter.fetchResponse) { + // A mixed-wire adapter may opt into native Responses delivery while still owning the + // physical HTTP transport (Mirasim signs, seals and mints a device ticket here). Native + // passthrough must not bypass that transport just because response bytes stay native. + const reportsOwnSends = transportState.adapter.reportsPhysicalSends === true; + if (!reportsOwnSends) transportState.noteRoutedAttemptSend(passthroughEstimate); + await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, upstream.signal); + const executor = providerFetch(route.provider, options.codexWsRuntimeIdentity, { + pacingSlotAcquired: true, + httpOnly: canonicalBufferedJson, + nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt + && responseEffects.plaintextV2AgentMessageToolNames.size === 0 + ? options.nativeControl : undefined, + dispatchOverride: oauthDispatch(request, parsed, "passthrough"), + providerName: route.providerName, + modelId: route.modelId, + onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId), + beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider) + ? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined, + }); + upstreamResponse = await transportState.adapter.fetchResponse(request, { + abortSignal: upstream.signal, + timeoutMs: connectMs, + returnRawErrors: true, + stream: parsed.stream, + executor, + ...(adapterDispatchBudget ? { sendBudget: adapterDispatchBudget } : {}), + onPhysicalSend: send => noteAdapterPhysicalSend( + passthroughEstimate, + send, + { includeFirst: reportsOwnSends }, + ), + onRecoveryWithheld: noteAdapterRecoveryWithheld, + }).then(adoptObservedResponse); + } else { + // Transient-5xx pre-stream retry (devlog/_plan/260716_claudecode_hardening/010): + // the ChatGPT backend emits transient 502/520s that an immediate retry absorbs. + // Body is a replayable string; nothing has streamed to the client yet. + upstreamResponse = await fetchWithTransientRetry( + recovery => { + // The pool-wide recovery window measures recovery traffic against observed demand, + // and this is where demand is observed: `recovery === undefined` is a new request's + // first send, everything after it is the same request trying again. Without this the + // ratio has no denominator and the window collapses to its quiet-pool floor, which + // would throttle recovery on a busy proxy exactly as hard as on an idle one (#4701). + if (recovery === undefined) classifyPoolRecoveryDispatch("initial"); + transportState.noteRoutedAttemptSend(passthroughEstimate, recovery); + return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({ + method: request.method, + headers: request.headers, + body: request.body, + }, recovery), upstream.signal, connectMs, upstreamRequestsStream, + providerFetch(route.provider, options.codexWsRuntimeIdentity, { + httpOnly: canonicalBufferedJson, + nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt + && responseEffects.plaintextV2AgentMessageToolNames.size === 0 + ? options.nativeControl : undefined, + dispatchOverride: oauthDispatch(request, parsed, "passthrough"), + providerName: route.providerName, + modelId: route.modelId, + onPhysicalDispatch: receiptMode ? sendBudgetState.noteInitialDispatch : undefined, + onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId), + beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider) + ? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined, + }), + route.provider.authMode === "forward") + // Every real attempt response — including an intermediate 5xx the + // retry wrapper replaces — proves the host was reached (#914 review). + .then(adoptObservedResponse); + }, + { abortSignal: upstream.signal, label: safeHostLabel(request.url), + attempts: sendBudgetState.initialSendAllowance(transientSendAttempts()), + // Reporter path: direct requests and enforced spend. Receipt-mode sends are settled + // by `onPhysicalDispatch`; a reporter as well would count them twice. + ...(receiptMode ? {} : { onSendsConsumed: transientSendReporter() }), + claimAmbiguousResend: claimPreHeaderResend, + }, + ); + } } catch (err) { return transportFailureResponse(err); } finally { @@ -1098,7 +1141,7 @@ export async function preparePassthroughExchange( resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy), config.cacheRetention, ); - if (!("passthrough" in retryAdapter) || !retryAdapter.passthrough) { + if (!adapterIsPassthrough(retryAdapter, parsed)) { upstream.abort(); return { failed: formatErrorResponse(502, "upstream_error", "Recovery changed the provider wire unexpectedly") }; } @@ -1227,7 +1270,7 @@ export async function preparePassthroughExchange( resolveWireProtocolOverride(route.providerName, route.modelId, replay.provider, inboundWire, route.staticPolicy), config.cacheRetention, ); - if (!("passthrough" in replayAdapter) || !replayAdapter.passthrough) { + if (!adapterIsPassthrough(replayAdapter, parsed)) { upstream.abort(); return formatErrorResponse(502, "upstream_error", "Native main refresh changed the provider wire unexpectedly"); } @@ -1376,7 +1419,7 @@ export async function preparePassthroughExchange( config.cacheRetention, ); bindRefreshedAdapter(refreshedAdapter); - if (!("passthrough" in refreshedAdapter) || !refreshedAdapter.passthrough) { + if (!adapterIsPassthrough(refreshedAdapter, parsed)) { if (route.providerName === "github-copilot" && refreshedAdapter.name === "openai-chat") { return handoffToChat(refreshedAdapter); } diff --git a/src/server/responses/request-transport.ts b/src/server/responses/request-transport.ts index 9ab1a23d14a..6f75564f495 100644 --- a/src/server/responses/request-transport.ts +++ b/src/server/responses/request-transport.ts @@ -18,7 +18,7 @@ import { getAccountSet, } from "../../oauth/store"; import { refreshKiroAccountModelsDetached } from "../../providers/kiro-model-catalog"; -import type { ProviderAdapter, AdapterRequest } from "../../adapters/base"; +import { adapterIsPassthrough, type ProviderAdapter, type AdapterRequest } from "../../adapters/base"; import { releaseProviderRequestSlot, waitForProviderRequestSlot, type ProviderRequestSlot } from "../../providers/request-pacing"; import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig, OcxUsage } from "../../types"; import type { AnthropicAccountSelectionReason } from "../../oauth/anthropic-routing"; @@ -1059,7 +1059,7 @@ export async function prepareResponsesTransport( ); if (passiveSubjectId) logCtx.activeAttempt.labRouteSubjectId = passiveSubjectId; } - const isPassthrough = "passthrough" in adapter && !!adapter.passthrough; + const isPassthrough = adapterIsPassthrough(adapter, parsed); const rawInput = (parsed._rawBody as { input?: unknown }).input; if (!isPassthrough && Array.isArray(rawInput) && rawInput.some( diff --git a/src/server/responses/sidecar-execution.ts b/src/server/responses/sidecar-execution.ts index 4b36aa8e978..56ebabca2a8 100644 --- a/src/server/responses/sidecar-execution.ts +++ b/src/server/responses/sidecar-execution.ts @@ -17,7 +17,7 @@ import { runWithImageBridge, clampImageMaxRounds, } from "../../images"; -import type { ProviderAdapter } from "../../adapters/base"; +import { adapterIsPassthrough, type ProviderAdapter } from "../../adapters/base"; import type { OcxParsedRequest } from "../../types"; import { rotateProviderTransportOn429, rateLimitRetryPolicyFor } from "../../providers/key-failover"; import { @@ -151,7 +151,7 @@ export async function executeResponsesSidecars( // // Keyed on the adapter, not on position: routedCompaction skips the passthrough branch above // yet still builds from _rawBody (see the :3703 comment). - if (!("passthrough" in transportState.adapter && transportState.adapter.passthrough)) { + if (!adapterIsPassthrough(transportState.adapter, parsed)) { const unpaired = parsed.context.messages.find( message => message.role === "toolResult" && (typeof (message as { toolCallId?: unknown }).toolCallId !== "string" diff --git a/src/server/search.ts b/src/server/search.ts index 7dd14464839..171428c1608 100644 --- a/src/server/search.ts +++ b/src/server/search.ts @@ -11,6 +11,7 @@ * paid backend than the one the operator named. */ import { formatErrorResponse } from "../bridge"; +import { fetchMirasim } from "../adapters/mirasim/transport"; import { openAiSidecarCreditRefusal } from "../providers/openai-sidecar-credit"; import { CodexAccountCooldownError, @@ -40,6 +41,11 @@ import { type ExactOpenAiSidecarAccount, } from "../providers/openai-sidecar"; import { previewRouteModel, routeModel } from "../router"; +import { + forceRefreshOAuthAccessSnapshot, + getValidAccessTokenSnapshot, + publicOAuthAuthenticationErrorMessage, +} from "../oauth"; import { handleAlphaSearchSidecarFallback, handleDevinAlphaSearch } from "../web-search/alpha-search"; import { readJsonRequestBody, resolveInboundBodyLimitBytes } from "./request-decompress"; import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "./auth-cors"; @@ -48,6 +54,7 @@ import { codexLogAccountId, decodeRequestErrorResponse } from "./responses"; import type { AdmissionLease } from "../lib/admission"; import { codexAccountSelectionForTurn } from "./lifecycle"; import { codexModelAvailabilityErrorResponse } from "./responses/codex-auth-error"; +import { providerFetch } from "./responses/fetch-helpers"; /** * Default TOTAL deadline for one search relay. alpha/search is non-streaming JSON — response @@ -59,6 +66,124 @@ import { codexModelAvailabilityErrorResponse } from "./responses/codex-auth-erro const SEARCH_UPSTREAM_TIMEOUT_MS = 200_000; export const SEARCH_RESPONSE_MAX_BYTES = 16 * 1024 * 1024; +async function handleMirasimSearch( + req: Request, + config: OcxConfig, + body: unknown, + model: string, + logCtx: RequestLogContext, + admission?: DataPlaneAdmission, +): Promise { + let route: ReturnType; + try { + route = routeModel(config, model); + } catch { + // Preserve the existing ChatGPT-forward / sidecar behavior for a model the normal router + // does not recognize. Mirasim takes this branch only after routing proves its ownership. + return undefined; + } + if (route.provider.adapter !== "mirasim") return undefined; + + const denial = admissionScopeDenial(config, admission, model, route); + if (denial) return denial; + if (!route.modelId.trim().toLowerCase().startsWith("gpt-")) { + return formatErrorResponse( + 400, + "invalid_request_error", + "Mirasim /v1/alpha/search requires a GPT Responses model", + ); + } + if (!body || typeof body !== "object" || Array.isArray(body)) { + return formatErrorResponse(400, "invalid_request_error", "search request body must be a JSON object"); + } + + logCtx.provider = route.providerName; + logCtx.model = route.modelId; + logCtx.routeDecision = route.routeDecision; + + let snapshot; + try { + snapshot = await getValidAccessTokenSnapshot(route.providerName); + } catch (error) { + return formatErrorResponse( + 401, + "authentication_error", + publicOAuthAuthenticationErrorMessage(error), + ); + } + + const timeoutMs = config.search?.timeoutMs ?? SEARCH_UPSTREAM_TIMEOUT_MS; + const linkedSignal = signalWithTimeout(timeoutMs, req.signal); + const executor = providerFetch(route.provider, undefined, { + providerName: route.providerName, + modelId: route.modelId, + }); + const outbound = { + url: `${route.provider.baseUrl.replace(/\/+$/, "")}/v1/alpha/search`, + method: "POST", + headers: { + "content-type": "application/json", + "accept": "application/json", + }, + body: JSON.stringify({ ...(body as Record), model: route.modelId }), + } as const; + let upstreamResponse: Response | undefined; + try { + upstreamResponse = await fetchMirasim(outbound, snapshot.accessToken, { + abortSignal: linkedSignal.signal, + timeoutMs, + executor, + }); + if (upstreamResponse.status === 401) { + try { + const refreshed = await forceRefreshOAuthAccessSnapshot(snapshot); + try { await upstreamResponse.body?.cancel(); } catch { /* already closed */ } + upstreamResponse = await fetchMirasim(outbound, refreshed.accessToken, { + abortSignal: linkedSignal.signal, + timeoutMs, + executor, + }); + } catch { + // Return the relay's authenticated rejection below. The public response never reflects + // token/device material or the refresh error body. + } + } + + const observed = await readBoundedResponseBytes(upstreamResponse, { + maxBytes: SEARCH_RESPONSE_MAX_BYTES, + signal: linkedSignal.signal, + }); + if (observed.oversized) { + return formatErrorResponse( + 502, + "upstream_error", + `search response too large (exceeded ${SEARCH_RESPONSE_MAX_BYTES} bytes)`, + ); + } + const relayHeaders: Record = {}; + const contentType = upstreamResponse.headers.get("content-type"); + if (contentType) relayHeaders["content-type"] = contentType; + return new Response(observed.bytes, { + status: upstreamResponse.status, + headers: relayHeaders, + }); + } catch (error) { + if (req.signal.aborted) { + return formatErrorResponse(499, "client_closed_request", "search request canceled by client"); + } + if (linkedSignal.signal.aborted || (error instanceof Error && error.name === "TimeoutError")) { + return formatErrorResponse(504, "upstream_error", "search upstream timed out"); + } + return formatErrorResponse(502, "upstream_error", "search relay failed"); + } finally { + linkedSignal.cleanup(); + const pendingBody = upstreamResponse?.body; + if (pendingBody && !pendingBody.locked) { + try { void pendingBody.cancel().catch(() => undefined); } catch { /* already closed */ } + } + } +} + export async function handleSearch( req: Request, config: OcxConfig, @@ -136,6 +261,10 @@ export async function handleSearch( return formatErrorResponse(400, "invalid_request_error", "Luna Reserve compatibility is only available as a conversation model, not the standalone search relay. Choose another search model."); } + if (typeof model === "string" && model.trim()) { + const mirasim = await handleMirasimSearch(req, config, body, model, logCtx, admission); + if (mirasim) return mirasim; + } const candidates = listOpenAiForwardSidecarCandidates(config); if (candidates.length === 0) { return handleAlphaSearchSidecarFallback(body, config, req.signal, logCtx, admission); diff --git a/tests/adapters/adapter-registry-authority.test.ts b/tests/adapters/adapter-registry-authority.test.ts index 1e7f7679f93..8af84929c73 100644 --- a/tests/adapters/adapter-registry-authority.test.ts +++ b/tests/adapters/adapter-registry-authority.test.ts @@ -24,6 +24,7 @@ const EXPECTED_ADAPTER_NAMES = { devin: "devin", zed: "zed", "mimo-free": "mimo-free", + mirasim: "mirasim", qoder: "qoder", "claude-cli": "claude-cli", } as const; @@ -78,10 +79,12 @@ describe("adapter registry authority", () => { expect(getAdapterDefinition("azure")?.contractParent).toBe("openai-responses"); expect(getAdapterDefinition("azure-openai")?.contractParent).toBe("openai-responses"); expect(getAdapterDefinition("mimo-free")?.contractParent).toBe("openai-chat"); + expect(getAdapterDefinition("mirasim")?.contractParent).toBe("openai-responses"); expect(effectiveAdapterContract("azure").wire).toBe("openai-responses"); expect(effectiveAdapterContract("azure-openai").wire).toBe("openai-responses"); expect(effectiveAdapterContract("mimo-free").wire).toBe("openai-chat"); + expect(effectiveAdapterContract("mirasim").wire).toBe("openai-responses"); expect(effectiveAdapterContract("cursor").mutation).toBe("codex-owned-with-gated-native-fallback"); }); diff --git a/tests/mirasim-crypto.test.ts b/tests/mirasim-crypto.test.ts new file mode 100644 index 00000000000..728d2171058 --- /dev/null +++ b/tests/mirasim-crypto.test.ts @@ -0,0 +1,105 @@ +import { describe, expect, test } from "bun:test"; +import { createPrivateKey } from "node:crypto"; +import { + canonicalMirasimSignaturePayload, + sealMirasimRelayMetadata, + signMirasimRequest, +} from "../src/adapters/mirasim/crypto"; + +const ED25519_PKCS8_SEED_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex"); + +function ed25519PemFromSeed(seed: Uint8Array): string { + const key = createPrivateKey({ + key: Buffer.concat([ED25519_PKCS8_SEED_PREFIX, Buffer.from(seed)]), + type: "pkcs8", + format: "der", + }); + return key.export({ type: "pkcs8", format: "pem" }).toString(); +} + +describe("Mirasim protocol crypto", () => { + test("mrs-sig-v2 matches the Mirasim crypto-core golden vector", () => { + const seed = Uint8Array.from({ length: 32 }, (_, index) => index); + const metadata = { + "x-mirasim-session": "mirasim_00000000-0000-4000-8000-000000000000", + "x-mirasim-agent": "claude", + "x-mirasim-call": "11111111-2222-4333-8444-555555555555", + }; + const body = Buffer.from('{"model":"claude-sonnet-5","messages":[]}', "utf8"); + const base = { + method: "POST", + path: "/v1/messages", + timestamp: "1788200000123", + nonce: "AAECAwQFBgcICQoL", + deviceId: "device-fixed", + clientVersion: "0.0.260", + credential: "ticket-fixed", + metadata, + body, + }; + + const expectedCanonical = [ + "mrs-sig-v2", + "POST", + "/v1/messages", + "1788200000123", + "AAECAwQFBgcICQoL", + "device-fixed", + "0.0.260", + "66ee005427e4f3b74ce4830f104c989613f0968f97036191a6fbaea245040170", + "91bcb885e5b045a9f55f270bb0c6d633930407b6cca792839034702ed233be6b", + "9df27ddbfc24ebaafa990cd41a7744f56c875d0dadf69e4941edc7e728aea6bd", + ].join("\n"); + + expect(canonicalMirasimSignaturePayload(base)).toBe(expectedCanonical); + + const signed = signMirasimRequest({ + ...base, + privateKeyPem: ed25519PemFromSeed(seed), + }); + expect(signed.canonicalPayload).toBe(expectedCanonical); + expect(signed.signature).toBe( + "zUYTEKW17Gzn7TEdEzWZ2aEOpO4oW9YFFpdsyzJaUyS4A_byq3DUNYzNOL96D24MExQ0mVbot75TkvkJw3vVAQ", + ); + }); + + test("empty metadata contributes a blank canonical line", () => { + const payload = canonicalMirasimSignaturePayload({ + method: "GET", + path: "/v1/models", + timestamp: "1", + nonce: "nonce", + deviceId: "device", + clientVersion: "0.0.260", + credential: "ticket", + body: new Uint8Array(), + }); + const lines = payload.split("\n"); + expect(lines).toHaveLength(10); + expect(lines[8]).toBe(""); + }); + + test("mrs-seal-v1 matches the Mirasim crypto-core golden vector", () => { + const plaintext = { + "x-mirasim-agent": "claude", + "x-mirasim-call": "11111111-2222-4333-8444-555555555555", + "x-mirasim-device": "device-fixed", + "x-mirasim-nonce": "AAECAwQFBgcICQoL", + "x-mirasim-session": "mirasim_00000000-0000-4000-8000-000000000000", + "x-mirasim-sig": "zUYTEKW17Gzn7TEdEzWZ2aEOpO4oW9YFFpdsyzJaUyS4A_byq3DUNYzNOL96D24MExQ0mVbot75TkvkJw3vVAQ", + "x-mirasim-ts": "1788200000123", + }; + const sealed = sealMirasimRelayMetadata(plaintext, "POST", "/v1/messages", { + recipientPublicKeyBase64: "NYBy1jZYgNGu6jKa35EhODhR7SGijjt16WXQ0s0WYlQ=", + ephemeralSecret: Buffer.from( + "404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f", + "hex", + ), + nonce: Buffer.from("a0a1a2a3a4a5a6a7a8a9aaab", "hex"), + }); + + expect(sealed).toBe( + "eaYx7t4b-cmPEgMs3q3Q56B5OY_HhriMyEbsia-FpRqgoaKjpKWmp6ipqqtWlxgybxeoS1fVDaS5_1az3V-kX_XGGNPghY8g8q81tF8LkfDoIwY8W2FWXoe5_27zjH9q2jM05ZuvNfmdYnjW0x616SP-p3g96-PvzI8GDuAbPgt9-0sIkQHeCCZ35opOpopxt_tdTp55bPp8CmjCpb1OR0aWs_5UezjAlVNibbN4979hGY_BcQ7z07Bkt92DCgJiP9aP8pLSXM1gcFHvnDDAiAqfqqA1cWx2f3EIHn585U-tdtQsRZ5BJ7wJ4sZgMswGl5CxDgSFJ-MhnsQsyj6zAR_MVujCO4jUkLVsRtI38N6sN-T79EWL4w4N1ksEfzIUJDtYDNfbr83XkXpl3sB6DvYIrrrPi5Gq96WSWldJf5Pgz0IdJq_O36wS2dNYVqQmsOU-nwgigBh0NrD94K23PthUn8qbkULkp7PgyPGDXN-4MkvdjN8LVN-oEP1oaP5FMVbiH6b_7K_9NaXvJCELj59p2P_fCnJ2ULHCpwyfDGOKjg", + ); + }); +}); diff --git a/tests/providers/mirasim-control-plane.test.ts b/tests/providers/mirasim-control-plane.test.ts new file mode 100644 index 00000000000..822f38a042a --- /dev/null +++ b/tests/providers/mirasim-control-plane.test.ts @@ -0,0 +1,428 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { mkdirSync, mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { normalizeMirasimCompactBody } from "../../src/adapters/mirasim/compact"; +import { + cachedMirasimThinkingShape, + fetchMirasimLiveCatalog, + fetchMirasimQuota, + resetMirasimControlPlaneStateForTests, + setCachedMirasimRosterForTests, +} from "../../src/adapters/mirasim/control-plane"; +import { createMirasimDeviceIdentity } from "../../src/adapters/mirasim/crypto"; +import { + fetchMirasim, + fetchMirasimControl, + mirasimCredentialCacheScope, + resetMirasimTransportStateForTests, +} from "../../src/adapters/mirasim/transport"; +import { fetchProviderModelsWithAuth } from "../../src/codex/catalog/provider-models"; +import type { CapturedProviderGather } from "../../src/codex/catalog/gather-capture"; +import { clearModelCache } from "../../src/codex/model-cache"; +import { getAccountSet, saveAccountCredential, saveCredential } from "../../src/oauth/store"; +import type { OcxProviderConfig } from "../../src/types"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; + +const previousHome = process.env.OPENCODEX_HOME; +let home = ""; + +beforeEach(() => { + home = mkdtempSync(join(tmpdir(), "opencodex-mirasim-control-")); + mkdirSync(home, { recursive: true }); + process.env.OPENCODEX_HOME = home; + resetMirasimTransportStateForTests(); + resetMirasimControlPlaneStateForTests(); + clearModelCache("mirasim"); +}); + +afterEach(() => { + resetMirasimTransportStateForTests(); + resetMirasimControlPlaneStateForTests(); + clearModelCache("mirasim"); + if (previousHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previousHome; + removeTreeWithRetry(home); +}); + +function syntheticCredential() { + const identity = createMirasimDeviceIdentity(); + return { + access: "mirasim-access-test", + refresh: "mirasim-refresh-test", + expires: Date.now() + 3_600_000, + source: "oauth" as const, + accountId: "mirasim-test-account", + mirasim: { + devicePrivateKey: identity.privateKeyPem, + relayUrl: "https://relay.mirasim.ai", + adminUrl: "https://auth.mirasim.ai", + clientVersion: "0.0.336", + }, + }; +} + +type CapturedCall = { url: string; method: string; headers: Headers; body?: string }; + +function capture(calls: CapturedCall[], input: string | URL | Request, init?: RequestInit): CapturedCall { + const url = input instanceof Request ? input.url : input.toString(); + const headers = new Headers(init?.headers); + const body = typeof init?.body === "string" ? init.body : undefined; + const call = { url, method: init?.method ?? "GET", headers, ...(body ? { body } : {}) }; + calls.push(call); + return call; +} + +function providerWithFetch(fakeFetch: typeof fetch): OcxProviderConfig & { fetch: typeof fetch } { + return { + adapter: "mirasim", + baseUrl: "https://relay.mirasim.ai", + authMode: "oauth", + fetch: fakeFetch, + } as OcxProviderConfig & { fetch: typeof fetch }; +} + +function ticketResponse(): Response { + return new Response(JSON.stringify({ ticket: "device-ticket-test", expiresIn: 600 }), { + status: 200, + headers: { "content-type": "application/json" }, + }); +} + +describe("Mirasim signed transport", () => { + test("keeps roster/model cache authority stable across access-token rotation", async () => { + const initial = syntheticCredential(); + await saveCredential("mirasim", initial); + const beforeScope = mirasimCredentialCacheScope(initial.access); + setCachedMirasimRosterForTests(initial.access, { + version: "rotation-v1", + agents: { + claude: [{ + id: "claude-haiku-4-5", + contextWindow: 200_000, + effort: ["high"], + adaptive: false, + }], + codex: [], + }, + }); + + const set = getAccountSet("mirasim"); + expect(set).toBeDefined(); + const accountId = set!.activeAccountId; + const rotated = { + ...initial, + access: "mirasim-access-rotated", + refresh: "mirasim-refresh-rotated", + expires: Date.now() + 7_200_000, + }; + await saveAccountCredential("mirasim", accountId, rotated); + + expect(mirasimCredentialCacheScope(rotated.access)).toBe(beforeScope); + expect(cachedMirasimThinkingShape(rotated.access, "claude-haiku-4-5")).toBe("budget"); + }); + + test("mints a device ticket, then signs /v1/models without inference metadata sealing", async () => { + await saveCredential("mirasim", syntheticCredential()); + const calls: CapturedCall[] = []; + const fakeFetch = (async (input: string | URL | Request, init?: RequestInit): Promise => { + const call = capture(calls, input, init); + if (new URL(call.url).pathname === "/v1/device/session") return ticketResponse(); + return new Response(JSON.stringify({ data: [{ id: "gpt-5.6-sol", object: "model" }] }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + }) as typeof fetch; + + const response = await fetchMirasimControl( + "mirasim", + providerWithFetch(fakeFetch), + "mirasim-access-test", + "/v1/models", + ); + expect(response.status).toBe(200); + expect(calls).toHaveLength(2); + + const mint = calls[0]!; + expect(new URL(mint.url).pathname).toBe("/v1/device/session"); + expect(mint.method).toBe("POST"); + expect(mint.headers.get("authorization")).toBe("Bearer mirasim-access-test"); + expect(mint.headers.get("x-mirasim-sig")).toBeTruthy(); + expect(mint.headers.get("x-mirasim-client")).toBe("0.0.336"); + expect(mint.headers.get("x-mirasim-enc")).toBeNull(); + expect(mint.headers.get("x-mirasim-session")).toBeNull(); + + const models = calls[1]!; + expect(new URL(models.url).pathname).toBe("/v1/models"); + expect(models.method).toBe("GET"); + expect(models.headers.get("authorization")).toBe("Bearer device-ticket-test"); + expect(models.headers.get("x-mirasim-sig")).toBeTruthy(); + expect(models.headers.get("x-mirasim-device")).toBeTruthy(); + expect(models.headers.get("x-mirasim-client")).toBe("0.0.336"); + expect(models.headers.get("x-mirasim-enc")).toBeNull(); + expect(models.headers.get("x-mirasim-session")).toBeNull(); + expect(models.headers.get("x-mirasim-agent")).toBeNull(); + expect(models.headers.get("x-mirasim-call")).toBeNull(); + }); + + test("combines ticket-auth /v1/models with access-auth /v1/model-roster", async () => { + await saveCredential("mirasim", syntheticCredential()); + const calls: CapturedCall[] = []; + const fakeFetch = (async (input: string | URL | Request, init?: RequestInit): Promise => { + const call = capture(calls, input, init); + const path = new URL(call.url).pathname; + if (path === "/v1/device/session") return ticketResponse(); + if (path === "/v1/models") { + return new Response(JSON.stringify({ + data: [ + { id: "gpt-5.6-sol", object: "model", max_input_tokens: 300_000 }, + { id: "claude-sonnet-5", object: "model", max_input_tokens: 900_000 }, + { id: "claude-sonnet-5-20270101", object: "model" }, + { id: "gpt-5.6-sol-paid", object: "model" }, + { id: "other/model", object: "model" }, + { id: "*", object: "model" }, + ], + }), { status: 200, headers: { "content-type": "application/json" } }); + } + if (path === "/v1/model-roster") { + return new Response(JSON.stringify({ + version: "signed-v1", + agents: { + claude: [{ + id: "claude-sonnet-5", + label: "Sonnet account", + contextWindow: 1_000_000, + maxOutput: 128_000, + autoCompactRatio: 0.8, + effort: ["low", "high", "max"], + adaptive: false, + }], + codex: [{ + id: "gpt-5.6-sol", + contextWindow: 372_000, + maxOutput: 128_000, + effort: ["low", "medium", "high"], + adaptive: false, + }], + }, + }), { status: 200, headers: { "content-type": "application/json" } }); + } + return new Response("not found", { status: 404 }); + }) as typeof fetch; + + const result = await fetchMirasimLiveCatalog( + "mirasim", + providerWithFetch(fakeFetch), + "mirasim-access-test", + ); + expect(result.ok).toBe(true); + if (!result.ok) throw new Error("expected live Mirasim catalog"); + expect(result.models).toEqual([ + { + id: "gpt-5.6-sol", + object: "model", + contextWindow: 372_000, + maxOutputTokens: 128_000, + reasoningEfforts: ["low", "medium", "high"], + adaptiveThinking: false, + }, + { + id: "claude-sonnet-5", + object: "model", + contextWindow: 1_000_000, + maxOutputTokens: 128_000, + displayName: "Sonnet account", + reasoningEfforts: ["low", "high", "max"], + adaptiveThinking: false, + autoCompactRatio: 0.8, + }, + { + id: "claude-sonnet-5[1m]", + object: "model", + contextWindow: 1_000_000, + maxOutputTokens: 128_000, + displayName: "Sonnet account [1m]", + reasoningEfforts: ["low", "high", "max"], + adaptiveThinking: false, + autoCompactRatio: 0.8, + }, + ]); + expect(calls.map(call => new URL(call.url).pathname)).toEqual([ + "/v1/device/session", + "/v1/models", + "/v1/model-roster", + ]); + expect(calls[1]!.headers.get("authorization")).toBe("Bearer device-ticket-test"); + expect(calls[2]!.headers.get("authorization")).toBe("Bearer mirasim-access-test"); + expect(calls[2]!.headers.get("x-mirasim-sig")).toBeTruthy(); + expect(calls[2]!.headers.get("x-mirasim-device")).toBeTruthy(); + expect(calls[2]!.headers.get("x-mirasim-enc")).toBeNull(); + }); + + test("keeps signed roster fields authoritative over static registry hints in the routed catalog", async () => { + await saveCredential("mirasim", syntheticCredential()); + const calls: CapturedCall[] = []; + const fakeFetch = (async (input: string | URL | Request, init?: RequestInit): Promise => { + const call = capture(calls, input, init); + const path = new URL(call.url).pathname; + if (path === "/v1/device/session") return ticketResponse(); + if (path === "/v1/models") { + return new Response(JSON.stringify({ + data: [{ id: "gpt-5.6-sol", object: "model", max_input_tokens: 300_000 }], + }), { status: 200, headers: { "content-type": "application/json" } }); + } + if (path === "/v1/model-roster") { + return new Response(JSON.stringify({ + version: "signed-newer-than-registry", + agents: { + claude: [], + codex: [{ + id: "gpt-5.6-sol", + label: "Sol account live", + contextWindow: 500_000, + maxOutput: 150_000, + autoCompactRatio: 0.8, + effort: ["low", "max"], + adaptive: false, + }], + }, + }), { status: 200, headers: { "content-type": "application/json" } }); + } + return new Response("not found", { status: 404 }); + }) as typeof fetch; + + const provider = { + ...providerWithFetch(fakeFetch), + liveModels: true, + models: ["gpt-5.6-sol"], + defaultModel: "gpt-5.6-sol", + modelContextWindows: { "gpt-5.6-sol": 372_000 }, + modelMaxOutputTokens: { "gpt-5.6-sol": 128_000 }, + modelDisplayNames: { "gpt-5.6-sol": "Static Sol" }, + modelReasoningEfforts: { + "gpt-5.6-sol": ["low", "medium", "high", "xhigh", "max", "ultra"], + }, + } as OcxProviderConfig & { fetch: typeof fetch }; + const captured = { + name: "mirasim", + provider, + discovery: { maxResponseBytes: 1024 * 1024, maxModels: 256 }, + request: { + method: "GET", + url: "https://relay.mirasim.ai/v1/models", + headersWithoutCredential: {}, + headersWithCredential: {}, + }, + metadataModelIdCaseFold: false, + effectiveAlias: null, + } as unknown as CapturedProviderGather; + + const result = await fetchProviderModelsWithAuth( + captured, + 60_000, + 400_000, + { + kind: "observed", + resolve: () => ({ apiKey: "mirasim-access-test", observed: true }), + }, + ); + const model = result.models.find(row => row.id === "gpt-5.6-sol"); + expect(model?.displayName).toBe("Sol account live"); + expect(model?.contextWindow).toBe(400_000); + expect(model?.maxInputTokens).toBe(400_000); + expect(model?.contextCapped).toBe(true); + expect(model?.maxOutputTokens).toBe(150_000); + expect(model?.reasoningEfforts).toEqual(["low", "max"]); + expect(model?.autoCompactTokenLimit).toBe(360_000); + expect(result.outcome.state).toBe("authoritative"); + + const cached = await fetchProviderModelsWithAuth( + captured, + 60_000, + 400_000, + { + kind: "observed", + resolve: () => ({ apiKey: "mirasim-access-test", observed: true }), + }, + ); + const cachedModel = cached.models.find(row => row.id === "gpt-5.6-sol"); + expect(cachedModel?.displayName).toBe("Sol account live"); + expect(cachedModel?.contextWindow).toBe(400_000); + expect(cachedModel?.maxOutputTokens).toBe(150_000); + expect(cachedModel?.reasoningEfforts).toEqual(["low", "max"]); + expect(cachedModel?.autoCompactTokenLimit).toBe(360_000); + expect(calls.filter(call => new URL(call.url).pathname === "/v1/models")).toHaveLength(1); + }); + + test("sends /v1/limits as a signed control probe with provider-owned probe metadata", async () => { + await saveCredential("mirasim", syntheticCredential()); + const calls: CapturedCall[] = []; + const fakeFetch = (async (input: string | URL | Request, init?: RequestInit): Promise => { + const call = capture(calls, input, init); + const path = new URL(call.url).pathname; + if (path === "/v1/device/session") return ticketResponse(); + if (path === "/v1/limits") { + return new Response(JSON.stringify({ + windows: [{ name: "5h", budget: 100, used: 42, model_scoped: false }], + }), { status: 200, headers: { "content-type": "application/json" } }); + } + return new Response("not found", { status: 404 }); + }) as typeof fetch; + + const quota = await fetchMirasimQuota( + "mirasim", + providerWithFetch(fakeFetch), + "mirasim-access-test", + ); + expect(quota?.fiveHourPercent).toBe(42); + const limits = calls.find(call => new URL(call.url).pathname === "/v1/limits"); + expect(limits?.headers.get("x-mirasim-probe")).toBe("usage"); + expect(limits?.headers.get("x-mirasim-sig")).toBeTruthy(); + expect(limits?.headers.get("x-mirasim-enc")).toBeNull(); + }); + + test("signs and seals /v1/responses/compact as inference and normalizes ultra to max", async () => { + await saveCredential("mirasim", syntheticCredential()); + const calls: CapturedCall[] = []; + const fakeFetch = (async (input: string | URL | Request, init?: RequestInit): Promise => { + const call = capture(calls, input, init); + if (new URL(call.url).pathname === "/v1/device/session") return ticketResponse(); + return new Response(JSON.stringify({ output: [{ type: "compaction", encrypted_content: "opaque" }] }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + }) as typeof fetch; + + const body = normalizeMirasimCompactBody({ + model: "ignored", + input: [{ role: "user", content: "hello" }], + stream: true, + reasoning: { effort: "ultra" }, + }, "gpt-5.6-sol"); + const response = await fetchMirasim({ + url: "https://relay.mirasim.ai/v1/responses/compact", + method: "POST", + headers: { + "content-type": "application/json", + "x-mirasim-session": "caller-must-not-control-this", + }, + body: JSON.stringify(body), + }, "mirasim-access-test", { executor: fakeFetch }); + expect(response.status).toBe(200); + + const compact = calls.find(call => new URL(call.url).pathname === "/v1/responses/compact"); + expect(compact).toBeDefined(); + expect(compact!.headers.get("authorization")).toBe("Bearer device-ticket-test"); + expect(compact!.headers.get("x-mirasim-client")).toBe("0.0.336"); + expect(compact!.headers.get("x-mirasim-enc")).toBeTruthy(); + expect(compact!.headers.get("x-mirasim-session")).toBeNull(); + expect(compact!.headers.get("x-mirasim-agent")).toBeNull(); + expect(compact!.headers.get("x-mirasim-call")).toBeNull(); + expect(compact!.headers.get("x-mirasim-sig")).toBeNull(); + expect(JSON.parse(compact!.body!)).toEqual({ + model: "gpt-5.6-sol", + input: [{ role: "user", content: "hello" }], + reasoning: { effort: "max" }, + }); + }); +}); diff --git a/tests/providers/mirasim-endpoints.test.ts b/tests/providers/mirasim-endpoints.test.ts new file mode 100644 index 00000000000..72dd3229368 --- /dev/null +++ b/tests/providers/mirasim-endpoints.test.ts @@ -0,0 +1,276 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { mkdirSync, mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createMirasimDeviceIdentity } from "../../src/adapters/mirasim/crypto"; +import { resetMirasimTransportStateForTests } from "../../src/adapters/mirasim/transport"; +import { saveCredential } from "../../src/oauth/store"; +import { providerConfigSeed } from "../../src/providers/derive"; +import { getProviderRegistryEntry } from "../../src/providers/registry"; +import { handleClaudeCountTokens } from "../../src/server/claude-messages"; +import { handleSearch } from "../../src/server/search"; +import { handleResponses } from "../../src/server/responses/core"; +import type { RequestLogContext } from "../../src/server/request-log"; +import type { OcxConfig, OcxProviderConfig } from "../../src/types"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; +import { acquireOwnedSpendHome } from "../helpers/owned-spend-home"; + +const previousHome = process.env.OPENCODEX_HOME; +let home = ""; + +beforeEach(() => { + home = mkdtempSync(join(tmpdir(), "opencodex-mirasim-endpoints-")); + mkdirSync(home, { recursive: true }); + process.env.OPENCODEX_HOME = home; + resetMirasimTransportStateForTests(); +}); + +afterEach(() => { + resetMirasimTransportStateForTests(); + if (previousHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previousHome; + removeTreeWithRetry(home); +}); + +function syntheticCredential() { + const identity = createMirasimDeviceIdentity(); + return { + access: "mirasim-endpoint-access", + refresh: "mirasim-endpoint-refresh", + expires: Date.now() + 3_600_000, + source: "oauth" as const, + accountId: "mirasim-endpoint-account", + mirasim: { + devicePrivateKey: identity.privateKeyPem, + relayUrl: "https://relay.mirasim.ai", + adminUrl: "https://auth.mirasim.ai", + clientVersion: "0.0.336", + }, + }; +} + +type Captured = { path: string; method: string; headers: Headers; body?: Record }; + +function mirasimConfig(fakeFetch: typeof fetch): OcxConfig { + const entry = getProviderRegistryEntry("mirasim"); + if (!entry) throw new Error("missing Mirasim registry entry"); + const provider = { + ...providerConfigSeed(entry), + fetch: fakeFetch, + } as OcxProviderConfig & { fetch: typeof fetch }; + return { + port: 0, + defaultProvider: "mirasim", + providers: { mirasim: provider }, + } as OcxConfig; +} + +function captureFetch(calls: Captured[], responder: (call: Captured) => Response): typeof fetch { + return (async (input: string | URL | Request, init?: RequestInit): Promise => { + const url = input instanceof Request ? input.url : input.toString(); + const rawBody = typeof init?.body === "string" ? init.body : undefined; + let body: Record | undefined; + if (rawBody) { + try { body = JSON.parse(rawBody) as Record; } catch { /* not JSON */ } + } + const call: Captured = { + path: new URL(url).pathname, + method: init?.method ?? "GET", + headers: new Headers(init?.headers), + ...(body ? { body } : {}), + }; + calls.push(call); + if (call.path === "/v1/device/session") { + return new Response(JSON.stringify({ ticket: "mirasim-endpoint-ticket", expiresIn: 600 }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + return responder(call); + }) as typeof fetch; +} + +describe("Mirasim auxiliary inference endpoints", () => { + test("non-stream GPT caller receives bounded JSON even though Mirasim forces upstream Responses SSE", async () => { + await saveCredential("mirasim", syntheticCredential()); + const calls: Captured[] = []; + const config = mirasimConfig(captureFetch(calls, call => { + if (call.path !== "/v1/responses") return new Response("not found", { status: 404 }); + expect(call.body).toMatchObject({ + model: "gpt-5.6-luna", + stream: true, + store: false, + parallel_tool_calls: true, + include: ["reasoning.encrypted_content"], + }); + const terminal = { + type: "response.completed", + response: { + id: "resp_mirasim_fixture", + object: "response", + created_at: 1, + status: "completed", + model: "gpt-5.6-luna", + // The live relay can leave the terminal snapshot empty even after emitting authoritative + // output_item.done frames. The non-stream collector must reconstruct output from them. + output: [], + usage: { input_tokens: 4, output_tokens: 1, total_tokens: 5 }, + }, + }; + const functionDone = { + type: "response.output_item.done", + output_index: 1, + item: { + id: "fc_mirasim_fixture", + type: "function_call", + call_id: "call_mirasim_fixture", + name: "lookup", + arguments: "{}", + status: "completed", + }, + }; + const messageDone = { + type: "response.output_item.done", + output_index: 0, + item: { + id: "msg_mirasim_fixture", + type: "message", + role: "assistant", + status: "completed", + content: [{ type: "output_text", text: "OK", annotations: [] }], + }, + }; + return new Response([ + `data: ${JSON.stringify(functionDone)}\n\n`, + `data: ${JSON.stringify(messageDone)}\n\n`, + `data: ${JSON.stringify(terminal)}\n\n`, + ].join(""), { + status: 200, + headers: { "content-type": "text/event-stream" }, + }); + })); + const releaseSpendHome = acquireOwnedSpendHome(); + try { + const response = await handleResponses(new Request("http://127.0.0.1/v1/responses", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: "mirasim/gpt-5.6-luna", + input: [{ + type: "message", + role: "user", + content: [{ type: "input_text", text: "Reply with OK only." }], + }], + stream: false, + }), + }), config, { model: "", provider: "" }, { abortSignal: AbortSignal.timeout(5_000) }); + + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toContain("application/json"); + const json = await response.json() as { + id?: string; + status?: string; + model?: string; + output?: Array<{ + type?: string; + content?: Array<{ text?: string }>; + call_id?: string; + name?: string; + arguments?: string; + }>; + }; + expect(json.id).toBe("resp_mirasim_fixture"); + expect(json.status).toBe("completed"); + expect(json.model).toBe("gpt-5.6-luna"); + expect(json.output?.[0]?.content?.[0]?.text).toBe("OK"); + expect(json.output?.[1]).toMatchObject({ + type: "function_call", + call_id: "call_mirasim_fixture", + name: "lookup", + arguments: "{}", + }); + expect(calls.map(call => call.path)).toEqual([ + "/v1/device/session", + "/v1/responses", + ]); + } finally { + releaseSpendHome(); + } + }); + + test("Claude count_tokens uses the signed relay, bare model id, and long-context beta", async () => { + await saveCredential("mirasim", syntheticCredential()); + const calls: Captured[] = []; + const config = mirasimConfig(captureFetch(calls, call => { + if (call.path === "/v1/messages/count_tokens") { + return new Response(JSON.stringify({ input_tokens: 321 }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + return new Response("not found", { status: 404 }); + })); + + const response = await handleClaudeCountTokens(new Request("http://127.0.0.1/v1/messages/count_tokens", { + method: "POST", + headers: { + "content-type": "application/json", + "anthropic-beta": "other-beta", + }, + body: JSON.stringify({ + model: "claude-sonnet-5[1m]", + messages: [{ role: "user", content: "count this" }], + }), + }), config); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ input_tokens: 321 }); + expect(calls.map(call => call.path)).toEqual([ + "/v1/device/session", + "/v1/messages/count_tokens", + ]); + const count = calls[1]!; + expect(count.body?.model).toBe("claude-sonnet-5"); + expect(count.headers.get("authorization")).toBe("Bearer mirasim-endpoint-ticket"); + expect(count.headers.get("anthropic-beta")).toBe("other-beta,context-1m-2025-08-07"); + expect(count.headers.get("x-mirasim-enc")).toBeTruthy(); + expect(count.headers.get("x-mirasim-agent")).toBeNull(); + }); + + test("alpha/search routes a Mirasim GPT model through the signed inference transport", async () => { + await saveCredential("mirasim", syntheticCredential()); + const calls: Captured[] = []; + const config = mirasimConfig(captureFetch(calls, call => { + if (call.path === "/v1/alpha/search") { + return new Response(JSON.stringify({ output: "mirasim search" }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + return new Response("not found", { status: 404 }); + })); + const logCtx = {} as RequestLogContext; + const response = await handleSearch(new Request("http://127.0.0.1/v1/alpha/search", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: "mirasim/gpt-5.6-sol", + commands: { search_query: [{ q: "Mirasim" }] }, + }), + }), config, logCtx); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ output: "mirasim search" }); + expect(calls.map(call => call.path)).toEqual([ + "/v1/device/session", + "/v1/alpha/search", + ]); + const search = calls[1]!; + expect(search.body?.model).toBe("gpt-5.6-sol"); + expect(search.headers.get("authorization")).toBe("Bearer mirasim-endpoint-ticket"); + expect(search.headers.get("x-mirasim-enc")).toBeTruthy(); + expect(search.headers.get("x-mirasim-agent")).toBeNull(); + expect(logCtx.provider).toBe("mirasim"); + expect(logCtx.model).toBe("gpt-5.6-sol"); + }); +}); diff --git a/tests/providers/mirasim-oauth.test.ts b/tests/providers/mirasim-oauth.test.ts new file mode 100644 index 00000000000..ab8d5f288f2 --- /dev/null +++ b/tests/providers/mirasim-oauth.test.ts @@ -0,0 +1,332 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { handleMirasimBrowserOAuthRequest, loginMirasim } from "../../src/oauth/mirasim"; +import { parseMirasimLoginOpts } from "../../src/oauth/login-cli"; + +const originalFetch = globalThis.fetch; + +afterEach(() => { + globalThis.fetch = originalFetch; +}); + +type AuthCall = { + path: string; + body?: Record; +}; + +function installEmailAuthServer( + calls: AuthCall[], + verifyPayload: Record = { + access_token: "opaque-access-token", + refresh_token: "opaque-refresh-token", + expires_in: 1800, + }, +): void { + globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit): Promise => { + const url = input instanceof Request ? input.url : input.toString(); + const path = new URL(url).pathname; + const rawBody = typeof init?.body === "string" ? init.body : undefined; + const body = rawBody ? JSON.parse(rawBody) as Record : undefined; + calls.push({ path, ...(body ? { body } : {}) }); + if (path === "/auth/code") { + // Development servers may echo a code. OpenCodex must never consume or surface it. + return new Response(JSON.stringify({ code: "server-must-not-drive-login" }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + if (path === "/auth/verify") { + return new Response(JSON.stringify(verifyPayload), { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + if (path === "/auth/me") { + return new Response(JSON.stringify({ email: "user@example.com", plan: "pro" }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + throw new Error(`unexpected Mirasim auth path: ${path}`); + }) as typeof fetch; +} + +function installBrowserAuthServer(calls: AuthCall[]): void { + globalThis.fetch = (async (input: string | URL | Request): Promise => { + const url = input instanceof Request ? input.url : input.toString(); + const path = new URL(url).pathname; + calls.push({ path }); + if (path === "/auth/oauth/providers") { + return new Response(JSON.stringify({ providers: ["github", "google"] }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + if (path === "/auth/me") { + return new Response(JSON.stringify({ email: "browser@example.com" }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + throw new Error(`unexpected Mirasim auth path: ${path}`); + }) as typeof fetch; +} + +describe("Mirasim OAuth/email login", () => { + test("email login requests a code, prompts locally, and persists renewable credential material", async () => { + const calls: AuthCall[] = []; + installEmailAuthServer(calls); + const progress: string[] = []; + const prompts: string[] = []; + + const credential = await loginMirasim({ + onProgress: message => progress.push(message), + onManualCodeInput: async (_state, prompt) => { + prompts.push(prompt ?? ""); + return " 123456 "; + }, + }, { email: " user@example.com " }); + + expect(calls).toEqual([ + { path: "/auth/code", body: { email: "user@example.com" } }, + { path: "/auth/verify", body: { email: "user@example.com", code: "123456" } }, + { path: "/auth/me" }, + ]); + expect(progress).toEqual(["Mirasim sent a sign-in code to user@example.com."]); + expect(prompts).toEqual(["Enter the Mirasim sign-in code: "]); + expect(credential.access).toBe("opaque-access-token"); + expect(credential.refresh).toBe("opaque-refresh-token"); + expect(credential.email).toBe("user@example.com"); + expect(credential.mirasim?.devicePrivateKey).toContain("PRIVATE KEY"); + expect(credential.mirasim?.relayUrl).toBe("https://relay.mirasim.ai"); + }); + + test("provided email code skips the send-code request", async () => { + const calls: AuthCall[] = []; + installEmailAuthServer(calls); + + const credential = await loginMirasim({}, { + email: "user@example.com", + code: "654321", + }); + + expect(calls).toEqual([ + { path: "/auth/verify", body: { email: "user@example.com", code: "654321" } }, + { path: "/auth/me" }, + ]); + expect(credential.refresh).toBe("opaque-refresh-token"); + }); + + test("email login refuses a non-renewable response", async () => { + const calls: AuthCall[] = []; + installEmailAuthServer(calls, { access_token: "short-lived-only" }); + + await expect(loginMirasim({}, { + email: "user@example.com", + code: "123456", + })).rejects.toThrow("no renewable credential"); + }); + + test("CLI Mirasim email options map into the normal OAuth login transaction", () => { + expect(parseMirasimLoginOpts([])).toBeUndefined(); + expect(parseMirasimLoginOpts([ + "--email", "user@example.com", + "--code", "123456", + ])).toEqual({ + mirasimEmail: "user@example.com", + mirasimCode: "123456", + }); + expect(() => parseMirasimLoginOpts(["--code", "123456"])) + .toThrow("--code requires --email"); + expect(() => parseMirasimLoginOpts(["--wat"])) + .toThrow("Unknown Mirasim login option"); + }); + + test("management browser flow starts on a local provider chooser instead of silently preferring GitHub", async () => { + const calls: AuthCall[] = []; + installBrowserAuthServer(calls); + let authUrl = ""; + const login = loginMirasim({ + onAuth: info => { authUrl = info.url; }, + }, { + browserBaseUrl: "http://127.0.0.1:10100", + }); + await Promise.resolve(); + + expect(new URL(authUrl).origin).toBe("http://127.0.0.1:10100"); + expect(new URL(authUrl).pathname).toBe("/oauth/mirasim/start"); + expect(new URL(authUrl).searchParams.get("lang")).toBe("en"); + expect(authUrl).not.toContain("/auth/oauth/github/login"); + + const chooser = await handleMirasimBrowserOAuthRequest(new Request(authUrl)); + expect(chooser?.status).toBe(200); + const chooserHtml = await chooser!.text(); + expect(chooserHtml).toContain("/provider-icons/mirasim.svg"); + expect(chooserHtml).toContain("Continue with GitHub"); + expect(chooserHtml).toContain("Continue with Google"); + + const start = new URL(authUrl); + const state = start.searchParams.get("state"); + expect(state).toBeTruthy(); + start.searchParams.set("provider", "google"); + const redirect = await handleMirasimBrowserOAuthRequest(new Request(start)); + expect(redirect?.status).toBe(302); + const upstream = new URL(redirect!.headers.get("location")!); + expect(upstream.origin).toBe("https://auth.mirasim.ai"); + expect(upstream.pathname).toBe("/auth/oauth/google/login"); + const redirectUri = new URL(upstream.searchParams.get("redirect_uri")!); + expect(redirectUri.origin).toBe("http://127.0.0.1:10100"); + expect(redirectUri.pathname).toMatch(/^\/oauth\/mirasim\/callback\/[A-Za-z0-9_-]{20,}$/); + expect(upstream.searchParams.get("state")).toBe(state); + + // Current Mirasim production omits the separately supplied state on the token callback. + // The unguessable one-use callback path is therefore the channel binding, matching the + // upstream CLI implementation. + const callback = new URL(redirectUri); + callback.searchParams.set("access_token", "browser-access"); + callback.searchParams.set("refresh_token", "browser-refresh"); + const callbackResponse = await handleMirasimBrowserOAuthRequest(new Request(callback)); + expect(callbackResponse?.status).toBe(303); + const clean = callbackResponse!.headers.get("location"); + expect(clean).not.toContain("access_token"); + expect(clean).not.toContain("refresh_token"); + + const completion = await handleMirasimBrowserOAuthRequest(new Request(clean!)); + expect(completion?.status).toBe(200); + const credential = await login; + expect(credential.access).toBe("browser-access"); + expect(credential.refresh).toBe("browser-refresh"); + expect(credential.email).toBe("browser@example.com"); + expect(calls.map(call => call.path)).toEqual([ + "/auth/oauth/providers", + "/auth/oauth/providers", + "/auth/me", + ]); + }); + + test("management browser flow follows English, Traditional Chinese, and Simplified Chinese locale", async () => { + const calls: AuthCall[] = []; + installBrowserAuthServer(calls); + const cases = [ + { + locale: "en", + htmlLang: "en", + title: "Sign in to Mirasim", + chooser: "Choose the account provider you want to use.", + button: "Continue with GitHub", + instruction: "Choose GitHub or Google on the Mirasim sign-in page.", + }, + { + locale: "zh-TW", + htmlLang: "zh-TW", + title: "登入 Mirasim", + chooser: "選擇要用於登入的帳號供應商。", + button: "使用 GitHub 繼續", + instruction: "請在 Mirasim 登入頁面選擇 GitHub 或 Google。", + }, + { + locale: "zh", + htmlLang: "zh-CN", + title: "登录 Mirasim", + chooser: "选择用于登录的账户提供商。", + button: "使用 GitHub 继续", + instruction: "请在 Mirasim 登录页面选择 GitHub 或 Google。", + }, + ] as const; + + for (const item of cases) { + const abort = new AbortController(); + let authUrl = ""; + let instruction = ""; + const login = loginMirasim({ + signal: abort.signal, + onAuth: info => { + authUrl = info.url; + instruction = info.instructions ?? ""; + }, + }, { + browserBaseUrl: "http://127.0.0.1:10100", + browserLocale: item.locale, + }); + await Promise.resolve(); + + expect(new URL(authUrl).searchParams.get("lang")).toBe(item.htmlLang); + expect(instruction).toBe(item.instruction); + const chooser = await handleMirasimBrowserOAuthRequest(new Request(authUrl)); + expect(chooser?.status).toBe(200); + const html = await chooser!.text(); + expect(html).toContain(` { + const traditional = await handleMirasimBrowserOAuthRequest(new Request( + "http://127.0.0.1:10100/oauth/mirasim/start?state=missing&lang=zh-TW", + )); + expect(traditional?.status).toBe(400); + expect(await traditional!.text()).toContain("此登入連結無效或已過期"); + + const simplified = await handleMirasimBrowserOAuthRequest(new Request( + "http://127.0.0.1:10100/oauth/mirasim/start?state=missing&lang=zh-CN", + )); + expect(simplified?.status).toBe(400); + expect(await simplified!.text()).toContain("此登录链接无效或已过期"); + + const english = await handleMirasimBrowserOAuthRequest(new Request( + "http://127.0.0.1:10100/oauth/mirasim/start?state=missing&lang=en", + )); + expect(english?.status).toBe(400); + expect(await english!.text()).toContain("This sign-in link is invalid or has expired"); + }); + + test("cancelled management browser flow invalidates its start capability", async () => { + const calls: AuthCall[] = []; + installBrowserAuthServer(calls); + const abort = new AbortController(); + let authUrl = ""; + const login = loginMirasim({ + signal: abort.signal, + onAuth: info => { authUrl = info.url; }, + }, { + browserBaseUrl: "http://127.0.0.1:10100", + }); + await Promise.resolve(); + abort.abort(); + await expect(login).rejects.toThrow("cancelled"); + + const expired = await handleMirasimBrowserOAuthRequest(new Request(authUrl)); + expect(expired?.status).toBe(400); + expect(await expired!.text()).toContain("invalid or has expired"); + }); + + test("random callback path accepts omitted state but rejects a present mismatched state", async () => { + const calls: AuthCall[] = []; + installBrowserAuthServer(calls); + let authUrl = ""; + const login = loginMirasim({ + onAuth: info => { authUrl = info.url; }, + }, { + browserBaseUrl: "http://127.0.0.1:10100", + }); + await Promise.resolve(); + + const start = new URL(authUrl); + start.searchParams.set("provider", "github"); + const redirect = await handleMirasimBrowserOAuthRequest(new Request(start)); + const upstream = new URL(redirect!.headers.get("location")!); + const callback = new URL(upstream.searchParams.get("redirect_uri")!); + callback.searchParams.set("state", "wrong-state"); + callback.searchParams.set("access_token", "must-not-save"); + callback.searchParams.set("refresh_token", "must-not-save"); + + const denied = await handleMirasimBrowserOAuthRequest(new Request(callback)); + expect(denied?.status).toBe(400); + expect(await denied!.text()).toContain("did not match this OpenCodex login attempt"); + await expect(login).rejects.toThrow("state mismatch"); + }); +}); diff --git a/tests/providers/mirasim-provider.test.ts b/tests/providers/mirasim-provider.test.ts new file mode 100644 index 00000000000..94c687e779c --- /dev/null +++ b/tests/providers/mirasim-provider.test.ts @@ -0,0 +1,309 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { createMirasimAdapter } from "../../src/adapters/mirasim"; +import { + ensureMirasimClaudeAgentSystemMarker, + MIRASIM_CLAUDE_AGENT_SYSTEM_MARKER, +} from "../../src/adapters/mirasim/anthropic"; +import { + parseMirasimLimits, + parseMirasimRoster, + resetMirasimControlPlaneStateForTests, + setCachedMirasimRosterForTests, +} from "../../src/adapters/mirasim/control-plane"; +import { OAUTH_PROVIDERS } from "../../src/oauth"; +import { providerConfigSeed } from "../../src/providers/derive"; +import { providerOAuthAccountQuotaMode, supportsPerAccountQuota } from "../../src/providers/quota"; +import { getProviderRegistryEntry } from "../../src/providers/registry"; +import type { OcxProviderConfig } from "../../src/types"; +import { withTestTranslatorBudget } from "../helpers/translator-budget"; + +function entry() { + const found = getProviderRegistryEntry("mirasim"); + if (!found) throw new Error("missing Mirasim registry entry"); + return found; +} + +function adapter() { + const provider = { + ...providerConfigSeed(entry()), + apiKey: "synthetic-mirasim-access", + } as OcxProviderConfig; + return withTestTranslatorBudget(createMirasimAdapter(provider)); +} + +describe("Mirasim provider", () => { + afterEach(() => resetMirasimControlPlaneStateForTests()); + + test("is a native OAuth provider with an HTTP/1.1-pinned live catalog", () => { + expect(entry().adapter).toBe("mirasim"); + expect(entry().authKind).toBe("oauth"); + expect(entry().oauthId).toBe("mirasim"); + expect(entry().liveModels).toBe(true); + expect(entry().modelDiscovery?.path).toBe("/v1/models"); + expect(OAUTH_PROVIDERS.mirasim?.providerConfig.upstreamHttpVersion).toBe("http1.1"); + expect(supportsPerAccountQuota("mirasim")).toBe(true); + expect(providerOAuthAccountQuotaMode("mirasim")).toBe("probe"); + }); + + test("routes Claude through Messages and uses adaptive thinking before a roster is observed", async () => { + const request = await adapter().buildRequest({ + modelId: "claude-haiku-4-5", + stream: true, + options: { reasoning: "high" }, + context: { + messages: [{ role: "user", content: "hello", timestamp: 0 }], + }, + }); + + expect(new URL(request.url).pathname).toBe("/v1/messages"); + const body = JSON.parse(request.body) as { + thinking?: { type?: string; budget_tokens?: number }; + output_config?: { effort?: string }; + system?: Array<{ type?: string; text?: string }>; + }; + expect(body.thinking).toEqual({ type: "adaptive" }); + expect(body.output_config?.effort).toBe("high"); + expect(body.thinking?.budget_tokens).toBeUndefined(); + expect(body.system?.[0]).toEqual({ + type: "text", + text: MIRASIM_CLAUDE_AGENT_SYSTEM_MARKER, + cache_control: { type: "ephemeral" }, + }); + expect(request.headers["x-opencodex-mirasim-wire"]).toBe("anthropic"); + }); + + test("prepends the relay's minimum Claude Agent marker without replacing the caller system prompt", async () => { + const request = await adapter().buildRequest({ + modelId: "claude-haiku-4-5", + stream: true, + options: {}, + context: { + systemPrompt: ["Keep the caller instruction intact."], + messages: [{ role: "user", content: "hello", timestamp: 0 }], + }, + }); + const body = JSON.parse(request.body) as { + system?: Array<{ type?: string; text?: string }>; + }; + expect(body.system?.[0]).toEqual({ + type: "text", + text: MIRASIM_CLAUDE_AGENT_SYSTEM_MARKER, + cache_control: { type: "ephemeral" }, + }); + expect(body.system?.some(block => block.text === "Keep the caller instruction intact.")).toBe(true); + expect(body.system?.filter(block => block.text === MIRASIM_CLAUDE_AGENT_SYSTEM_MARKER)).toHaveLength(1); + }); + + test("keeps the Claude Agent marker cacheable while capping Mirasim at four breakpoints", () => { + const body: Record = { + system: [{ + type: "text", + text: "system", + cache_control: { type: "ephemeral", ttl: "1h" }, + }], + tools: [{ + name: "lookup", + cache_control: { type: "ephemeral", ttl: "1h" }, + }], + messages: [ + { + role: "user", + content: [{ + type: "text", + text: "older", + cache_control: { type: "ephemeral", ttl: "1h" }, + }], + }, + { + role: "user", + content: [{ + type: "text", + text: "newer", + cache_control: { type: "ephemeral", ttl: "1h" }, + }], + }, + ], + }; + + ensureMirasimClaudeAgentSystemMarker(body); + + const system = body.system as Array>; + const tools = body.tools as Array>; + const messages = body.messages as Array<{ content: Array> }>; + expect(system[0]).toMatchObject({ + type: "text", + text: MIRASIM_CLAUDE_AGENT_SYSTEM_MARKER, + cache_control: { type: "ephemeral", ttl: "1h" }, + }); + expect(system[1]?.cache_control).toEqual({ type: "ephemeral", ttl: "1h" }); + expect(tools[0]?.cache_control).toEqual({ type: "ephemeral", ttl: "1h" }); + expect(messages[0]?.content[0]?.cache_control).toBeUndefined(); + expect(messages[1]?.content[0]?.cache_control).toEqual({ type: "ephemeral", ttl: "1h" }); + }); + + test("uses the signed roster's budget thinking shape without probing during inference", async () => { + setCachedMirasimRosterForTests("synthetic-mirasim-access", { + version: "test-v1", + agents: { + claude: [{ + id: "claude-haiku-4-5", + contextWindow: 200_000, + effort: ["low", "medium", "high"], + adaptive: false, + }], + codex: [], + }, + }); + + const request = await adapter().buildRequest({ + modelId: "claude-haiku-4-5", + stream: true, + options: { reasoning: "high" }, + context: { + messages: [{ role: "user", content: "hello", timestamp: 0 }], + }, + }); + const body = JSON.parse(request.body) as { + thinking?: { type?: string; budget_tokens?: number }; + output_config?: { effort?: string }; + }; + expect(body.thinking).toEqual({ type: "enabled", budget_tokens: 24_576 }); + expect(body.output_config?.effort).toBeUndefined(); + }); + + test("strips the [1m] selector and merges the long-context beta without duplication", async () => { + setCachedMirasimRosterForTests("synthetic-mirasim-access", { + version: "test-v1", + agents: { + claude: [{ + id: "claude-sonnet-5", + contextWindow: 1_000_000, + effort: ["low", "high", "max"], + adaptive: true, + }], + codex: [], + }, + }); + const request = await adapter().buildRequest({ + modelId: "claude-sonnet-5[1m]", + stream: true, + options: { reasoning: "high" }, + context: { + messages: [{ role: "user", content: "hello", timestamp: 0 }], + }, + }, { + headers: new Headers({ + "anthropic-beta": "other-beta,context-1m-2025-08-07", + }), + }); + const body = JSON.parse(request.body) as { model?: string }; + expect(body.model).toBe("claude-sonnet-5"); + expect(request.headers["anthropic-beta"]).toBe( + "other-beta,context-1m-2025-08-07", + ); + }); + + test("routes GPT through Responses and folds ultra to the relay's max single-turn effort", async () => { + const mirasim = adapter(); + const parsed = { + modelId: "gpt-5.6-sol", + stream: false, + options: { reasoning: "ultra" }, + context: { messages: [] }, + _rawBody: { + model: "gpt-5.6-sol", + input: "hello", + reasoning: { effort: "ultra" }, + }, + }; + expect(mirasim.passthroughFor?.(parsed)).toBe(true); + const request = await mirasim.buildRequest(parsed); + + expect(new URL(request.url).pathname).toBe("/v1/responses"); + const body = JSON.parse(request.body) as { + reasoning?: { effort?: string }; + stream?: boolean; + store?: boolean; + parallel_tool_calls?: boolean; + include?: string[]; + }; + expect(body.reasoning?.effort).toBe("max"); + expect(body.stream).toBe(true); + expect(body.store).toBe(false); + expect(body.parallel_tool_calls).toBe(true); + expect(body.include).toEqual(["reasoning.encrypted_content"]); + expect(request.headers["x-opencodex-mirasim-wire"]).toBe("responses"); + expect(mirasim.passthroughFor?.({ + modelId: "claude-haiku-4-5", + stream: true, + options: {}, + context: { messages: [] }, + })).toBe(false); + }); + + test("parses the signed roster conservatively and excludes paid/foreign families", () => { + const roster = parseMirasimRoster({ + version: "v2", + agents: { + claude: [ + { + id: "Claude-Sonnet-5", + label: "Sonnet live", + contextWindow: 1_000_000, + maxOutput: 128_000, + autoCompactRatio: 0.8, + effort: ["low", "HIGH", "high", "bogus"], + adaptive: true, + }, + { id: "claude-opus-5-paid", contextWindow: 1_000_000, adaptive: true }, + ], + codex: [ + { id: "gpt-5.6-sol", contextWindow: 372_000, maxOutput: 128_000, effort: ["max"], adaptive: false }, + { id: "claude-wrong-family", contextWindow: 123_000, adaptive: true }, + ], + }, + }); + + expect(roster?.version).toBe("v2"); + expect(roster?.agents.claude).toEqual([{ + id: "claude-sonnet-5", + label: "Sonnet live", + contextWindow: 1_000_000, + maxOutput: 128_000, + autoCompactRatio: 0.8, + effort: ["low", "high"], + adaptive: true, + }]); + expect(roster?.agents.codex.map(model => model.id)).toEqual(["gpt-5.6-sol"]); + }); + + test("normalizes structured /v1/limits windows without promoting model limits to account bars", () => { + const quota = parseMirasimLimits({ + paid: true, + degraded: false, + windows: [ + { name: "5h", budget: 100, used: 25, reset_at: 1_800_000_000, model_scoped: false }, + { name: "7d", budget: 100, used: 99, reset_at: "2030-01-01T00:00:00Z", model_scoped: false }, + { name: "7d_fable", budget: 100, used: 100, model_scoped: true }, + ], + }); + + expect(quota?.fiveHourPercent).toBe(25); + expect(quota?.weeklyPercent).toBe(100); + expect(quota?.customWindows).toEqual([ + { label: "Model · 7d_fable", percent: 100 }, + ]); + }); + + test("keeps canonical 5h/7d windows even when there are no custom limits", () => { + expect(parseMirasimLimits({ + windows: [ + { name: "7d", budget: 100, used: 10, reset_at: 1_800_000_000, model_scoped: false }, + ], + })).toMatchObject({ + weeklyPercent: 10, + weeklyResetAt: 1_800_000_000_000, + customWindows: [], + }); + }); +}); diff --git a/tests/responses/responses-compaction-routing.test.ts b/tests/responses/responses-compaction-routing.test.ts index 587159b58dd..6af79361f7e 100644 --- a/tests/responses/responses-compaction-routing.test.ts +++ b/tests/responses/responses-compaction-routing.test.ts @@ -74,6 +74,11 @@ describe("supportsNativeResponsesCompactEndpoint (#422)", () => { baseUrl: "https://api.openai.com/v1", authMode: "key", } as OcxProviderConfig; + const mirasim = { + adapter: "mirasim", + baseUrl: "https://relay.mirasim.ai", + authMode: "oauth", + } as OcxProviderConfig; test("accepts the canonical ChatGPT backend and the official OpenAI API", () => { expect(supportsNativeResponsesCompactEndpoint("openai", canonicalForward)).toBe(true); @@ -96,6 +101,15 @@ describe("supportsNativeResponsesCompactEndpoint (#422)", () => { baseUrl: "https://gateway.example/v1", })).toBe(false); }); + + test("accepts only the canonical Mirasim relay for native signed compact", () => { + expect(supportsNativeResponsesCompactEndpoint("mirasim", mirasim)).toBe(true); + expect(supportsNativeResponsesCompactEndpoint("mirasim", { + ...mirasim, + baseUrl: "https://gateway.example", + })).toBe(false); + expect(supportsNativeResponsesCompactEndpoint("renamed-mirasim", mirasim)).toBe(false); + }); }); describe("Codex auth-context error parity (#2392)", () => { From 2d86bd7d804c4dbce840620e70a86d6a95620ae7 Mon Sep 17 00:00:00 2001 From: letr1n1ty Date: Mon, 21 Sep 2026 15:31:46 +0800 Subject: [PATCH 02/33] feat(gui): integrate Mirasim provider management --- gui/public/provider-icons/README.md | 5 + gui/public/provider-icons/mirasim.svg | 14 +++ gui/src/components/QuotaBars.tsx | 6 + .../provider-workspace/ProviderAuthPanel.tsx | 9 +- gui/src/pages/use-providers-oauth.ts | 39 ++++++- gui/src/provider-icons.ts | 2 + gui/src/provider-workspace/auth.ts | 18 +++ .../add-provider-oauth-url-leak.test.tsx | 19 +++ .../mirasim-auth-panel-presentation.test.tsx | 108 ++++++++++++++++++ gui/tests/mirasim-oauth-cancel-roster.test.ts | 68 +++++++++++ gui/tests/mirasim-quota-presentation.test.ts | 42 +++++++ gui/tests/provider-icons.test.ts | 1 + gui/tests/provider-workspace-auth.test.ts | 26 +++++ 13 files changed, 352 insertions(+), 5 deletions(-) create mode 100644 gui/public/provider-icons/mirasim.svg create mode 100644 gui/tests/mirasim-auth-panel-presentation.test.tsx create mode 100644 gui/tests/mirasim-oauth-cancel-roster.test.ts create mode 100644 gui/tests/mirasim-quota-presentation.test.ts create mode 100644 gui/tests/provider-workspace-auth.test.ts 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, ``/`<desc>` 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 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 793 698" role="img" aria-labelledby="title"> + <title id="title">Mirasim + + + + + + + + + + + + diff --git a/gui/src/components/QuotaBars.tsx b/gui/src/components/QuotaBars.tsx index fc72b0bb5f3..a7d35b970f4 100644 --- a/gui/src/components/QuotaBars.tsx +++ b/gui/src/components/QuotaBars.tsx @@ -25,6 +25,7 @@ export type QuotaBarRow = { */ function rawCustomWindowRank(rawLabel: string): number { if (rawLabel === "5h") return 0; + if (/^Model\s*·\s*7d_(claude|fable)$/i.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 +54,11 @@ export function isCustomQuotaWindowIncomplete( } function localizeCustomQuotaLabel(rawLabel: string, t: TFn): string { + const mirasimWeekly = rawLabel.match(/^Model\s*·\s*7d_(claude|fable)$/i); + if (mirasimWeekly) { + const family = mirasimWeekly[1]?.toLowerCase() === "fable" ? "Fable" : "Claude"; + 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({
  • ${escapeHtml(copy.continueWith(label))}
  • `; - }).join(""); +function mirasimBrowserFormAction(session: MirasimBrowserSession): string { + const action = new URL(MIRASIM_BROWSER_START_PATH, `${session.baseUrl}/`); + action.searchParams.set("state", session.state); + action.searchParams.set("lang", session.locale); + return action.pathname + action.search; +} + +function renderMirasimEmailEntry(session: MirasimBrowserSession, message?: string, status = 200): Response { + const copy = MIRASIM_BROWSER_COPY[session.locale]; + const notice = message ? `

    ${escapeHtml(message)}

    ` : ""; return browserHtml( - locale, + session.locale, + copy.signInTitle, + `Mirasim

    ${escapeHtml(copy.chooseProvider)}

    ${notice}
    `, + status, + ); +} + +function renderMirasimCodeEntry(session: MirasimBrowserSession, message?: string, status = 200): Response { + const copy = MIRASIM_BROWSER_COPY[session.locale]; + const notice = message ? `

    ${escapeHtml(message)}

    ` : ""; + const instruction = session.locale === "zh-TW" + ? "驗證碼已寄出。請輸入郵件中的驗證碼。" + : session.locale === "zh-CN" + ? "验证码已发送。请输入邮件中的验证码。" + : "The verification code was sent. Enter the code from your email."; + const label = session.locale === "zh-TW" ? "驗證碼" : session.locale === "zh-CN" ? "验证码" : "Verification code"; + const submit = session.locale === "zh-TW" ? "完成登入" : session.locale === "zh-CN" ? "完成登录" : "Complete sign-in"; + return browserHtml( + session.locale, copy.signInTitle, - `Mirasim

    ${escapeHtml(copy.chooseProvider)}

      ${links}
    `, + `

    ${escapeHtml(instruction)}

    ${notice}
    `, + status, ); } function settleMirasimBrowserSession(session: MirasimBrowserSession, tokens: CallbackTokens): void { if (session.completed) return; session.completed = true; - session.pendingTokens = undefined; clearTimeout(session.timeout); session.abort?.(); session.abort = undefined; session.resolve(tokens); - const cleanup = setTimeout(() => { - if (mirasimBrowserSessions.get(session.state) === session) { - mirasimBrowserCallbackStates.delete(session.callbackToken); - mirasimBrowserSessions.delete(session.state); - } - }, MIRASIM_BROWSER_COMPLETED_TTL_MS); - cleanup.unref?.(); + mirasimBrowserSessions.delete(session.state); } function rejectMirasimBrowserSession(session: MirasimBrowserSession, error: Error): void { if (mirasimBrowserSessions.get(session.state) !== session) return; - mirasimBrowserCallbackStates.delete(session.callbackToken); mirasimBrowserSessions.delete(session.state); clearTimeout(session.timeout); session.abort?.(); session.abort = undefined; - session.pendingTokens = undefined; session.reject(error); } +async function readBoundedBrowserForm(req: Request): Promise { + const contentType = req.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase(); + if (contentType !== "application/x-www-form-urlencoded") { + throw new Error("Mirasim browser form used an unsupported content type"); + } + const reader = req.body?.getReader(); + if (!reader) return new URLSearchParams(); + const chunks: Uint8Array[] = []; + let total = 0; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + total += value.byteLength; + if (total > MIRASIM_BROWSER_FORM_MAX_BYTES) { + try { await reader.cancel(); } catch { /* already closed */ } + throw new Error("Mirasim browser form exceeded the accepted size"); + } + chunks.push(value); + } + } finally { + reader.releaseLock(); + } + const bytes = new Uint8Array(total); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + return new URLSearchParams(new TextDecoder().decode(bytes)); +} + /** - * Public browser resources for the management-owned Mirasim OAuth flow. + * Public browser resource for the management-owned Mirasim login flow. * - * These routes deliberately sit outside /api management auth. The 256-bit pending state is the - * capability, matching the upstream CPA design: the start route only chooses an offered provider, - * while the callback accepts bounded renewable credentials only for that one live state. + * Renewable credentials never cross the browser URL. The browser POSTs only an email address and + * a short-lived verification code to this management origin; OpenCodex exchanges those values + * server-to-server with Mirasim and stores the returned access/refresh material directly. */ export async function handleMirasimBrowserOAuthRequest( req: Request, url = new URL(req.url), ): Promise { - const isStart = url.pathname === MIRASIM_BROWSER_START_PATH; - const isCallbackNamespace = url.pathname.startsWith(MIRASIM_BROWSER_CALLBACK_PREFIX); - if (!isStart && !isCallbackNamespace) { - return null; - } + if (url.pathname !== MIRASIM_BROWSER_START_PATH) return null; const requestLocale = normalizeMirasimBrowserLocale( url.searchParams.get("lang") ?? req.headers.get("accept-language"), ); - if (req.method !== "GET") { - return new Response(MIRASIM_BROWSER_COPY[requestLocale].methodNotAllowed, { - status: 405, - headers: oauthBrowserHeaders("text/plain; charset=utf-8"), - }); - } - const returnedState = url.searchParams.get("state")?.trim() ?? ""; - const session = isStart - ? (returnedState ? currentMirasimBrowserSession(returnedState) : undefined) - : currentMirasimBrowserSessionByCallback(url.pathname); + const session = returnedState ? currentMirasimBrowserSession(returnedState) : undefined; if (!session) { const copy = MIRASIM_BROWSER_COPY[requestLocale]; return browserHtml( @@ -795,175 +735,88 @@ export async function handleMirasimBrowserOAuthRequest( } const locale = session.locale; const copy = MIRASIM_BROWSER_COPY[locale]; - const state = session.state; - if (returnedState && returnedState !== state) { - rejectMirasimBrowserSession(session, new Error("Mirasim OAuth state mismatch")); - return browserHtml( - locale, - copy.failedTitle, - `

    ${escapeHtml(copy.mismatchBody)}

    `, - 400, - ); + + if (req.method === "GET") { + return session.email ? renderMirasimCodeEntry(session) : renderMirasimEmailEntry(session); + } + if (req.method !== "POST") { + return new Response(copy.methodNotAllowed, { + status: 405, + headers: oauthBrowserHeaders("text/plain; charset=utf-8"), + }); } - if (isStart) { - let providers: string[]; + let form: URLSearchParams; + try { + form = await readBoundedBrowserForm(req); + } catch { + return renderMirasimEmailEntry(session, copy.failedBody, 400); + } + const action = form.get("action")?.trim(); + if (action === "send") { + let email: string; try { - providers = await discoverLoginProviders(session.adminUrl, req.signal); + email = normalizeLoginEmail(form.get("email") ?? ""); } catch { - return browserHtml( - locale, - copy.unavailableTitle, - `

    ${escapeHtml(copy.unavailableBody)}

    `, - 503, - ); + return renderMirasimEmailEntry(session, copy.failedBody, 400); } - const provider = url.searchParams.get("provider")?.trim().toLowerCase() ?? ""; - if (!provider) return renderMirasimProviderSelection(state, providers, locale); - if (!PROVIDER_SLUG.test(provider) || !providers.includes(provider)) { - return browserHtml( - locale, - copy.unsupportedTitle, - `

    ${escapeHtml(copy.unsupportedBody)}

    `, - 400, - ); + if (session.email && session.email !== email) { + return renderMirasimCodeEntry(session, copy.failedBody, 409); + } + try { + const response = await postMirasimAuthJson(session.adminUrl, "/auth/code", { email }, req.signal); + try { await response.body?.cancel(); } catch { /* already closed */ } + session.email = email; + return renderMirasimCodeEntry(session); + } catch { + return renderMirasimEmailEntry(session, copy.unavailableBody, 502); } - - const callbackUrl = new URL(session.callbackPath, `${session.baseUrl}/`); - const authUrl = new URL(`${session.adminUrl}/auth/oauth/${encodeURIComponent(provider)}/login`); - authUrl.searchParams.set("redirect_uri", callbackUrl.toString()); - authUrl.searchParams.set("state", state); - const headers = oauthBrowserHeaders(); - headers.set("Location", authUrl.toString()); - return new Response(null, { status: 302, headers }); } - if (url.searchParams.get("result") === "complete") { - if (session.completed) { - return browserHtml( - locale, - copy.completeTitle, - `

    ${escapeHtml(copy.completeBody)}

    `, - ); + if (action === "verify") { + const email = session.email; + if (!email) return renderMirasimEmailEntry(session, copy.incompleteBody, 400); + let code: string; + try { + code = normalizeLoginCode(form.get("code") ?? ""); + } catch { + return renderMirasimCodeEntry(session, copy.failedBody, 400); } - const tokens = session.pendingTokens; - if (!tokens) { - return browserHtml( - locale, - copy.incompleteTitle, - `

    ${escapeHtml(copy.incompleteBody)}

    `, - 400, + try { + const response = await postMirasimAuthJson( + session.adminUrl, + "/auth/verify", + { email, code }, + req.signal, ); - } - settleMirasimBrowserSession(session, tokens); - return browserHtml( - locale, - copy.completeTitle, - `

    ${escapeHtml(copy.completeBody)}

    `, - ); - } - - try { - const tokens = parseCallbackTokens(url, state); - session.pendingTokens = tokens; - const clean = new URL(session.callbackPath, `${session.baseUrl}/`); - clean.searchParams.set("state", state); - clean.searchParams.set("result", "complete"); - const headers = oauthBrowserHeaders(); - headers.set("Location", clean.toString()); - return new Response(null, { status: 303, headers }); - } catch (error) { - rejectMirasimBrowserSession( - session, - error instanceof Error ? error : new Error("Mirasim OAuth callback failed"), - ); - return browserHtml( - locale, - copy.failedTitle, - `

    ${escapeHtml(copy.failedBody)}

    `, - 400, - ); - } -} - -async function waitForCallback( - ctrl: OAuthController, - expectedState: string, - callbackPath: string, -): Promise<{ callbackUrl: string; tokens: Promise; stop: () => void }> { - let resolveTokens!: (tokens: CallbackTokens) => void; - let rejectTokens!: (error: Error) => void; - const tokens = new Promise((resolve, reject) => { - resolveTokens = resolve; - rejectTokens = reject; - }); - - let pendingTokens: CallbackTokens | undefined; - let completed = false; - const server = Bun.serve({ - hostname: "127.0.0.1", - port: 0, - reusePort: false, - fetch(request) { - const url = new URL(request.url); - const locale = normalizeMirasimBrowserLocale(request.headers.get("accept-language")); - const copy = MIRASIM_BROWSER_COPY[locale]; - if (url.pathname !== callbackPath) return new Response(copy.notFound, { status: 404 }); - - if (url.searchParams.get("result") === "complete") { - if (completed) return new Response(copy.callbackAlreadyUsed, { status: 409 }); - if (!pendingTokens) { - return new Response(copy.failedBody, { - status: 400, - headers: oauthBrowserHeaders("text/plain; charset=utf-8"), - }); - } - completed = true; - const tokens = pendingTokens; - pendingTokens = undefined; - resolveTokens(tokens); - return browserHtml( - locale, - copy.completeTitle, - `

    ${escapeHtml(copy.completeBody)}

    `, - ); + code = ""; + const payload = await boundedJson(response); + let accessToken = typeof payload.access_token === "string" ? payload.access_token : ""; + let refreshToken = typeof payload.refresh_token === "string" ? payload.refresh_token : ""; + if (!accessToken.trim() || !refreshToken.trim()) { + accessToken = ""; + refreshToken = ""; + throw new Error("Mirasim browser sign-in returned no renewable credential"); } - - if (completed || pendingTokens) return new Response(copy.callbackAlreadyUsed, { status: 409 }); - try { - pendingTokens = parseCallbackTokens(url, expectedState); - const clean = new URL(callbackPath, url.origin); - clean.searchParams.set("result", "complete"); - const headers = oauthBrowserHeaders(); - headers.set("Location", clean.toString()); - return new Response(null, { status: 303, headers }); - } catch (error) { - const failure = error instanceof Error ? error : new Error("Mirasim OAuth callback failed"); - rejectTokens(failure); - return new Response(copy.failedBody, { - status: 400, - headers: { "Content-Type": "text/plain; charset=utf-8", "Connection": "close" }, - }); + const tokens = { + accessToken: normalizeCredentialSecret("access token", accessToken), + refreshToken: normalizeCredentialSecret("refresh token", refreshToken), + }; + accessToken = ""; + refreshToken = ""; + settleMirasimBrowserSession(session, tokens); + return browserHtml(locale, copy.completeTitle, `

    ${escapeHtml(copy.completeBody)}

    `); + } catch (error) { + code = ""; + if (error instanceof Error && error.message.includes("no renewable credential")) { + rejectMirasimBrowserSession(session, error); + return browserHtml(locale, copy.failedTitle, `

    ${escapeHtml(copy.failedBody)}

    `, 502); } - }, - }); - - const timeout = setTimeout(() => rejectTokens(new Error("Mirasim OAuth login timed out")), LOGIN_TIMEOUT_MS); - timeout.unref?.(); - const abort = () => rejectTokens(new Error("Mirasim OAuth login cancelled")); - ctrl.signal?.addEventListener("abort", abort, { once: true }); + return renderMirasimCodeEntry(session, copy.failedBody, 400); + } + } - const stop = () => { - clearTimeout(timeout); - ctrl.signal?.removeEventListener("abort", abort); - // Let the active callback response flush before closing the ephemeral listener. - server.stop(false); - }; - return { - callbackUrl: `http://127.0.0.1:${server.port}${callbackPath}`, - tokens: tokens.finally(stop), - stop, - }; + return renderMirasimEmailEntry(session, copy.failedBody, 400); } export async function loginMirasim( @@ -995,54 +848,18 @@ export async function loginMirasim( }); ctrl.onProgress?.(browserCopy.waitingForBrowser); - const browserResult = pending.tokens.then(tokens => ({ source: "browser" as const, tokens })); - const result = ctrl.onManualCodeInput - ? await Promise.race([ - browserResult, - ctrl.onManualCodeInput(state).then(input => ({ - source: "manual" as const, - tokens: parseManualCallback(input, state), - })), - ]) - : await browserResult; - if (result.source === "manual") pending.discard(); + const tokens = await pending.tokens; return enrichMirasimCredentialFromProfile( - credentialsFromTokens(result.tokens.accessToken, result.tokens.refreshToken, mirasim), + credentialsFromTokens(tokens.accessToken, tokens.refreshToken, mirasim), ctrl.signal, ); } - const providers = await discoverLoginProviders(mirasim.adminUrl, ctrl.signal); - const provider = chooseLoginProvider(providers); - const callbackPath = `/mirasim/oauth/${randomBytes(24).toString("base64url")}`; - const pending = await waitForCallback(ctrl, state, callbackPath); - - const authUrl = new URL(`${mirasim.adminUrl}/auth/oauth/${encodeURIComponent(provider)}/login`); - authUrl.searchParams.set("redirect_uri", pending.callbackUrl); - authUrl.searchParams.set("state", state); - ctrl.onAuth?.({ - url: authUrl.toString(), - instructions: browserCopy.continueInstruction( - provider === "github" ? "GitHub" : provider === "google" ? "Google" : provider, - ), - }); - ctrl.onProgress?.(browserCopy.waitingForBrowser); - - try { - const callbackPromise = pending.tokens; - const tokens = ctrl.onManualCodeInput - ? await Promise.race([ - callbackPromise, - ctrl.onManualCodeInput(state).then(input => parseManualCallback(input, state)), - ]) - : await callbackPromise; - return enrichMirasimCredentialFromProfile( - credentialsFromTokens(tokens.accessToken, tokens.refreshToken, mirasim), - ctrl.signal, - ); - } finally { - pending.stop(); + if (!ctrl.onManualCodeInput) { + throw new Error("Mirasim sign-in requires an interactive account email"); } + const email = await ctrl.onManualCodeInput(undefined, "Enter the Mirasim account email: "); + return loginMirasimWithEmail(ctrl, mirasim, { email }); } export async function refreshMirasimToken( diff --git a/tests/providers/mirasim-oauth.test.ts b/tests/providers/mirasim-oauth.test.ts index 587c76608ee..8ae865d985b 100644 --- a/tests/providers/mirasim-oauth.test.ts +++ b/tests/providers/mirasim-oauth.test.ts @@ -55,26 +55,6 @@ function installEmailAuthServer( }) as typeof fetch; } -function installBrowserAuthServer(calls: AuthCall[]): void { - globalThis.fetch = (async (input: string | URL | Request): Promise => { - const url = input instanceof Request ? input.url : input.toString(); - const path = new URL(url).pathname; - calls.push({ path }); - if (path === "/auth/oauth/providers") { - return new Response(JSON.stringify({ providers: ["github", "google"] }), { - status: 200, - headers: { "content-type": "application/json" }, - }); - } - if (path === "/auth/me") { - return new Response(JSON.stringify({ email: "browser@example.com" }), { - status: 200, - headers: { "content-type": "application/json" }, - }); - } - throw new Error(`unexpected Mirasim auth path: ${path}`); - }) as typeof fetch; -} describe("Mirasim OAuth/email login", () => { test("rejects control characters in the runtime protocol version", () => { @@ -187,48 +167,45 @@ describe("Mirasim OAuth/email login", () => { } }); - test("malformed management callback paths terminate inside the OAuth namespace", async () => { + test("legacy management token-callback paths are no longer owned", async () => { const response = await handleMirasimBrowserOAuthRequest(new Request( - "http://127.0.0.1:10100/oauth/mirasim/callback/abc?lang=zh-TW", + "http://127.0.0.1:10100/oauth/mirasim/callback/abc?access_token=must-not-arrive&refresh_token=must-not-arrive", )); - expect(response?.status).toBe(400); - expect(await response!.text()).toContain("Mirasim 登入連結已過期"); + expect(response).toBeNull(); }); - test("CLI loopback callback redirects away from credential-bearing query before completing", async () => { + test("default CLI login uses email verification without creating a browser callback", async () => { const calls: AuthCall[] = []; - installBrowserAuthServer(calls); + installEmailAuthServer(calls); + const prompts: string[] = []; + const answers = ["cli@example.com", "123456"]; let authUrl = ""; - const login = loginMirasim({ + + const credential = await loginMirasim({ onAuth: info => { authUrl = info.url; }, + onManualCodeInput: async (_state, prompt) => { + prompts.push(prompt ?? ""); + return answers.shift() ?? ""; + }, }); - for (let i = 0; i < 100 && !authUrl; i++) await Bun.sleep(1); - expect(authUrl).toBeTruthy(); - - const upstream = new URL(authUrl); - const redirectUri = upstream.searchParams.get("redirect_uri"); - expect(redirectUri).toBeTruthy(); - const callback = new URL(redirectUri!); - callback.searchParams.set("access_token", "cli-browser-access"); - callback.searchParams.set("refresh_token", "cli-browser-refresh"); - - const first = await originalFetch(callback, { redirect: "manual" }); - expect(first.status).toBe(303); - const clean = first.headers.get("location"); - expect(clean).toBeTruthy(); - expect(clean).not.toContain("access_token"); - expect(clean).not.toContain("refresh_token"); - - const completion = await originalFetch(new URL(clean!, callback), { redirect: "manual" }); - expect(completion.status).toBe(200); - const credential = await login; - expect(credential.access).toBe("cli-browser-access"); - expect(credential.refresh).toBe("cli-browser-refresh"); + + expect(authUrl).toBe(""); + expect(prompts).toEqual([ + "Enter the Mirasim account email: ", + "Enter the Mirasim sign-in code: ", + ]); + expect(calls.map(call => call.path)).toEqual(["/auth/code", "/auth/verify", "/auth/me"]); + expect(credential.access).toBeTruthy(); + expect(credential.refresh).toBeTruthy(); }); - test("management browser flow starts on a local provider chooser instead of silently preferring GitHub", async () => { + test("management browser flow exchanges an emailed code server-side without credentials in the URL", async () => { const calls: AuthCall[] = []; - installBrowserAuthServer(calls); + installEmailAuthServer(calls, { + access_token: "browser-access", + refresh_token: "browser-refresh", + expires_in: 1800, + }); let authUrl = ""; const login = loginMirasim({ onAuth: info => { authUrl = info.url; }, @@ -237,84 +214,85 @@ describe("Mirasim OAuth/email login", () => { }); await Promise.resolve(); - expect(new URL(authUrl).origin).toBe("http://127.0.0.1:10100"); - expect(new URL(authUrl).pathname).toBe("/oauth/mirasim/start"); - expect(new URL(authUrl).searchParams.get("lang")).toBe("en"); - expect(authUrl).not.toContain("/auth/oauth/github/login"); - - const chooser = await handleMirasimBrowserOAuthRequest(new Request(authUrl)); - expect(chooser?.status).toBe(200); - const chooserHtml = await chooser!.text(); - expect(chooserHtml).toContain("/provider-icons/mirasim.svg"); - expect(chooserHtml).toContain("Continue with GitHub"); - expect(chooserHtml).toContain("Continue with Google"); - const start = new URL(authUrl); - const state = start.searchParams.get("state"); - expect(state).toBeTruthy(); - start.searchParams.set("provider", "google"); - const redirect = await handleMirasimBrowserOAuthRequest(new Request(start)); - expect(redirect?.status).toBe(302); - const upstream = new URL(redirect!.headers.get("location")!); - expect(upstream.origin).toBe("https://auth.mirasim.ai"); - expect(upstream.pathname).toBe("/auth/oauth/google/login"); - const redirectUri = new URL(upstream.searchParams.get("redirect_uri")!); - expect(redirectUri.origin).toBe("http://127.0.0.1:10100"); - expect(redirectUri.pathname).toMatch(/^\/oauth\/mirasim\/callback\/[A-Za-z0-9_-]{20,}$/); - expect(upstream.searchParams.get("state")).toBe(state); - - // Current Mirasim production omits the separately supplied state on the token callback. - // The unguessable one-use callback path is therefore the channel binding, matching the - // upstream CLI implementation. - const callback = new URL(redirectUri); - callback.searchParams.set("access_token", "browser-access"); - callback.searchParams.set("refresh_token", "browser-refresh"); - const callbackResponse = await handleMirasimBrowserOAuthRequest(new Request(callback)); - expect(callbackResponse?.status).toBe(303); - const clean = callbackResponse!.headers.get("location"); - expect(clean).not.toContain("access_token"); - expect(clean).not.toContain("refresh_token"); - - const completion = await handleMirasimBrowserOAuthRequest(new Request(clean!)); - expect(completion?.status).toBe(200); + expect(start.origin).toBe("http://127.0.0.1:10100"); + expect(start.pathname).toBe("/oauth/mirasim/start"); + expect(start.searchParams.get("lang")).toBe("en"); + expect(authUrl).not.toContain("access_token"); + expect(authUrl).not.toContain("refresh_token"); + expect(authUrl).not.toContain("/auth/oauth/"); + + const entry = await handleMirasimBrowserOAuthRequest(new Request(authUrl)); + expect(entry?.status).toBe(200); + const entryHtml = await entry!.text(); + expect(entryHtml).toContain("/provider-icons/mirasim.svg"); + expect(entryHtml).toContain("Send verification code"); + expect(entryHtml).not.toContain("access_token"); + expect(entryHtml).not.toContain("refresh_token"); + + const send = await handleMirasimBrowserOAuthRequest(new Request(authUrl, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: "action=send&email=browser%40example.com", + })); + expect(send?.status).toBe(200); + expect(await send!.text()).toContain("Verification code"); + + const verify = await handleMirasimBrowserOAuthRequest(new Request(authUrl, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: "action=verify&code=123456", + })); + expect(verify?.status).toBe(200); + const verifyHtml = await verify!.text(); + expect(verifyHtml).toContain("Mirasim sign-in complete"); + expect(verifyHtml).not.toContain("browser-access"); + expect(verifyHtml).not.toContain("browser-refresh"); + const credential = await login; expect(credential.access).toBe("browser-access"); expect(credential.refresh).toBe("browser-refresh"); - expect(credential.email).toBe("browser@example.com"); - expect(calls.map(call => call.path)).toEqual([ - "/auth/oauth/providers", - "/auth/oauth/providers", - "/auth/me", + expect(credential.email).toBe("user@example.com"); + expect(calls).toEqual([ + { path: "/auth/code", body: { email: "browser@example.com" } }, + { path: "/auth/verify", body: { email: "browser@example.com", code: "123456" } }, + { path: "/auth/me" }, ]); + + const replay = await handleMirasimBrowserOAuthRequest(new Request(authUrl, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: "action=verify&code=123456", + })); + expect(replay?.status).toBe(400); + expect(await replay!.text()).toContain("invalid or has expired"); }); test("management browser flow follows English, Traditional Chinese, and Simplified Chinese locale", async () => { - const calls: AuthCall[] = []; - installBrowserAuthServer(calls); const cases = [ { locale: "en", htmlLang: "en", title: "Sign in to Mirasim", - chooser: "Choose the account provider you want to use.", - button: "Continue with GitHub", - instruction: "Choose GitHub or Google on the Mirasim sign-in page.", + chooser: "Enter the email address for your Mirasim account.", + button: "Send verification code", + instruction: "Complete Mirasim sign-in in the browser using the emailed verification code.", }, { locale: "zh-TW", htmlLang: "zh-TW", title: "登入 Mirasim", - chooser: "選擇要用於登入的帳號供應商。", - button: "使用 GitHub 繼續", - instruction: "請在 Mirasim 登入頁面選擇 GitHub 或 Google。", + chooser: "輸入 Mirasim 帳號的電子郵件地址", + button: "傳送驗證碼", + instruction: "請在瀏覽器中使用電子郵件驗證碼完成 Mirasim 登入。", }, { locale: "zh", htmlLang: "zh-CN", title: "登录 Mirasim", - chooser: "选择用于登录的账户提供商。", - button: "使用 GitHub 继续", - instruction: "请在 Mirasim 登录页面选择 GitHub 或 Google。", + chooser: "输入 Mirasim 账户的电子邮件地址", + button: "发送验证码", + instruction: "请在浏览器中使用电子邮件验证码完成 Mirasim 登录。", }, ] as const; @@ -336,13 +314,15 @@ describe("Mirasim OAuth/email login", () => { expect(new URL(authUrl).searchParams.get("lang")).toBe(item.htmlLang); expect(instruction).toBe(item.instruction); - const chooser = await handleMirasimBrowserOAuthRequest(new Request(authUrl)); - expect(chooser?.status).toBe(200); - const html = await chooser!.text(); + const page = await handleMirasimBrowserOAuthRequest(new Request(authUrl)); + expect(page?.status).toBe(200); + const html = await page!.text(); expect(html).toContain(` { }); test("cancelled management browser flow invalidates its start capability", async () => { - const calls: AuthCall[] = []; - installBrowserAuthServer(calls); const abort = new AbortController(); let authUrl = ""; const login = loginMirasim({ @@ -389,9 +367,9 @@ describe("Mirasim OAuth/email login", () => { expect(await expired!.text()).toContain("invalid or has expired"); }); - test("random callback path accepts omitted state but rejects a present mismatched state", async () => { + test("management browser verification refuses a non-renewable auth response without exposing it", async () => { const calls: AuthCall[] = []; - installBrowserAuthServer(calls); + installEmailAuthServer(calls, { access_token: "short-lived-only" }); let authUrl = ""; const login = loginMirasim({ onAuth: info => { authUrl = info.url; }, @@ -400,18 +378,20 @@ describe("Mirasim OAuth/email login", () => { }); await Promise.resolve(); - const start = new URL(authUrl); - start.searchParams.set("provider", "github"); - const redirect = await handleMirasimBrowserOAuthRequest(new Request(start)); - const upstream = new URL(redirect!.headers.get("location")!); - const callback = new URL(upstream.searchParams.get("redirect_uri")!); - callback.searchParams.set("state", "wrong-state"); - callback.searchParams.set("access_token", "must-not-save"); - callback.searchParams.set("refresh_token", "must-not-save"); - - const denied = await handleMirasimBrowserOAuthRequest(new Request(callback)); - expect(denied?.status).toBe(400); - expect(await denied!.text()).toContain("did not match this OpenCodex login attempt"); - await expect(login).rejects.toThrow("state mismatch"); + await handleMirasimBrowserOAuthRequest(new Request(authUrl, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: "action=send&email=user%40example.com", + })); + const denied = await handleMirasimBrowserOAuthRequest(new Request(authUrl, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: "action=verify&code=123456", + })); + expect(denied?.status).toBe(502); + const html = await denied!.text(); + expect(html).not.toContain("short-lived-only"); + await expect(login).rejects.toThrow("no renewable credential"); }); + }); From fd16a13ef08234921005b0b31ca45c3885cd5043 Mon Sep 17 00:00:00 2001 From: letr1n1ty Date: Mon, 28 Sep 2026 20:51:00 +0800 Subject: [PATCH 17/33] fix(mirasim): bound browser verification attempts --- src/oauth/mirasim.ts | 22 +++++++++--- tests/providers/mirasim-oauth.test.ts | 49 +++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/src/oauth/mirasim.ts b/src/oauth/mirasim.ts index 7a02e0aa20f..2764c00eca5 100644 --- a/src/oauth/mirasim.ts +++ b/src/oauth/mirasim.ts @@ -471,6 +471,7 @@ interface CallbackTokens { 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; @@ -479,6 +480,8 @@ interface MirasimBrowserSession { baseUrl: string; expiresAt: number; email?: string; + codeSent: boolean; + verifyAttempts: number; completed: boolean; promise: Promise; resolve: (tokens: CallbackTokens) => void; @@ -546,6 +549,8 @@ function createMirasimBrowserSession( adminUrl, baseUrl, expiresAt: Date.now() + LOGIN_TIMEOUT_MS, + codeSent: false, + verifyAttempts: 0, completed: false, promise, resolve: resolveTokens, @@ -760,16 +765,20 @@ export async function handleMirasimBrowserOAuthRequest( } catch { return renderMirasimEmailEntry(session, copy.failedBody, 400); } - if (session.email && session.email !== email) { - return renderMirasimCodeEntry(session, copy.failedBody, 409); + if (session.codeSent) { + return renderMirasimCodeEntry(session, copy.callbackAlreadyUsed, 409); } + // Spend the one send before the network call so concurrent POSTs cannot turn a leaked + // login capability into a mail relay. A failed send requires starting a fresh login. + session.codeSent = true; + session.email = email; try { const response = await postMirasimAuthJson(session.adminUrl, "/auth/code", { email }, req.signal); try { await response.body?.cancel(); } catch { /* already closed */ } - session.email = email; return renderMirasimCodeEntry(session); } catch { - return renderMirasimEmailEntry(session, copy.unavailableBody, 502); + rejectMirasimBrowserSession(session, new Error("Mirasim browser sign-in code request failed")); + return browserHtml(locale, copy.unavailableTitle, `

    ${escapeHtml(copy.unavailableBody)}

    `, 502); } } @@ -812,6 +821,11 @@ export async function handleMirasimBrowserOAuthRequest( rejectMirasimBrowserSession(session, error); return browserHtml(locale, copy.failedTitle, `

    ${escapeHtml(copy.failedBody)}

    `, 502); } + session.verifyAttempts += 1; + if (session.verifyAttempts >= MIRASIM_BROWSER_MAX_VERIFY_ATTEMPTS) { + rejectMirasimBrowserSession(session, new Error("Mirasim browser sign-in verification attempts exhausted")); + return browserHtml(locale, copy.failedTitle, `

    ${escapeHtml(copy.failedBody)}

    `, 400); + } return renderMirasimCodeEntry(session, copy.failedBody, 400); } } diff --git a/tests/providers/mirasim-oauth.test.ts b/tests/providers/mirasim-oauth.test.ts index 8ae865d985b..6979497b3c0 100644 --- a/tests/providers/mirasim-oauth.test.ts +++ b/tests/providers/mirasim-oauth.test.ts @@ -238,6 +238,14 @@ describe("Mirasim OAuth/email login", () => { expect(send?.status).toBe(200); expect(await send!.text()).toContain("Verification code"); + const duplicateSend = await handleMirasimBrowserOAuthRequest(new Request(authUrl, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: "action=send&email=browser%40example.com", + })); + expect(duplicateSend?.status).toBe(409); + expect(calls.filter(call => call.path === "/auth/code")).toHaveLength(1); + const verify = await handleMirasimBrowserOAuthRequest(new Request(authUrl, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, @@ -367,6 +375,47 @@ describe("Mirasim OAuth/email login", () => { expect(await expired!.text()).toContain("invalid or has expired"); }); + test("management browser verification caps failed code attempts and expires the session", async () => { + const calls: AuthCall[] = []; + globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit): Promise => { + const path = new URL(input instanceof Request ? input.url : input.toString()).pathname; + calls.push({ path }); + if (path === "/auth/code") return new Response(null, { status: 200 }); + if (path === "/auth/verify") return new Response(null, { status: 400 }); + throw new Error(`unexpected Mirasim auth path: ${path}`); + }) as typeof fetch; + + let authUrl = ""; + const login = loginMirasim({ + onAuth: info => { authUrl = info.url; }, + }, { + browserBaseUrl: "http://127.0.0.1:10100", + }); + await Promise.resolve(); + + const send = await handleMirasimBrowserOAuthRequest(new Request(authUrl, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: "action=send&email=user%40example.com", + })); + expect(send?.status).toBe(200); + + for (let attempt = 1; attempt <= 5; attempt += 1) { + const denied = await handleMirasimBrowserOAuthRequest(new Request(authUrl, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: `action=verify&code=${100000 + attempt}`, + })); + expect(denied?.status).toBe(400); + } + expect(calls.filter(call => call.path === "/auth/verify")).toHaveLength(5); + await expect(login).rejects.toThrow("verification attempts exhausted"); + + const expired = await handleMirasimBrowserOAuthRequest(new Request(authUrl)); + expect(expired?.status).toBe(400); + expect(await expired!.text()).toContain("invalid or has expired"); + }); + test("management browser verification refuses a non-renewable auth response without exposing it", async () => { const calls: AuthCall[] = []; installEmailAuthServer(calls, { access_token: "short-lived-only" }); From 55ea957e38a4dcd4272c879a0c22acd394995915 Mon Sep 17 00:00:00 2001 From: letr1n1ty Date: Mon, 28 Sep 2026 20:53:45 +0800 Subject: [PATCH 18/33] refactor(mirasim): keep auth wiring within size gates --- src/oauth/index.ts | 28 +++++----------------------- src/oauth/mirasim.ts | 31 +++++++++++++++++++++++++++++++ src/providers/quota.ts | 3 +-- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/src/oauth/index.ts b/src/oauth/index.ts index 1b28877abc3..835328c8e84 100644 --- a/src/oauth/index.ts +++ b/src/oauth/index.ts @@ -49,10 +49,7 @@ import { loginGithubCopilot, refreshGithubCopilotToken, validateCopilotApiBaseUr import { loginCommandCode, refreshCommandCodeToken } from "./command-code"; import { loginMetaMuse, refreshMetaMuseToken } from "./meta-muse"; import { - loginMirasim, - MirasimTokenRefreshError, - mirasimRelayUrl, - refreshMirasimToken, + loginMirasimForProvider, mirasimOAuthProviderConfig, MirasimTokenRefreshError, mirasimRelayUrl, refreshMirasimToken, } from "./mirasim"; import { loginOrcaRouter, orcaRouterInferenceBaseUrl, refreshOrcaRouterKey } from "./orcarouter"; import { ANTIGRAVITY_REQUEST_UA } from "../adapters/google-antigravity-wire"; @@ -254,26 +251,11 @@ function oauthDefaultModel(id: string): string { export const OAUTH_PROVIDERS: Record = { mirasim: { - login: (ctrl, opts) => loginMirasim(ctrl, { - ...(opts?.mirasimBrowserBaseUrl ? { browserBaseUrl: opts.mirasimBrowserBaseUrl } : {}), - ...(opts?.mirasimBrowserLocale ? { browserLocale: opts.mirasimBrowserLocale } : {}), - ...(opts?.mirasimEmail ? { email: opts.mirasimEmail } : {}), - ...(opts?.mirasimCode ? { code: opts.mirasimCode } : {}), - }), - refresh: (refreshToken, signal, credential) => - refreshMirasimToken(refreshToken, signal, credential), - providerConfig: { - ...oauthConfig("mirasim"), - upstreamHttpVersion: "http1.1", - }, - resolveProviderConfig: () => ({ - ...oauthConfig("mirasim"), - baseUrl: mirasimRelayUrl(), - upstreamHttpVersion: "http1.1", - }), + login: loginMirasimForProvider, + refresh: refreshMirasimToken, + providerConfig: mirasimOAuthProviderConfig(oauthConfig("mirasim")), + resolveProviderConfig: () => mirasimOAuthProviderConfig(oauthConfig("mirasim"), mirasimRelayUrl()), defaultModel: oauthDefaultModel("mirasim"), - // Mirasim refresh tokens are used only when a request needs a fresh bearer. Avoid creating - // unattended auth traffic on behalf of a relay account. defaultRefreshPolicy: "lazy-only", }, "command-code": { diff --git a/src/oauth/mirasim.ts b/src/oauth/mirasim.ts index 2764c00eca5..c7c13a78e68 100644 --- a/src/oauth/mirasim.ts +++ b/src/oauth/mirasim.ts @@ -1,6 +1,7 @@ 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"; @@ -833,6 +834,36 @@ export async function handleMirasimBrowserOAuthRequest( return renderMirasimEmailEntry(session, copy.failedBody, 400); } +export interface MirasimProviderLoginOptions { + mirasimBrowserBaseUrl?: string; + mirasimBrowserLocale?: string; + mirasimEmail?: string; + mirasimCode?: string; +} + +export function loginMirasimForProvider( + ctrl: OAuthController, + opts?: MirasimProviderLoginOptions, +): Promise { + return loginMirasim(ctrl, { + ...(opts?.mirasimBrowserBaseUrl ? { browserBaseUrl: opts.mirasimBrowserBaseUrl } : {}), + ...(opts?.mirasimBrowserLocale ? { browserLocale: opts.mirasimBrowserLocale } : {}), + ...(opts?.mirasimEmail ? { email: opts.mirasimEmail } : {}), + ...(opts?.mirasimCode ? { code: opts.mirasimCode } : {}), + }); +} + +export function mirasimOAuthProviderConfig( + base: OcxProviderConfig, + relayUrl?: string, +): OcxProviderConfig { + return { + ...base, + ...(relayUrl ? { baseUrl: relayUrl } : {}), + upstreamHttpVersion: "http1.1", + }; +} + export async function loginMirasim( ctrl: OAuthController, options: MirasimLoginOptions = {}, diff --git a/src/providers/quota.ts b/src/providers/quota.ts index 69a254309f1..9a3fb629b03 100644 --- a/src/providers/quota.ts +++ b/src/providers/quota.ts @@ -355,8 +355,7 @@ async function readExplicitAccountQuota(provider: string, accountId: string, con if (!credential || credential.access !== accessToken) return null; // Pair the post-renewal credential with the destination captured before renewal. const identity = explicitQuotaIdentity(provider, accountId, config); - const isCurrent = () => epoch === explicitAccountEpoch - && identity === explicitQuotaIdentity(provider, accountId, configured); + const isCurrent = () => epoch === explicitAccountEpoch && identity === explicitQuotaIdentity(provider, accountId, configured); if (!isCurrent()) return null; let result: ProviderQuotaProbeResult; switch (provider) { From 4a7bc92324d85ee7a5794741e6326ff1c09be84d Mon Sep 17 00:00:00 2001 From: letr1n1ty Date: Tue, 29 Sep 2026 01:32:23 +0800 Subject: [PATCH 19/33] fix(mirasim): reserve browser verify attempts before await --- src/oauth/mirasim.ts | 35 +++++++- tests/providers/mirasim-oauth.test.ts | 125 ++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 3 deletions(-) diff --git a/src/oauth/mirasim.ts b/src/oauth/mirasim.ts index c7c13a78e68..d26777ff039 100644 --- a/src/oauth/mirasim.ts +++ b/src/oauth/mirasim.ts @@ -483,6 +483,7 @@ interface MirasimBrowserSession { email?: string; codeSent: boolean; verifyAttempts: number; + verifyInFlight: number; completed: boolean; promise: Promise; resolve: (tokens: CallbackTokens) => void; @@ -552,6 +553,7 @@ function createMirasimBrowserSession( expiresAt: Date.now() + LOGIN_TIMEOUT_MS, codeSent: false, verifyAttempts: 0, + verifyInFlight: 0, completed: false, promise, resolve: resolveTokens, @@ -591,6 +593,19 @@ function currentMirasimBrowserSession(state: string): MirasimBrowserSession | un 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", @@ -792,6 +807,12 @@ export async function handleMirasimBrowserOAuthRequest( } catch { return renderMirasimCodeEntry(session, copy.failedBody, 400); } + if (!reserveMirasimBrowserVerifyAttempt(session)) { + const active = mirasimBrowserSessions.get(session.state) === session && !session.completed; + return active + ? renderMirasimCodeEntry(session, copy.failedBody, 400) + : browserHtml(locale, copy.expiredTitle, `

    ${escapeHtml(copy.expiredBody)}

    `, 400); + } try { const response = await postMirasimAuthJson( session.adminUrl, @@ -814,19 +835,27 @@ export async function handleMirasimBrowserOAuthRequest( }; accessToken = ""; refreshToken = ""; + finishMirasimBrowserVerifyAttempt(session); settleMirasimBrowserSession(session, tokens); return browserHtml(locale, copy.completeTitle, `

    ${escapeHtml(copy.completeBody)}

    `); } catch (error) { code = ""; + const active = finishMirasimBrowserVerifyAttempt(session); if (error instanceof Error && error.message.includes("no renewable credential")) { - rejectMirasimBrowserSession(session, error); + if (active) rejectMirasimBrowserSession(session, error); return browserHtml(locale, copy.failedTitle, `

    ${escapeHtml(copy.failedBody)}

    `, 502); } - session.verifyAttempts += 1; - if (session.verifyAttempts >= MIRASIM_BROWSER_MAX_VERIFY_ATTEMPTS) { + if ( + active + && session.verifyAttempts >= MIRASIM_BROWSER_MAX_VERIFY_ATTEMPTS + && session.verifyInFlight === 0 + ) { rejectMirasimBrowserSession(session, new Error("Mirasim browser sign-in verification attempts exhausted")); return browserHtml(locale, copy.failedTitle, `

    ${escapeHtml(copy.failedBody)}

    `, 400); } + if (!active) { + return browserHtml(locale, copy.expiredTitle, `

    ${escapeHtml(copy.expiredBody)}

    `, 400); + } return renderMirasimCodeEntry(session, copy.failedBody, 400); } } diff --git a/tests/providers/mirasim-oauth.test.ts b/tests/providers/mirasim-oauth.test.ts index 6979497b3c0..bbda7e1adf3 100644 --- a/tests/providers/mirasim-oauth.test.ts +++ b/tests/providers/mirasim-oauth.test.ts @@ -416,6 +416,131 @@ describe("Mirasim OAuth/email login", () => { expect(await expired!.text()).toContain("invalid or has expired"); }); + test("management browser verification reserves the five-attempt budget before concurrent upstream calls", async () => { + let verifyCalls = 0; + let releaseVerify!: () => void; + const verifyGate = new Promise(resolve => { releaseVerify = resolve; }); + globalThis.fetch = (async (input: string | URL | Request): Promise => { + const path = new URL(input instanceof Request ? input.url : input.toString()).pathname; + if (path === "/auth/code") return new Response(null, { status: 200 }); + if (path === "/auth/verify") { + verifyCalls += 1; + await verifyGate; + return new Response(null, { status: 400 }); + } + throw new Error(`unexpected Mirasim auth path: ${path}`); + }) as typeof fetch; + + let authUrl = ""; + const login = loginMirasim({ + onAuth: info => { authUrl = info.url; }, + }, { + browserBaseUrl: "http://127.0.0.1:10100", + }); + await Promise.resolve(); + + const send = await handleMirasimBrowserOAuthRequest(new Request(authUrl, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: "action=send&email=user%40example.com", + })); + expect(send?.status).toBe(200); + + let locallySettled = 0; + const attempts = Array.from({ length: 8 }, (_, index) => + handleMirasimBrowserOAuthRequest(new Request(authUrl, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: `action=verify&code=${200000 + index}`, + })).finally(() => { locallySettled += 1; }), + ); + + for (let spin = 0; spin < 1_000 && verifyCalls + locallySettled < attempts.length; spin += 1) { + await Bun.sleep(1); + } + const callsBeforeRelease = verifyCalls; + releaseVerify(); + + const responses = await Promise.all(attempts); + expect(responses.every(response => response?.status === 400)).toBe(true); + await expect(login).rejects.toThrow("verification attempts exhausted"); + expect(callsBeforeRelease).toBe(5); + expect(verifyCalls).toBe(5); + + const expired = await handleMirasimBrowserOAuthRequest(new Request(authUrl)); + expect(expired?.status).toBe(400); + expect(await expired!.text()).toContain("invalid or has expired"); + }); + + test("management browser verification waits for a reserved late success before exhausting the session", async () => { + let verifyCalls = 0; + let releaseFailures!: () => void; + let releaseSuccess!: () => void; + const failureGate = new Promise(resolve => { releaseFailures = resolve; }); + const successGate = new Promise(resolve => { releaseSuccess = resolve; }); + globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit): Promise => { + const path = new URL(input instanceof Request ? input.url : input.toString()).pathname; + if (path === "/auth/code") return new Response(null, { status: 200 }); + if (path === "/auth/verify") { + verifyCalls += 1; + const payload = JSON.parse(String(init?.body ?? "{}")) as { code?: string }; + if (payload.code === "300004") { + await successGate; + return Response.json({ + access_token: "late-success-access", + refresh_token: "late-success-refresh", + expires_in: 1800, + }); + } + await failureGate; + return new Response(null, { status: 400 }); + } + if (path === "/auth/me") { + return Response.json({ email: "user@example.com" }); + } + throw new Error(`unexpected Mirasim auth path: ${path}`); + }) as typeof fetch; + + let authUrl = ""; + let loginSettled = false; + const login = loginMirasim({ + onAuth: info => { authUrl = info.url; }, + }, { + browserBaseUrl: "http://127.0.0.1:10100", + }).finally(() => { loginSettled = true; }); + await Promise.resolve(); + + await handleMirasimBrowserOAuthRequest(new Request(authUrl, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: "action=send&email=user%40example.com", + })); + + const attempts = Array.from({ length: 5 }, (_, index) => + handleMirasimBrowserOAuthRequest(new Request(authUrl, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: `action=verify&code=${300000 + index}`, + })), + ); + for (let spin = 0; spin < 1_000 && verifyCalls < attempts.length; spin += 1) { + await Bun.sleep(1); + } + expect(verifyCalls).toBe(5); + + releaseFailures(); + const failed = await Promise.all(attempts.slice(0, 4)); + expect(failed.every(response => response?.status === 400)).toBe(true); + await Promise.resolve(); + expect(loginSettled).toBe(false); + + releaseSuccess(); + expect((await attempts[4])?.status).toBe(200); + const credential = await login; + expect(credential.access).toBe("late-success-access"); + expect(credential.refresh).toBe("late-success-refresh"); + }); + test("management browser verification refuses a non-renewable auth response without exposing it", async () => { const calls: AuthCall[] = []; installEmailAuthServer(calls, { access_token: "short-lived-only" }); From 4bfd8aba147671d7617fe83fee99b323973ea6dc Mon Sep 17 00:00:00 2001 From: letr1n1ty Date: Tue, 29 Sep 2026 01:35:24 +0800 Subject: [PATCH 20/33] refactor(mirasim): keep provider routes within size gate --- src/server/management/provider-routes.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/server/management/provider-routes.ts b/src/server/management/provider-routes.ts index 8543bd45bf8..55990eec27c 100644 --- a/src/server/management/provider-routes.ts +++ b/src/server/management/provider-routes.ts @@ -1712,10 +1712,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise Date: Tue, 29 Sep 2026 04:28:07 +0800 Subject: [PATCH 21/33] fix(gui): keep Mirasim locale and native icon gates in sync --- desktop/src-tauri/src/provider_icons.rs | 2 ++ gui/tests/fr-localization.test.ts | 7 +++++++ gui/tests/locale-parity.test.ts | 7 +++++++ 3 files changed, 16 insertions(+) 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/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) From 312798fdd91d0f0e27acb423eee397b009ffe4b4 Mon Sep 17 00:00:00 2001 From: letr1n1ty Date: Wed, 30 Sep 2026 10:33:16 +0800 Subject: [PATCH 22/33] fix(mirasim): migrate legacy signing metadata before quota probe --- src/oauth/index.ts | 9 +++---- src/oauth/mirasim.ts | 4 ++++ src/providers/quota/account-cache.ts | 7 +++++- tests/providers/mirasim-recovery.test.ts | 30 ++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/oauth/index.ts b/src/oauth/index.ts index 835328c8e84..e0bc8106dce 100644 --- a/src/oauth/index.ts +++ b/src/oauth/index.ts @@ -48,9 +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 { - loginMirasimForProvider, mirasimOAuthProviderConfig, MirasimTokenRefreshError, mirasimRelayUrl, refreshMirasimToken, -} from "./mirasim"; +import { loginMirasimForProvider, mirasimCredentialNeedsMigration, mirasimOAuthProviderConfig, MirasimTokenRefreshError, mirasimRelayUrl, refreshMirasimToken } from "./mirasim"; import { loginOrcaRouter, orcaRouterInferenceBaseUrl, refreshOrcaRouterKey } from "./orcarouter"; import { ANTIGRAVITY_REQUEST_UA } from "../adapters/google-antigravity-wire"; import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive"; @@ -626,7 +624,10 @@ 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); diff --git a/src/oauth/mirasim.ts b/src/oauth/mirasim.ts index d26777ff039..ebadbd7288b 100644 --- a/src/oauth/mirasim.ts +++ b/src/oauth/mirasim.ts @@ -205,6 +205,10 @@ export function mirasimAdminUrl(): string { ); } +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)) { diff --git a/src/providers/quota/account-cache.ts b/src/providers/quota/account-cache.ts index 423d10686f2..83730b0102c 100644 --- a/src/providers/quota/account-cache.ts +++ b/src/providers/quota/account-cache.ts @@ -2,6 +2,7 @@ import { parseAnthropicFamilyHeaders, mergeAnthropicFamilyWindows } from "./anth import { anthropicModelQuotaFor, ANTHROPIC_PASSIVE_FAMILY_MAX_AGE_MS } from "../../oauth/anthropic-model-quota"; import { createHash } from "node:crypto"; import { getValidAccessTokenForAccount } from "../../oauth"; +import { mirasimCredentialNeedsMigration } from "../../oauth/mirasim"; import { credentialGeneration, getAccountCredential, getAccountCredentialWithStatus, getAccountSet } from "../../oauth/store"; import { isAnthropicInstanceId, type AnthropicInstanceId } from "../anthropic-instance-id"; import type { GenerationContext } from "../../lib/state-store-sweeper"; @@ -503,7 +504,11 @@ export async function getTokenForAccountQuotaProbe(provider: string, accountId: if (row.paused) throw new Error("account is paused; quota probe skipped"); if (row.needsReauth && isAnthropicInstanceId(provider)) throw new Error("account needs sign-in; quota probe skipped"); const stored = row.credential; - if (stored.expires > Date.now() + ACCOUNT_TOKEN_SKEW_MS) return stored.access; + // A fresh legacy Mirasim bearer is still unusable by the signed control plane when its + // persisted device metadata is missing. Let the OAuth resolver refresh once so it can + // migrate the credential instead of returning a token that fetchMirasimControl cannot sign. + const requiresCredentialMigration = provider === "mirasim" && mirasimCredentialNeedsMigration(stored); + if (!requiresCredentialMigration && stored.expires > Date.now() + ACCOUNT_TOKEN_SKEW_MS) return stored.access; const activeId = getAccountSet(provider)?.activeAccountId; if (activeId !== accountId && stored.source === "local-cli") { throw new Error("background local-cli token expired; skip CLI-adopting refresh for quota probe"); diff --git a/tests/providers/mirasim-recovery.test.ts b/tests/providers/mirasim-recovery.test.ts index d7f72e72b10..849b501cd54 100644 --- a/tests/providers/mirasim-recovery.test.ts +++ b/tests/providers/mirasim-recovery.test.ts @@ -11,6 +11,7 @@ import { getValidAccessTokenSnapshot } from "../../src/oauth"; import { getAccountSet, saveCredential } from "../../src/oauth/store"; import { providerConfigSeed } from "../../src/providers/derive"; import { getProviderRegistryEntry } from "../../src/providers/registry"; +import { getTokenForAccountQuotaProbe } from "../../src/providers/quota/account-cache"; import { handleResponses } from "../../src/server/responses/core"; import { handleResponsesCompact } from "../../src/server/responses/compact"; import type { OcxConfig, OcxProviderConfig } from "../../src/types"; @@ -77,6 +78,35 @@ function mirasimConfig(fakeFetch: typeof fetch): OcxConfig { } describe("Mirasim OAuth recovery", () => { + test("fresh legacy credential migrates missing Mirasim device metadata", async () => { + const seeded = credential(); + const legacy = { ...seeded, mirasim: undefined }; + await saveCredential("mirasim", legacy); + + let refreshCalls = 0; + globalThis.fetch = (async () => { + refreshCalls += 1; + return new Response(JSON.stringify({ + access_token: "migrated-access", + refresh_token: "migrated-refresh", + expires_in: 1800, + }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + }) as typeof fetch; + + const accountId = getAccountSet("mirasim")?.accounts[0]?.id; + expect(accountId).toBeTruthy(); + const accessToken = await getTokenForAccountQuotaProbe("mirasim", accountId!); + const stored = getAccountSet("mirasim")?.accounts[0]?.credential; + expect(accessToken).toBe("migrated-access"); + expect(refreshCalls).toBe(1); + expect(stored?.mirasim?.devicePrivateKey).toBeTruthy(); + expect(stored?.mirasim?.relayUrl).toBe("https://relay.mirasim.ai"); + expect(stored?.mirasim?.adminUrl).toBe("https://auth.mirasim.ai"); + }); + test("terminal invalid_grant marks the exact account for reauthentication and is not retried forever", async () => { await saveCredential("mirasim", credential({ expires: Date.now() - 1_000 })); From 8f0b74ff5bc6bcc2818f6784566e9d9dccdabf76 Mon Sep 17 00:00:00 2001 From: letr1n1ty Date: Sat, 3 Oct 2026 01:42:00 +0800 Subject: [PATCH 23/33] fix(mirasim): keep GPT-5.6 Sol exec native --- src/adapters/mirasim.ts | 12 +- tests/providers/mirasim-endpoints.test.ts | 86 ++++++++++ tests/providers/mirasim-provider.test.ts | 198 +++++++++++----------- 3 files changed, 196 insertions(+), 100 deletions(-) diff --git a/src/adapters/mirasim.ts b/src/adapters/mirasim.ts index 03a47f06fad..ad93bf61e26 100644 --- a/src/adapters/mirasim.ts +++ b/src/adapters/mirasim.ts @@ -18,7 +18,11 @@ import { type MirasimWire = "anthropic" | "responses"; const RESPONSE_WIRE_HEADER = "x-opencodex-mirasim-response-wire"; -const MIRASIM_GPT6_NATIVE_CUSTOM_TOOLS = new Set(["exec"]); +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(); @@ -241,13 +245,13 @@ export function createMirasimAdapter(provider: OcxProviderConfig): ProviderAdapt upstreamWebsocket: false, }; const responses = createResponsesPassthroughAdapter(responsesProvider); - const gpt6Responses = createResponsesPassthroughAdapter(responsesProvider, { - routedCustomToolPassthroughNames: MIRASIM_GPT6_NATIVE_CUSTOM_TOOLS, + const nativeExecResponses = createResponsesPassthroughAdapter(responsesProvider, { + routedCustomToolPassthroughNames: MIRASIM_NATIVE_EXEC_TOOLS, }); const parser = (wire: MirasimWire, modelId?: string): ProviderAdapter => { if (wire === "anthropic") return anthropic; - return modelId && /^gpt-6(?:-|$)/i.test(modelId) ? gpt6Responses : responses; + return modelId && usesNativeMirasimExec(modelId) ? nativeExecResponses : responses; }; return { diff --git a/tests/providers/mirasim-endpoints.test.ts b/tests/providers/mirasim-endpoints.test.ts index 11d8326d8f8..64b46803dc0 100644 --- a/tests/providers/mirasim-endpoints.test.ts +++ b/tests/providers/mirasim-endpoints.test.ts @@ -197,6 +197,92 @@ describe("Mirasim auxiliary inference endpoints", () => { } }); + test("GPT-5.6 Sol code-mode exec stays native custom through the relay", async () => { + await saveCredential("mirasim", syntheticCredential()); + const calls: Captured[] = []; + const config = mirasimConfig(captureFetch(calls, call => { + if (call.path !== "/v1/responses") return new Response("not found", { status: 404 }); + const tool = Array.isArray(call.body?.tools) + ? call.body.tools[0] as Record | undefined + : undefined; + expect(tool).toMatchObject({ type: "custom", name: "exec" }); + const item = { + id: "ctc_mirasim_exec_56", + type: "custom_tool_call", + call_id: "call_mirasim_exec_56", + name: "exec", + input: "text(\"ok\")", + status: "completed", + }; + const added = { + type: "response.output_item.added", + output_index: 0, + item: { ...item, input: "", status: "in_progress" }, + }; + const done = { type: "response.output_item.done", output_index: 0, item }; + const terminal = { + type: "response.completed", + response: { + id: "resp_mirasim_exec_56", + object: "response", + created_at: 1, + status: "completed", + model: "gpt-5.6-sol", + output: [item], + usage: { input_tokens: 4, output_tokens: 1, total_tokens: 5 }, + }, + }; + return new Response([ + `data: ${JSON.stringify(added)}\n\n`, + `data: ${JSON.stringify(done)}\n\n`, + `data: ${JSON.stringify(terminal)}\n\n`, + "data: [DONE]\n\n", + ].join(""), { + status: 200, + headers: { "content-type": "text/event-stream" }, + }); + })); + const releaseSpendHome = acquireOwnedSpendHome(); + try { + const response = await handleResponses(new Request("http://127.0.0.1/v1/responses", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: "mirasim/gpt-5.6-sol", + stream: true, + input: [{ + type: "message", + role: "user", + content: [{ type: "input_text", text: "Run pwd." }], + }], + tools: [{ + type: "namespace", + name: "functions", + tools: [{ + type: "custom", + name: "exec", + description: "Run JavaScript", + format: { type: "grammar", syntax: "lark" }, + }], + }], + tool_choice: "auto", + }), + }), config, { model: "", provider: "" }, { abortSignal: AbortSignal.timeout(5_000) }); + + expect(response.status).toBe(200); + const inference = calls.find(call => call.path === "/v1/responses"); + const tools = inference?.body?.tools as Array> | undefined; + expect(tools?.[0]).toMatchObject({ type: "custom", name: "exec" }); + const sse = await response.text(); + expect(sse).toContain('"type":"custom_tool_call"'); + expect(sse).toContain('"name":"exec"'); + expect(sse).toContain('"input":"text(\\\"ok\\\")"'); + expect(sse).not.toContain('"type":"function_call","name":"exec"'); + } finally { + releaseSpendHome(); + } + }); + test("non-stream GPT caller receives bounded JSON even though Mirasim forces upstream Responses SSE", async () => { await saveCredential("mirasim", syntheticCredential()); const calls: Captured[] = []; diff --git a/tests/providers/mirasim-provider.test.ts b/tests/providers/mirasim-provider.test.ts index 8057b797852..4fc25bea958 100644 --- a/tests/providers/mirasim-provider.test.ts +++ b/tests/providers/mirasim-provider.test.ts @@ -386,104 +386,110 @@ describe("Mirasim provider", () => { expect(request.headers["x-opencodex-mirasim-wire"]).toBe("responses"); }); - test("keeps GPT-6 code-mode exec native on the Mirasim Responses wire", async () => { - const mirasim = adapter(); - const exec = { - type: "custom", - name: "exec", - description: "Run JavaScript", - format: { type: "grammar", syntax: "lark" }, - }; - const request = await mirasim.buildRequest({ - modelId: "gpt-6-astra", - stream: true, - options: { reasoning: "high" }, - context: { - messages: [], - tools: [{ - name: "exec", - description: "Run JavaScript", - parameters: { type: "object", properties: { input: { type: "string" } }, required: ["input"] }, - freeform: true, - }], - }, - _rawBody: { - model: "gpt-6-astra", - input: "run pwd", - tools: [{ type: "namespace", name: "functions", tools: [exec] }], - tool_choice: "auto", - }, - }); - const body = JSON.parse(request.body) as { tools?: Array> }; - - expect(body.tools).toEqual([exec]); - expect([...(request.convertedRoutedCustomToolNames ?? [])]).not.toContain("exec"); - }); + test.each(["gpt-5.6-sol", "gpt-6-astra"])( + "keeps %s code-mode exec native on the Mirasim Responses wire", + async modelId => { + const mirasim = adapter(); + const exec = { + type: "custom", + name: "exec", + description: "Run JavaScript", + format: { type: "grammar", syntax: "lark" }, + }; + const request = await mirasim.buildRequest({ + modelId, + stream: true, + options: { reasoning: "high" }, + context: { + messages: [], + tools: [{ + name: "exec", + description: "Run JavaScript", + parameters: { type: "object", properties: { input: { type: "string" } }, required: ["input"] }, + freeform: true, + }], + }, + _rawBody: { + model: modelId, + input: "run pwd", + tools: [{ type: "namespace", name: "functions", tools: [exec] }], + tool_choice: "auto", + }, + }); + const body = JSON.parse(request.body) as { tools?: Array> }; - test("keeps GPT-6 native exec semantics across continuation history and tool_choice", async () => { - const exec = { - type: "custom", - name: "exec", - description: "Run JavaScript", - format: { type: "grammar", syntax: "lark" }, - }; - const request = await adapter().buildRequest({ - modelId: "gpt-6-astra", - stream: true, - options: {}, - context: { - messages: [], - tools: [{ - name: "exec", - description: "Run JavaScript", - parameters: { type: "object", properties: { input: { type: "string" } }, required: ["input"] }, - freeform: true, - }], - }, - _rawBody: { - model: "gpt-6-astra", - input: [ - { - type: "custom_tool_call", - id: "ctc_previous_exec", - call_id: "call_previous_exec", + expect(body.tools).toEqual([exec]); + expect([...(request.convertedRoutedCustomToolNames ?? [])]).not.toContain("exec"); + }, + ); + + test.each(["gpt-5.6-sol", "gpt-6-astra"])( + "keeps %s native exec semantics across continuation history and tool_choice", + async modelId => { + const exec = { + type: "custom", + name: "exec", + description: "Run JavaScript", + format: { type: "grammar", syntax: "lark" }, + }; + const request = await adapter().buildRequest({ + modelId, + stream: true, + options: {}, + context: { + messages: [], + tools: [{ name: "exec", - input: 'text("before")', - status: "completed", - }, - { - type: "custom_tool_call_output", - call_id: "call_previous_exec", - output: "ok", - }, - { type: "message", role: "user", content: [{ type: "input_text", text: "continue" }] }, - ], - tools: [{ type: "namespace", name: "functions", tools: [exec] }], - tool_choice: { type: "custom", namespace: "functions", name: "exec" }, - }, - }); - const body = JSON.parse(request.body) as { - input?: Array>; - tools?: Array>; - tool_choice?: Record; - }; + description: "Run JavaScript", + parameters: { type: "object", properties: { input: { type: "string" } }, required: ["input"] }, + freeform: true, + }], + }, + _rawBody: { + model: modelId, + input: [ + { + type: "custom_tool_call", + id: "ctc_previous_exec", + call_id: "call_previous_exec", + name: "exec", + input: 'text("before")', + status: "completed", + }, + { + type: "custom_tool_call_output", + call_id: "call_previous_exec", + output: "ok", + }, + { type: "message", role: "user", content: [{ type: "input_text", text: "continue" }] }, + ], + tools: [{ type: "namespace", name: "functions", tools: [exec] }], + tool_choice: { type: "custom", namespace: "functions", name: "exec" }, + }, + }); + const body = JSON.parse(request.body) as { + input?: Array>; + tools?: Array>; + tool_choice?: Record; + }; - expect(body.tools).toEqual([exec]); - expect(body.tool_choice).toEqual({ type: "custom", name: "exec" }); - expect(body.input?.[0]).toMatchObject({ - type: "custom_tool_call", - id: "ctc_previous_exec", - call_id: "call_previous_exec", - name: "exec", - input: 'text("before")', - }); - expect(body.input?.[1]).toEqual({ - type: "custom_tool_call_output", - call_id: "call_previous_exec", - output: "ok", - }); - expect([...(request.convertedRoutedCustomToolNames ?? [])]).not.toContain("exec"); - }); + expect(body.tools).toEqual([exec]); + expect(body.tool_choice).toEqual({ type: "custom", name: "exec" }); + expect(body.input?.[0]).toMatchObject({ + type: "custom_tool_call", + id: "ctc_previous_exec", + call_id: "call_previous_exec", + name: "exec", + input: 'text("before")', + }); + expect(body.input?.[1]).toEqual({ + type: "custom_tool_call_output", + call_id: "call_previous_exec", + output: "ok", + }); + expect([...(request.convertedRoutedCustomToolNames ?? [])]).not.toContain("exec"); + }, + ); test("keeps the shared custom-tool fallback for pre-GPT-6 and non-exec tools", async () => { const build = async (modelId: string, name: string) => { @@ -513,7 +519,7 @@ describe("Mirasim provider", () => { }; }; - const legacyExec = await build("gpt-5.6-sol", "exec"); + const legacyExec = await build("gpt-5.6-luna", "exec"); expect(legacyExec.body.tools?.[0]).toMatchObject({ type: "function", name: "exec" }); expect(legacyExec.converted).toContain("exec"); From 87edb30d9320eb1de5b474473fc691df5309a8db Mon Sep 17 00:00:00 2001 From: letr1n1ty Date: Sat, 3 Oct 2026 01:49:00 +0800 Subject: [PATCH 24/33] refactor(mirasim): keep OAuth wiring within size gate --- src/oauth/index.ts | 25 +++---------------------- src/oauth/mirasim.ts | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/oauth/index.ts b/src/oauth/index.ts index e0bc8106dce..239d2952bdf 100644 --- a/src/oauth/index.ts +++ b/src/oauth/index.ts @@ -48,7 +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 { loginMirasimForProvider, mirasimCredentialNeedsMigration, mirasimOAuthProviderConfig, MirasimTokenRefreshError, mirasimRelayUrl, refreshMirasimToken } from "./mirasim"; +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"; @@ -183,22 +183,10 @@ function verdictKey(p:string,a:string,c:OAuthCredentials){return `${p}\0${a}\0${ function cached(p:string,a:string,c:OAuthCredentials,now:()=>number){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; - /** - * Management-owned browser origin for Mirasim OAuth. - * The GUI supplies this so Mirasim can return to the long-lived OpenCodex server - * instead of a random ephemeral loopback listener. - */ - mirasimBrowserBaseUrl?: string; - /** Dashboard/browser locale forwarded to the Mirasim management-owned OAuth pages. */ - mirasimBrowserLocale?: string; - /** Mirasim CLI-only email-code login. Browser/GUI login leaves these unset. */ - mirasimEmail?: string; - /** Optional already-received Mirasim email verification code. */ - mirasimCode?: string; /** * ChatGPT only: `device` selects the deviceauth grant instead of the * localhost:1455 callback flow, for hosts with no browser or no loopback @@ -248,14 +236,7 @@ function oauthDefaultModel(id: string): string { } export const OAUTH_PROVIDERS: Record = { - mirasim: { - login: loginMirasimForProvider, - refresh: refreshMirasimToken, - providerConfig: mirasimOAuthProviderConfig(oauthConfig("mirasim")), - resolveProviderConfig: () => mirasimOAuthProviderConfig(oauthConfig("mirasim"), mirasimRelayUrl()), - defaultModel: oauthDefaultModel("mirasim"), - defaultRefreshPolicy: "lazy-only", - }, + 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" }), diff --git a/src/oauth/mirasim.ts b/src/oauth/mirasim.ts index ebadbd7288b..fdeaf3561c2 100644 --- a/src/oauth/mirasim.ts +++ b/src/oauth/mirasim.ts @@ -897,6 +897,20 @@ export function mirasimOAuthProviderConfig( }; } +export function mirasimOAuthProviderDefinition( + resolveBaseConfig: () => OcxProviderConfig, + defaultModel: string, +) { + return { + login: loginMirasimForProvider, + refresh: refreshMirasimToken, + providerConfig: mirasimOAuthProviderConfig(resolveBaseConfig()), + resolveProviderConfig: () => mirasimOAuthProviderConfig(resolveBaseConfig(), mirasimRelayUrl()), + defaultModel, + defaultRefreshPolicy: "lazy-only" as const, + }; +} + export async function loginMirasim( ctrl: OAuthController, options: MirasimLoginOptions = {}, From af6c6bcbf2711da23dfeff913cd8f066bfd4ec47 Mon Sep 17 00:00:00 2001 From: letr1n1ty Date: Sat, 3 Oct 2026 03:33:22 +0800 Subject: [PATCH 25/33] fix(mirasim): recover direct computer-use tool calls --- src/responses/custom-tool-compat.ts | 68 ++++++++++-- src/server/responses-custom-tool-repair.ts | 46 ++++++-- src/server/responses-undeclared-tool-guard.ts | 40 +++++++ src/server/responses/passthrough-delivery.ts | 6 +- src/server/responses/passthrough-dispatch.ts | 26 ++++- structure/transports/responses-wire-shapes.md | 6 +- tests/providers/mirasim-endpoints.test.ts | 104 ++++++++++++++++++ .../responses-code-mode-mcp-direct.test.ts | 87 +++++++++++++++ 8 files changed, 359 insertions(+), 24 deletions(-) diff --git a/src/responses/custom-tool-compat.ts b/src/responses/custom-tool-compat.ts index d25ffe7bd63..36f817f77f2 100644 --- a/src/responses/custom-tool-compat.ts +++ b/src/responses/custom-tool-compat.ts @@ -1,4 +1,9 @@ -import { namespacedToolName, normalizeDeclaredToolName } from "../types"; +import { + CODE_MODE_EXEC_TOOL_NAME, + isCodeModeMcpDirectName, + namespacedToolName, + normalizeDeclaredToolName, +} from "../types"; import { normalizeApplyPatchDelimiters, repairFreeformToolInput, @@ -73,11 +78,34 @@ export function routedCustomToolTargetName( value: unknown, names: ReadonlySet, declaredNames?: ReadonlySet, + directMcpRecoveryNames?: ReadonlySet, ): string | undefined { const wireName = routedCustomToolWireName(value); if (wireName === undefined) return undefined; if (names.has(wireName)) return wireName; - if (!isPlainObject(value) || typeof value.namespace === "string") return undefined; + if (!isPlainObject(value)) return undefined; + + const directName = wireName.startsWith("default.") + ? wireName.slice("default.".length) + : wireName; + if ( + directMcpRecoveryNames?.has(CODE_MODE_EXEC_TOOL_NAME) === true + && isCodeModeMcpDirectName(directName) + ) { + const directTarget = normalizeDeclaredToolName( + wireName, + declaredNames, + undefined, + directMcpRecoveryNames, + ); + if (directTarget !== wireName && directMcpRecoveryNames.has(directTarget)) { + return directTarget; + } + } + + // Ordinary namespaced identities keep their namespace. Only the direct-MCP recovery above may + // cross that boundary, and only with a request-proven bare custom exec. + if (typeof value.namespace === "string") return undefined; const normalized = normalizeDeclaredToolName(wireName, declaredNames, undefined, names); return normalized !== wireName && names.has(normalized) ? normalized : undefined; } @@ -476,13 +504,19 @@ export function restoreRoutedCustomCalls( names: ReadonlySet, repairNames: ReadonlySet = new Set(), declaredNames?: ReadonlySet, + directMcpRecoveryNames?: ReadonlySet, ): { value: unknown; changed: boolean } { if (!isPlainObject(value)) return { value, changed: false }; const restoreItem = (item: unknown): { value: unknown; changed: boolean } => { if (!isPlainObject(item)) return { value: item, changed: false }; const wireName = routedCustomToolWireName(item); - const targetName = routedCustomToolTargetName(item, names, declaredNames); + const targetName = routedCustomToolTargetName( + item, + names, + declaredNames, + directMcpRecoveryNames, + ); if ( (item.type === "function_call" || item.type === "custom_tool_call") && typeof item.name === "string" @@ -492,10 +526,17 @@ export function restoreRoutedCustomCalls( const sourceInput = item.type === "function_call" ? item.arguments : item.input; const aliased = targetName !== wireName; const itemNamespace = typeof item.namespace === "string" ? item.namespace : undefined; + // A namespace restore may have already split a flattened direct-MCP identity into + // {namespace, name}. When that call is recovered through code-mode exec, the nested host + // helper is the full wire identity, not only the local child name ("js"). Ordinary aliases + // keep their historical local-name behavior. + const aliasHelperName = aliased && isCodeModeMcpDirectName(wireName) + ? wireName + : String(item.name); // Name-based alias first; otherwise let a raw patch envelope submitted as the `exec` // body resolve to the same apply_patch helper (devlog/_plan/260905_apply_patch_envelope_gap). const helper = aliased && sourceInput !== "" - ? item.name + ? aliasHelperName : resolveCodeModeHelperName(undefined, targetName, sourceInput, itemNamespace, declaredNames); // Native custom input is already the tool's raw grammar. Only a recognized // helper/envelope may reinterpret it; a JSON-looking native body is not a wrapper. @@ -513,7 +554,7 @@ export function restoreRoutedCustomCalls( id: customToolItemId(item.id), name: aliased ? targetName : item.name, input: helper - ? compileCodeModeHelperInput(sourceInput, helper, aliased ? String(item.name) : targetName) + ? compileCodeModeHelperInput(sourceInput, helper, aliased ? aliasHelperName : targetName) : repairFreeformToolInput( sourceInput, targetName, @@ -572,7 +613,13 @@ export function restoreRoutedCustomCalls( && value.type.startsWith("response.") && isPlainObject(value.response) ) { - const response = restoreRoutedCustomCalls(value.response, names, repairNames, declaredNames); + const response = restoreRoutedCustomCalls( + value.response, + names, + repairNames, + declaredNames, + directMcpRecoveryNames, + ); if (response.changed) { restored.response = response.value; changed = true; @@ -587,6 +634,7 @@ export function restoreRoutedCustomCallsInJson( names: ReadonlySet, repairNames: ReadonlySet = new Set(), declaredNames?: ReadonlySet, + directMcpRecoveryNames?: ReadonlySet, ): string { if (names.size === 0 && repairNames.size === 0) return text; let payload: unknown; @@ -595,7 +643,13 @@ export function restoreRoutedCustomCallsInJson( } catch { return text; } - const restored = restoreRoutedCustomCalls(payload, names, repairNames, declaredNames); + const restored = restoreRoutedCustomCalls( + payload, + names, + repairNames, + declaredNames, + directMcpRecoveryNames, + ); return restored.changed ? JSON.stringify(restored.value) : text; } diff --git a/src/server/responses-custom-tool-repair.ts b/src/server/responses-custom-tool-repair.ts index 137a28f9b59..74540e1c1a8 100644 --- a/src/server/responses-custom-tool-repair.ts +++ b/src/server/responses-custom-tool-repair.ts @@ -3,7 +3,7 @@ import { mayBecomePatchEnvelope, normalizeApplyPatchDelimiters } from "../respon import { compileCodeModeHelperInput, resolveCodeModeHelperName } from "../responses/code-mode-helper-compat"; import { mayBecomeCodeModeShellInput } from "../responses/code-mode-shell-input"; import { progressiveFreeformInput } from "../responses/progressive-freeform-input"; -import { declaresCodeModeExec } from "../types/tools"; +import { declaresCodeModeExec, isCodeModeMcpDirectName } from "../types/tools"; import { customToolItemId, restoreRoutedCustomCalls, @@ -53,8 +53,9 @@ export function createRoutedCustomToolRestoreBlockRewrite( budget?: TranslatorBudget, repairNames: ReadonlySet = new Set(), declaredNames?: ReadonlySet, + directMcpRecoveryNames?: ReadonlySet, ): SseBlockRewrite { - const itemNames = new Map(); + const itemNames = new Map(); // Native helper aliases and genuine bare code-mode exec calls share completion repair. const customExecItemNames = new Map(); const repairItemNames = new Map(); @@ -156,7 +157,12 @@ export function createRoutedCustomToolRestoreBlockRewrite( ) { const upstreamItemId = typeof parsed.item.id === "string" ? parsed.item.id : undefined; const wireName = routedCustomToolWireName(parsed.item); - const targetName = routedCustomToolTargetName(parsed.item, names, declaredNames); + const targetName = routedCustomToolTargetName( + parsed.item, + names, + declaredNames, + directMcpRecoveryNames, + ); const aliased = targetName !== undefined && targetName !== wireName; const codeModeExec = targetName === "exec" && parsed.item.name === "exec" && parsed.item.namespace === undefined && declaresCodeModeExec(declaredNames); @@ -169,7 +175,13 @@ export function createRoutedCustomToolRestoreBlockRewrite( const repairable = wireName !== undefined && repairNames.has(wireName); if (upstreamItemId && repairable) repairItemNames.set(upstreamItemId, parsed.item.name); const restored = repairable || aliased || codeModeExec - ? restoreRoutedCustomCalls(parsed, names, repairNames, declaredNames) + ? restoreRoutedCustomCalls( + parsed, + names, + repairNames, + declaredNames, + directMcpRecoveryNames, + ) : { value: parsed, changed: false }; if (type === "response.output_item.done" && upstreamItemId) releaseCall(upstreamItemId); return restored.changed @@ -183,13 +195,19 @@ export function createRoutedCustomToolRestoreBlockRewrite( && typeof parsed.item.name === "string" ) { const upstreamItemId = typeof parsed.item.id === "string" ? parsed.item.id : undefined; - const targetName = routedCustomToolTargetName(parsed.item, names, declaredNames); + const targetName = routedCustomToolTargetName( + parsed.item, + names, + declaredNames, + directMcpRecoveryNames, + ); const routed = targetName !== undefined; const wireName = routedCustomToolWireName(parsed.item); if (upstreamItemId) { if (routed) { itemNames.set(upstreamItemId, { name: parsed.item.name, + wireName: wireName!, aliased: targetName !== wireName, ...(typeof parsed.item.namespace === "string" ? { namespace: parsed.item.namespace } : {}), }); @@ -209,7 +227,13 @@ export function createRoutedCustomToolRestoreBlockRewrite( if (upstreamItemId && pending.length > 0 && !openCalls.has(upstreamItemId)) { openCalls.set(upstreamItemId, { argumentsText: "", emittedInput: "", retainedBytes: 0 }); } - const restored = restoreRoutedCustomCalls(parsed, names, repairNames, declaredNames); + const restored = restoreRoutedCustomCalls( + parsed, + names, + repairNames, + declaredNames, + directMcpRecoveryNames, + ); const restoredBlock = restored.changed ? replaceSseDataPayload(block, JSON.stringify(restored.value)) : block; @@ -349,7 +373,7 @@ export function createRoutedCustomToolRestoreBlockRewrite( // Name-based alias first; otherwise a raw patch envelope submitted as the `exec` body // resolves to the same apply_patch helper (devlog/_plan/260905_apply_patch_envelope_gap). const helper = itemName?.aliased - ? itemName.name + ? (isCodeModeMcpDirectName(itemName.wireName) ? itemName.wireName : itemName.name) : resolveCodeModeHelperName(undefined, itemName?.name ?? "", source, itemName?.namespace, declaredNames); const next = { ...rest, @@ -362,7 +386,13 @@ export function createRoutedCustomToolRestoreBlockRewrite( return [replaceSseDataPayload(replaceSseEventName(block, nextType), JSON.stringify(next))]; } - const restored = restoreRoutedCustomCalls(parsed, names, repairNames, declaredNames); + const restored = restoreRoutedCustomCalls( + parsed, + names, + repairNames, + declaredNames, + directMcpRecoveryNames, + ); const terminal = type === "response.completed" || type === "response.failed" || type === "response.incomplete"; if (terminal) releaseAll(); return restored.changed diff --git a/src/server/responses-undeclared-tool-guard.ts b/src/server/responses-undeclared-tool-guard.ts index 905454044af..02da6cf36d4 100644 --- a/src/server/responses-undeclared-tool-guard.ts +++ b/src/server/responses-undeclared-tool-guard.ts @@ -255,6 +255,46 @@ export function collectDeclaredBareCustomWireToolNames(body: unknown): Set { + const names = new Set(); + if (!isPlainObject(body) || !namespace || namespace === BUILTIN_FUNCTIONS_NAMESPACE) return names; + const specGroups: unknown[] = [body.tools]; + if (Array.isArray(body.input)) { + for (const item of body.input) { + if ( + isPlainObject(item) + && (item.type === "additional_tools" || item.type === "tool_search_output") + ) specGroups.push(item.tools); + } + } + for (const specs of specGroups) { + if (!Array.isArray(specs)) continue; + for (const spec of specs) { + if ( + !isPlainObject(spec) + || spec.type !== "namespace" + || spec.name !== namespace + || !Array.isArray(spec.tools) + ) continue; + for (const inner of spec.tools) { + if (!isPlainObject(inner)) continue; + const name = wireToolInnerName(inner); + if (name) names.add(namespacedToolName(namespace, name)); + } + } + } + return names; +} + function addNamelessClientCallTypes(callTypes: Set, specs: unknown): void { if (!Array.isArray(specs)) return; for (const spec of specs) { diff --git a/src/server/responses/passthrough-delivery.ts b/src/server/responses/passthrough-delivery.ts index e91ced38c1e..cb8062591bf 100644 --- a/src/server/responses/passthrough-delivery.ts +++ b/src/server/responses/passthrough-delivery.ts @@ -551,7 +551,9 @@ export async function deliverPassthroughResponse( && isCanonicalOpenAiForwardProvider(route.provider); const originalContentType = upstreamResponse.headers.get("content-type"); if (isUsageDebugEnabled() && originalContentType) logCtx.usageDebugContentType = originalContentType; - if (responseEffects.plaintextV2AgentMessageToolNames.size > 0 + const shouldProbeResponsesSse = responseEffects.plaintextV2AgentMessageToolNames.size > 0 + || route.provider.adapter === "mirasim"; + if (shouldProbeResponsesSse && upstreamResponse.ok && upstreamResponse.body && (parsed.stream || canonicalBufferedJson) && !originalContentType?.toLowerCase().includes("text/event-stream") && !originalContentType?.toLowerCase().includes("application/json") @@ -940,6 +942,7 @@ export async function deliverPassthroughResponse( translatorBudget, routedCustomToolRepairNames, declaredWireToolNames, + recoverableBareCustomWireToolNames, ) : undefined, routedToolSearchNames.size > 0 @@ -1426,6 +1429,7 @@ export async function deliverPassthroughResponse( routedCustomToolNames, routedCustomToolRepairNames, declaredWireToolNames, + recoverableBareCustomWireToolNames, ); const restoredToolSearch = restoreRoutedToolSearchCallsInJson( restored, diff --git a/src/server/responses/passthrough-dispatch.ts b/src/server/responses/passthrough-dispatch.ts index f3aa2bea555..bd9c00e2b10 100644 --- a/src/server/responses/passthrough-dispatch.ts +++ b/src/server/responses/passthrough-dispatch.ts @@ -25,6 +25,7 @@ import { collectDeclaredWireToolNames, collectDeclaredBareWireToolNames, collectDeclaredBareCustomWireToolNames, + collectDeclaredNamespacedWireToolNames, collectDeclaredNamelessClientCallTypes, collectProviderExecutedCallTypes, undeclaredToolCallName, @@ -406,11 +407,17 @@ export async function preparePassthroughExchange( ) routedCustomToolRepairNames.add(name); } } - // Only grant direct MCP recovery when this delivery actually restores converted custom - // calls. Native forward and injection paths have no such rewrite. + // Grant direct MCP recovery only for a caller-declared bare custom tool that this delivery + // can faithfully expose again. Converted custom tools use routedCustomToolNames; native + // custom tools use the representation-repair set. const recoverableBareCustomWireToolNames = new Set( - [...clientDeclaredBareCustomWireToolNames].filter(name => routedCustomToolNames.has(name)), + [...clientDeclaredBareCustomWireToolNames].filter( + name => routedCustomToolNames.has(name) || routedCustomToolRepairNames.has(name), + ), ); + const nestedMcpThroughExecWireNames = recoverableBareCustomWireToolNames.has(CODE_MODE_EXEC_TOOL_NAME) + ? collectDeclaredNamespacedWireToolNames(clientToolAuthorizationBody, "mcp__cua_repl") + : new Set(); for (const name of request.convertedRoutedToolSearchNames ?? []) { // The adapter already keeps this set empty when tool_choice forbids the private search. // Its wire name may be collision-aliased, so comparing it to the caller-facing name here @@ -500,13 +507,15 @@ export async function preparePassthroughExchange( declaredBareWireToolNames.clear(); if (replayedInputPrefixLength === 0) { for (const name of collectDeclaredWireToolNames(outboundRequestBody)) { - declaredWireToolNames.add(name); + if (!nestedMcpThroughExecWireNames.has(name)) declaredWireToolNames.add(name); } for (const name of collectDeclaredBareWireToolNames(outboundRequestBody)) { declaredBareWireToolNames.add(name); } } - for (const name of clientDeclaredWireToolNames) declaredWireToolNames.add(name); + for (const name of clientDeclaredWireToolNames) { + if (!nestedMcpThroughExecWireNames.has(name)) declaredWireToolNames.add(name); + } for (const name of clientDeclaredBareWireToolNames) declaredBareWireToolNames.add(name); declaredNamelessClientCallTypes.clear(); if (replayedInputPrefixLength === 0) { @@ -523,6 +532,12 @@ export async function preparePassthroughExchange( // current-turn wire snapshot above may authorize a call. if (replayedInputPrefixLength === 0) { for (const name of toolBridgeMaps.declaredToolNames) { + // Code-mode-only nested MCP helpers remain model-visible in the bridge catalog, but + // they are not client-dispatchable wire tools. The current caller snapshot above + // already classified these names for recovery through bare custom `exec`; adding + // them back here would undo that boundary and make normalizeDeclaredToolName preserve + // the provider's direct MCP call instead of compiling it into `exec`. + if (nestedMcpThroughExecWireNames.has(name)) continue; // `buildToolBridgeMaps` also aliases a namespaced tool under its bare name when the // caller's `tool_choice` selected it unambiguously, which the bridge needs to route the // call back. For `exec` alone that alias would also switch on nested-helper @@ -637,6 +652,7 @@ export async function preparePassthroughExchange( routedCustomToolNames, routedCustomToolRepairNames, declaredWireToolNames, + recoverableBareCustomWireToolNames, ).value; const normalizedResponse = (functionRepairSchemas.size > 0 ? JSON.parse(normalizeFunctionCompletionJson(JSON.stringify(restored))) diff --git a/structure/transports/responses-wire-shapes.md b/structure/transports/responses-wire-shapes.md index 5d5b476f0cc..f9254cf7436 100644 --- a/structure/transports/responses-wire-shapes.md +++ b/structure/transports/responses-wire-shapes.md @@ -31,9 +31,9 @@ The same applies to a provider-added `default.` prefix when neither explicit `de The request must carry verified custom-tool provenance: an ordinary JSON function named `exec` does not authorize this repair. Explicitly declared MCP tools keep their identity, legacy shell catalogs stay unchanged, and unknown nested tools fail at the host. -Names and arguments are serialized as data; plain-text tool-call transcripts are never -promoted into executable calls by this rule. Native forwarding and injection lack this -restoration step, so their undeclared-tool guard still rejects a direct MCP call. +Names and arguments are serialized as data; plain-text tool-call transcripts are never promoted into executable calls. +Native custom tools may use the same recovery only when the current request proves a genuine bare custom code-mode `exec` +and the adapter marks that wire identity for representation repair; native forwarding or injection without it is rejected. Per-wire request and stream shapes on the Responses data plane: mixed-wire model defaults, xAI agent-message continuation, declared-tool membership by inbound wire, and passthrough SSE stream diff --git a/tests/providers/mirasim-endpoints.test.ts b/tests/providers/mirasim-endpoints.test.ts index 64b46803dc0..b7d5a3ecdba 100644 --- a/tests/providers/mirasim-endpoints.test.ts +++ b/tests/providers/mirasim-endpoints.test.ts @@ -283,6 +283,110 @@ describe("Mirasim auxiliary inference endpoints", () => { } }); + test("GPT-5.6 Sol recovers direct cua_repl when the relay labels Responses SSE as text/plain", async () => { + await saveCredential("mirasim", syntheticCredential()); + const calls: Captured[] = []; + const config = mirasimConfig(captureFetch(calls, call => { + if (call.path !== "/v1/responses") return new Response("not found", { status: 404 }); + const tool = Array.isArray(call.body?.tools) + ? call.body.tools[0] as Record | undefined + : undefined; + expect(tool).toMatchObject({ type: "custom", name: "exec" }); + const item = { + id: "fc_mirasim_cua_56", + type: "function_call", + call_id: "call_mirasim_cua_56", + name: "mcp__cua_repl__js", + arguments: '{"code":"1+1"}', + status: "completed", + }; + const added = { + type: "response.output_item.added", + output_index: 0, + item: { ...item, arguments: "", status: "in_progress" }, + }; + const argumentsDone = { + type: "response.function_call_arguments.done", + output_index: 0, + item_id: item.id, + arguments: item.arguments, + }; + const done = { type: "response.output_item.done", output_index: 0, item }; + const terminal = { + type: "response.completed", + response: { + id: "resp_mirasim_cua_56", + object: "response", + created_at: 1, + status: "completed", + model: "gpt-5.6-sol", + output: [item], + usage: { input_tokens: 4, output_tokens: 1, total_tokens: 5 }, + }, + }; + return new Response([ + `data: ${JSON.stringify(added)}\n\n`, + `data: ${JSON.stringify(argumentsDone)}\n\n`, + `data: ${JSON.stringify(done)}\n\n`, + `data: ${JSON.stringify(terminal)}\n\n`, + "data: [DONE]\n\n", + ].join(""), { + status: 200, + // Live Mirasim relay may return Responses SSE bytes under text/plain. + // The passthrough must classify the bounded prefix before tool-call rewrites. + headers: { "content-type": "text/plain; charset=utf-8" }, + }); + })); + const releaseSpendHome = acquireOwnedSpendHome(); + try { + const response = await handleResponses(new Request("http://127.0.0.1/v1/responses", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + model: "mirasim/gpt-5.6-sol", + stream: true, + input: [{ + type: "message", + role: "user", + content: [{ type: "input_text", text: "Use Computer Use." }], + }], + tools: [ + { + type: "namespace", + name: "functions", + tools: [{ + type: "custom", + name: "exec", + description: "Run JavaScript", + format: { type: "grammar", syntax: "lark" }, + }], + }, + { + type: "namespace", + name: "mcp__cua_repl", + tools: [ + { type: "function", name: "js", parameters: { type: "object" } }, + { type: "function", name: "js_reset", parameters: { type: "object" } }, + ], + }, + ], + tool_choice: "auto", + }), + }), config, { model: "", provider: "" }, { abortSignal: AbortSignal.timeout(5_000) }); + + expect(response.status).toBe(200); + const sse = await response.text(); + expect(sse).toContain('"type":"custom_tool_call"'); + expect(sse).toContain('"name":"exec"'); + expect(sse).toContain( + 'const result = await tools.mcp__cua_repl__js({\\\"code\\\":\\\"1+1\\\"});\\ntext(result);', + ); + expect(sse).not.toContain('"type":"function_call","name":"mcp__cua_repl__js"'); + } finally { + releaseSpendHome(); + } + }); + test("non-stream GPT caller receives bounded JSON even though Mirasim forces upstream Responses SSE", async () => { await saveCredential("mirasim", syntheticCredential()); const calls: Captured[] = []; diff --git a/tests/responses/responses-code-mode-mcp-direct.test.ts b/tests/responses/responses-code-mode-mcp-direct.test.ts index d1d0cb34f87..72771b49d6b 100644 --- a/tests/responses/responses-code-mode-mcp-direct.test.ts +++ b/tests/responses/responses-code-mode-mcp-direct.test.ts @@ -16,6 +16,7 @@ import { dataPayload, frame } from "../helpers/custom-tool-repair-fixtures"; const CODE_MODE = new Set(["exec"]); const MCP_NAME = "mcp__codex_app__get_usage_limits"; +const CUA_MCP_NAME = "mcp__cua_repl__js"; const CALL = { type: "function_call", id: "fc_mcp", call_id: "call_mcp", name: MCP_NAME, arguments: "{}" }; const CUSTOM_EXEC = { type: "namespace", name: "functions", tools: [{ type: "custom", name: "exec", format: { type: "text" } }] }; const FUNCTION_EXEC = { type: "namespace", name: "functions", tools: [{ type: "function", name: "exec", parameters: { type: "object" } }] }; @@ -169,6 +170,92 @@ describe("code-mode direct mcp tool-call recovery", () => { expect(undeclaredToolCallNameInResponse(restored, CODE_MODE)).toBeUndefined(); }); + test("restores a direct cua_repl call when exec stays native custom", () => { + const native = rewriteRoutedCustomToolsForUpstream( + { tools: [CUSTOM_EXEC] }, + undefined, + CODE_MODE, + ); + expect(native.names).toEqual(new Set()); + expect(native.repairNames).toEqual(CODE_MODE); + + const source = { + output: [{ + type: "function_call", + id: "fc_cua", + call_id: "call_cua", + name: CUA_MCP_NAME, + arguments: '{"code":"1+1"}', + }], + }; + const withoutRecovery = restoreRoutedCustomCallsInJson( + JSON.stringify(source), + native.names, + native.repairNames, + CODE_MODE, + ); + expect(JSON.parse(withoutRecovery).output[0]).toMatchObject({ + type: "function_call", + name: CUA_MCP_NAME, + }); + + const restored = JSON.parse(restoreRoutedCustomCallsInJson( + JSON.stringify(source), + native.names, + native.repairNames, + CODE_MODE, + CODE_MODE, + )); + expect(restored.output[0]).toMatchObject({ + type: "custom_tool_call", + name: "exec", + call_id: "call_cua", + input: 'const result = await tools.mcp__cua_repl__js({"code":"1+1"});\ntext(result);', + }); + }); + + test("native custom exec SSE recovery compiles direct cua_repl into exec", () => { + const native = rewriteRoutedCustomToolsForUpstream( + { tools: [CUSTOM_EXEC] }, + undefined, + CODE_MODE, + ); + const rewrite = createRoutedCustomToolRestoreBlockRewrite( + native.names, + undefined, + native.repairNames, + CODE_MODE, + CODE_MODE, + ); + const added = rewrite(frame("response.output_item.added", { + output_index: 0, + item: { + type: "function_call", + id: "fc_cua", + call_id: "call_cua", + name: CUA_MCP_NAME, + arguments: "", + status: "in_progress", + }, + })); + expect(dataPayload(added[0]!).item).toMatchObject({ + type: "custom_tool_call", + name: "exec", + call_id: "call_cua", + }); + const done = rewrite(frame("response.function_call_arguments.done", { + output_index: 0, + item_id: "fc_cua", + arguments: '{"code":"1+1"}', + })); + expect(dataPayload(done[0]!)).toMatchObject({ + type: "response.custom_tool_call_input.done", + item_id: "ctc_cua", + input: 'const result = await tools.mcp__cua_repl__js({"code":"1+1"});\ntext(result);', + }); + rewrite.dispose?.(); + }); + test("native SSE restoration compiles only a converted custom exec call", () => { for (const name of [MCP_NAME, `default.${MCP_NAME}`]) { const customNames = rewriteRoutedCustomToolsForUpstream({ tools: [CUSTOM_EXEC] }, false).names; From 5b6babfed46bbf13878c5ee945d70e3ab36d195e Mon Sep 17 00:00:00 2001 From: letr1n1ty Date: Mon, 5 Oct 2026 01:57:40 +0800 Subject: [PATCH 26/33] fix(mirasim): honor model discovery rate limits --- gui/src/i18n/de.ts | 2 + gui/src/i18n/en.ts | 2 + gui/src/i18n/fr.ts | 2 + gui/src/i18n/ja.ts | 2 + gui/src/i18n/ko.ts | 2 + gui/src/i18n/ru.ts | 2 + gui/src/i18n/tr.ts | 2 + gui/src/i18n/vi.ts | 2 + gui/src/i18n/zh-TW.ts | 2 + gui/src/i18n/zh.ts | 2 + gui/src/pages/Models.tsx | 3 +- gui/src/pages/models-provider-hints.tsx | 4 +- gui/src/pages/models-shared.ts | 13 +++- gui/tests/models-empty-provider.test.tsx | 9 +++ src/adapters/mirasim/control-plane.ts | 15 ++++- src/codex/catalog/provider-models.ts | 16 ++++- src/codex/model-cache.ts | 16 ++++- tests/codex-integration/model-cache.test.ts | 21 ++++++ tests/providers/mirasim-control-plane.test.ts | 67 ++++++++++++++++++- 19 files changed, 172 insertions(+), 12 deletions(-) diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 4390416a768..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.", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 5f7e820db2c..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.", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 336bb3aa79c..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.", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index f336842bcc3..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": "ネットワークエラーによりモデル検出に失敗しました。", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 88a2d8eac85..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": "네트워크 오류로 모델 검색에 실패했습니다.", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index a284c7a8c40..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": "Обнаружение моделей не удалось из-за сетевой ошибки.", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index bd77b5efbc1..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.", diff --git a/gui/src/i18n/vi.ts b/gui/src/i18n/vi.ts index 90ff8b3ac32..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.", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 2bbeb1bb545..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": "由於網路錯誤,模型發現失敗。", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 38af6b0dac7..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": "由于网络错误,模型发现失败。", 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({