From 370f112e1a92c5bce7fea177bb60256077c637fa Mon Sep 17 00:00:00 2001 From: letr1n1ty Date: Mon, 21 Sep 2026 15:31:37 +0800 Subject: [PATCH 01/13] 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 | 7 + src/providers/quota/account-cache.ts | 17 +- 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 | 17 +- src/server/management/provider-routes.ts | 21 + src/server/responses/compact.ts | 50 +- src/server/responses/core.ts | 3 +- src/server/responses/passthrough-delivery.ts | 124 ++- src/server/responses/passthrough-dispatch.ts | 126 ++- 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, 5490 insertions(+), 67 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 69cf9f23230..ce9fbf1f03a 100644 --- a/src/adapters/base.ts +++ b/src/adapters/base.ts @@ -49,6 +49,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. @@ -115,6 +122,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 c8c43748402..5494039e475 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"; @@ -136,6 +137,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 9df115fa24b..c6a5b935481 100644 --- a/src/cli/dispatch.ts +++ b/src/cli/dispatch.ts @@ -367,7 +367,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 23a9696850c..e719680dd21 100644 --- a/src/codex/catalog/provider-models.ts +++ b/src/codex/catalog/provider-models.ts @@ -57,6 +57,8 @@ import { fetchQoderModels } from "../../adapters/qoder/live-models"; import { resolveQoderProfile } from "../../adapters/qoder/profiles"; 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 { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers"; import { COMBO_NAMESPACE, @@ -103,7 +105,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"; @@ -245,6 +247,102 @@ export async function fetchProviderModelsWithAuth( ? [...models, vertexDefaultSeed] : models ); + 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 84d76a6d520..7f77da0336a 100644 --- a/src/oauth/index.ts +++ b/src/oauth/index.ts @@ -43,6 +43,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"; @@ -171,6 +172,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 @@ -218,6 +231,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" }), @@ -616,6 +653,7 @@ export async function getValidAccessTokenSnapshot(provider: string): Promise\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 @@ -129,13 +161,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 { const login = deps.runLogin ?? runLogin; const launch = deps.openUrl ?? openUrl; const browser = createBrowserLaunchReport(deps.warn); @@ -150,13 +189,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 a21f8d84a45..6cdb4e1cc22 100644 --- a/src/oauth/store.ts +++ b/src/oauth/store.ts @@ -581,6 +581,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; } @@ -1195,7 +1211,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.addedAt !== undefined ? { addedAt: account.addedAt } : {}), diff --git a/src/oauth/types.ts b/src/oauth/types.ts index d3369fe238a..1f79957dbc8 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; @@ -73,6 +86,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). */ @@ -106,7 +121,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 5c6124f29df..ae521f60425 100644 --- a/src/providers/openai-tiers-destination.ts +++ b/src/providers/openai-tiers-destination.ts @@ -66,6 +66,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; @@ -98,5 +101,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 ee2fbf02acf..0d8a9a83f74 100644 --- a/src/providers/quota.ts +++ b/src/providers/quota.ts @@ -28,6 +28,7 @@ import { isProviderQuotaReportCurrent, LAST_GOOD_MAX_AGE_MS, providerQuotaBeforePublishForTests, + report, routingEvidence, setProviderQuotaReportCache, TERMINAL_QUOTA_FAILURE, @@ -69,6 +70,7 @@ 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 { fetchMirasimQuota } from "../adapters/mirasim/control-plane"; export type { ProviderQuota, ProviderQuotaCreditsUsd, ProviderQuotaWindow } from "./quota-types"; export { QUOTA_RESPONSE_MAX_BYTES } from "./quota-wire"; @@ -363,6 +365,11 @@ async function readExplicitAccountQuota(provider: string, accountId: string, con case "cursor": result = await fetchCursorQuota(provider, accessToken); break; case "kimi": result = await fetchKimiQuota(provider, config, accessToken); break; case "command-code": result = await fetchCommandCodeQuota(provider, config, accessToken); 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 6e490c3846d..8409ad602d8 100644 --- a/src/providers/quota/account-cache.ts +++ b/src/providers/quota/account-cache.ts @@ -180,7 +180,8 @@ export function supportsPerAccountQuota(provider: string): boolean { } export function explicitAccountReader(provider: string): boolean { - return provider === "xai" || provider === "cursor" || provider === "kimi" || provider === "command-code"; + return provider === "xai" || provider === "cursor" || provider === "kimi" + || provider === "command-code" || provider === "mirasim"; } export function providerOAuthAccountQuotaMode(provider: string): AccountQuotaMode { @@ -464,6 +465,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, ])).digest("hex"); } @@ -472,6 +476,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. return provider === "xai" || provider === "cursor"; } diff --git a/src/providers/registry/entries-extended.ts b/src/providers/registry/entries-extended.ts index 11f7dc51cf2..6d514423ba3 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, @@ -132,6 +139,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 33e8f36d776..0577791320a 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, @@ -45,6 +47,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, @@ -81,6 +84,12 @@ import { resolveApiSurfaceSettings, resolveProtocolSettings } from "../protocols import { markProtocolBlocked, markProtocolEntry } from "../protocols/trace"; import { recordProtocolShadowPlan } from "../protocols/shadow-plan"; import { nativeMessagesDeclineReason, type NativeMessagesSelector } from "./messages-native-eligibility"; +import { providerFetch } from "./responses/fetch-helpers"; +import { + forceRefreshOAuthAccessSnapshot, + getValidAccessTokenSnapshot, + publicOAuthAuthenticationErrorMessage, +} from "../oauth"; import { isApiAuthRequired, isDataPlaneAdmissionSecret, @@ -1494,11 +1503,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; @@ -1522,6 +1646,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) { @@ -1546,7 +1671,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 97a7c674b70..79c8408a12f 100644 --- a/src/server/index/serve-options.ts +++ b/src/server/index/serve-options.ts @@ -657,6 +657,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, @@ -1509,7 +1515,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 1f0259f7e65..d44e37136e9 100644 --- a/src/server/management/oauth-account-routes.ts +++ b/src/server/management/oauth-account-routes.ts @@ -216,7 +216,14 @@ 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 }; + const body = await readManagementJsonBodyOr(req, {}) as { + provider?: string; + addAccount?: boolean; + accountId?: string; + reauth?: boolean; + openBrowser?: 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 @@ -244,6 +251,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 a0dc1a61633..0de69bbefa4 100644 --- a/src/server/management/provider-routes.ts +++ b/src/server/management/provider-routes.ts @@ -1713,6 +1713,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) { @@ -909,7 +928,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, @@ -1013,6 +1034,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 635d5e37c17..cd66de81dd8 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -27,6 +27,7 @@ import { createAdapterContinuations } from "./adapter-continuation"; import { deliverAdapterResponse } from "./adapter-delivery"; import { releaseUpstreamHostAdmission } from "../../codex/upstream-host-health"; import { releaseCodexAuthContextProbeLease } from "../../codex/auth-context"; +import { adapterIsPassthrough } from "../../adapters/base"; /** Public Responses entry and compatibility exports. Implementations live with their owners. */ @@ -111,7 +112,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) { return await executePassthroughResponse( requestContext, admissionState, diff --git a/src/server/responses/passthrough-delivery.ts b/src/server/responses/passthrough-delivery.ts index 366c6c00ddc..6fb63e0c6e1 100644 --- a/src/server/responses/passthrough-delivery.ts +++ b/src/server/responses/passthrough-delivery.ts @@ -1,4 +1,5 @@ import { isNativeControlResponse } from "./native-response-control"; +import { Buffer } from "node:buffer"; import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options"; import type { PreparedResponsesRequest } from "./request-prepare"; import type { ResponsesTransport } from "./request-transport"; @@ -135,6 +136,102 @@ import { inspectResponseLogJson } from "../request-log"; import { restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat"; import { restoreRoutedToolSearchCallsInJson } from "../../responses/tool-search-compat"; import { responsesJsonToSseStream } from "../responses-json-events"; +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; @@ -360,7 +457,7 @@ export async function deliverPassthroughResponse( }); } - const headers = sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions); + let headers = sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions); const resolvedModel = headers.get("openai-model")?.trim(); if (resolvedModel) { logCtx.servedModel = resolvedModel; @@ -369,8 +466,31 @@ 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); + if ( + 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 41bdbfe931d..53c11fd4963 100644 --- a/src/server/responses/passthrough-dispatch.ts +++ b/src/server/responses/passthrough-dispatch.ts @@ -53,7 +53,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"; @@ -161,6 +161,7 @@ import { ambiguousResendAllowanceFor, selfContainedResponsesBody } from "./reset import { upstreamErrorMessageFromPayload, ENCRYPTED_FUNCTION_OUTPUT_REJECTION } from "../../lib/errors"; import { isTransientConsoleGoUploadRejection } from "../../providers/opencode-zen-rate-limit"; import { planReasoningEffortDowngrade } from "../../providers/reasoning-metadata"; +import { waitForProviderRequestSlot } from "../../providers/request-pacing"; /** Prepares and recovers one native Responses exchange before client commitment. */ export async function preparePassthroughExchange( @@ -221,6 +222,9 @@ export async function preparePassthroughExchange( | "pendingHopPermit" | "workflowRootId" | "sendsUsed" + | "adapterDispatchBudget" + | "noteAdapterPhysicalSend" + | "noteAdapterRecoveryWithheld" >, ) { const { config, logCtx, options, req } = requestContext; @@ -253,6 +257,9 @@ export async function preparePassthroughExchange( claimAmbiguousResend, reserveCredentialHop, workflowRootId, + adapterDispatchBudget, + noteAdapterPhysicalSend, + noteAdapterRecoveryWithheld, } = sendBudgetState; const codexSafetyBufferingOptions = isCanonicalOpenAiForwardProvider(route.provider) @@ -913,44 +920,79 @@ export async function preparePassthroughExchange( const initialBodyRefusal = refuseOversizedOutboundBody(request); if (initialBodyRefusal) return initialBodyRefusal; 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, parsed.stream, - providerFetch(route.provider, options.codexWsRuntimeIdentity, { - nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt - && responseEffects.plaintextV2AgentMessageToolNames.size === 0 - ? options.nativeControl : undefined, - dispatchOverride: oauthDispatch(request), - providerName: route.providerName, - modelId: route.modelId, - onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId), - beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider) - ? createCodexReserveDispatchGuard(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: remainingTransientSendBudget(transientSendAttempts()), onSendsConsumed: noteTransientSends, - 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, + nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt + && responseEffects.plaintextV2AgentMessageToolNames.size === 0 + ? options.nativeControl : undefined, + dispatchOverride: oauthDispatch(request), + providerName: route.providerName, + modelId: route.modelId, + onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId), + beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider) + ? createCodexReserveDispatchGuard(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, parsed.stream, + providerFetch(route.provider, options.codexWsRuntimeIdentity, { + nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt + && responseEffects.plaintextV2AgentMessageToolNames.size === 0 + ? options.nativeControl : undefined, + dispatchOverride: oauthDispatch(request), + providerName: route.providerName, + modelId: route.modelId, + onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId), + beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider) + ? createCodexReserveDispatchGuard(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: remainingTransientSendBudget(transientSendAttempts()), onSendsConsumed: noteTransientSends, + claimAmbiguousResend: claimPreHeaderResend, + }, + ); + } } catch (err) { return transportFailureResponse(err); } finally { @@ -974,7 +1016,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") }; } @@ -1101,7 +1143,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"); } @@ -1241,7 +1283,7 @@ export async function preparePassthroughExchange( resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire, route.staticPolicy), config.cacheRetention, ); - if (!("passthrough" in refreshedAdapter) || !refreshedAdapter.passthrough) { + if (!adapterIsPassthrough(refreshedAdapter, parsed)) { upstream.abort(); return formatErrorResponse(502, "upstream_error", "OAuth refresh changed the provider wire unexpectedly"); } diff --git a/src/server/responses/request-transport.ts b/src/server/responses/request-transport.ts index 8cbd60d83b6..a4c0ed24efa 100644 --- a/src/server/responses/request-transport.ts +++ b/src/server/responses/request-transport.ts @@ -7,7 +7,7 @@ import { getAccountCredentialWithStatus, credentialGeneration, } from "../../oauth/store"; -import type { ProviderAdapter, AdapterRequest } from "../../adapters/base"; +import { adapterIsPassthrough, type ProviderAdapter, type AdapterRequest } from "../../adapters/base"; import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig, OcxUsage } from "../../types"; import type { AnthropicAccountSelectionReason } from "../../oauth/anthropic-routing"; import { @@ -733,7 +733,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 a369a903bef..bfd979bb020 100644 --- a/src/server/responses/sidecar-execution.ts +++ b/src/server/responses/sidecar-execution.ts @@ -16,7 +16,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 { @@ -126,7 +126,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 60db8c25ab3..e4d853c8e5a 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 { CodexAccountCooldownError, codexMainProfileDrainingResponse, @@ -39,6 +40,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"; @@ -47,6 +53,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 @@ -58,6 +65,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, @@ -135,6 +260,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 cc4fcab6dc2..f881af68d1c 100644 --- a/tests/adapters/adapter-registry-authority.test.ts +++ b/tests/adapters/adapter-registry-authority.test.ts @@ -23,6 +23,7 @@ const EXPECTED_ADAPTER_NAMES = { cursor: "cursor", devin: "devin", "mimo-free": "mimo-free", + mirasim: "mirasim", qoder: "qoder", "claude-cli": "claude-cli", } as const; @@ -77,10 +78,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 52540f1dd4a..a1a064546b1 100644 --- a/tests/responses/responses-compaction-routing.test.ts +++ b/tests/responses/responses-compaction-routing.test.ts @@ -72,6 +72,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); @@ -94,6 +99,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 1fa5869efaad7068b242740c17685d7d5cb2eba7 Mon Sep 17 00:00:00 2001 From: letr1n1ty Date: Mon, 21 Sep 2026 15:31:46 +0800 Subject: [PATCH 02/13] 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 3f0878f924e..89727d881ee 100644 --- a/gui/public/provider-icons/README.md +++ b/gui/public/provider-icons/README.md @@ -202,6 +202,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 7a9e848eb38..6c736f111d7 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 6db3c02046e..75675e12b2e 100644 --- a/gui/src/components/provider-workspace/ProviderAuthPanel.tsx +++ b/gui/src/components/provider-workspace/ProviderAuthPanel.tsx @@ -7,7 +7,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 { formatOAuthHealthLabel, @@ -533,6 +537,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 (
  • @@ -544,7 +549,7 @@ export default function ProviderAuthPanel({