diff --git a/.env.example b/.env.example index 2ba4099968..477e26f2e3 100644 --- a/.env.example +++ b/.env.example @@ -2304,6 +2304,18 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis # ───────────────────────────────────────────────────────────────────────────── # HYPERAGENT_USAGE_URL=https://hyperagent.com/api/settings/billing/usage +# ───────────────────────────────────────────────────────────────────────────── +# ChatGPT Web (Codex) headless browser and outbound tool tunnel +# Used by: open-sse/executors/chatgpt-web-codex.ts +# Connection values entered in the dashboard override these global defaults. +# ───────────────────────────────────────────────────────────────────────────── +# CHATGPT_WEB_CODEX_CHROME_PATH=/usr/bin/chromium +# CHROME_PATH=/usr/bin/chromium +# CHATGPT_WEB_CODEX_CDP_URL=http://chatgpt-web-codex-browser:9223 +# CHATGPT_WEB_CODEX_TUNNEL_ID=tunnel_0123456789abcdef0123456789abcdef +# CHATGPT_WEB_CODEX_RUNTIME_KEY= +# CHATGPT_WEB_CODEX_CONNECTOR_NAME=OmniRoute Codex + # ───────────────────────────────────────────────────────────────────────────── # Browser-login VNC sessions (optional — src/lib/vncSession/manifest.ts) # Containerized Chromium+VNC used for interactive browser-login credential diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000000..45fcfed7bd --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,26 @@ +# Third-Party Notices + +## codex-chatgpt-web + +Parts of `open-sse/vendor/codex-chatgpt-web/` are adapted from +[`miuuyy/codex-chatgpt-web`](https://github.com/miuuyy/codex-chatgpt-web), commit +`55592fca0ba19a27f1b769cec8fff61ff340a785`. + +MIT License + +Copyright (c) 2026 codex-chatgpt-web contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/providers/CHATGPT_WEB_CODEX.md b/docs/providers/CHATGPT_WEB_CODEX.md new file mode 100644 index 0000000000..9af963054d --- /dev/null +++ b/docs/providers/CHATGPT_WEB_CODEX.md @@ -0,0 +1,94 @@ +# ChatGPT Web (Codex) + +`ChatGPT Web (Codex)` ist ein zusätzlicher Provider. Der bestehende Provider +`ChatGPT Web (Plus/Pro)` bleibt für normale Chats, Bilder und dessen bisherige +Tool-Emulation unverändert. + +## Voraussetzungen + +- ein vollständiger Cookie-Header einer angemeldeten ChatGPT-Sitzung; +- Chrome oder Chromium bei npm-, systemd- und PM2-Installationen; +- beim Docker-Profil `web` der interne Chromium-Dienst aus `docker-compose.yml`; +- ein OpenAI-Tunnel und ein ChatGPT-Custom-Connector für lokale Codex-Tools. + +Der Tunnel ist nur für Tool-Runden nötig. `pro` ist read-only und benötigt keinen +lokalen Tool-Connector. + +## Einrichtung in der Weboberfläche + +1. Öffne den Provider `ChatGPT Web (Codex)` und füge eine Connection hinzu. +2. Füge den vollständigen ChatGPT-Cookie, die Tunnel-ID, den Runtime-Key und den + Namen des Custom Connectors ein. +3. Starte die Prüfung. OmniRoute öffnet headless einen Temporary Chat und erkennt + dabei auch, ob `pro` für das Konto verfügbar ist. +4. Speichere die Connection. OmniRoute ersetzt den eingegebenen Cookie durch den + geprüften Playwright-Storage-State und speichert ihn zusammen mit dem Runtime-Key + über die verschlüsselte Credential-Abstraktion. + +Der rohe Cookie wird nach erfolgreichem Speichern nicht zusätzlich aufbewahrt. +Wenn die Sitzung abläuft, öffne die Connection, gib einen frischen vollständigen +Cookie ein und prüfe sie erneut. Der Doctor-Status im Edit-Dialog zeigt Browser, +Storage-State, Anmeldung, Temporary Chat, Tunnel, Connector und Tool-Roundtrip +getrennt an. + +## Modelle und Combos + +Die festen Modelle sind: + +- `chatgpt-web-codex/instant` +- `chatgpt-web-codex/medium` +- `chatgpt-web-codex/high` +- `chatgpt-web-codex/extra-high` +- `chatgpt-web-codex/pro` + +Füge eines davon wie jedes andere Modell zu einer Combo hinzu. Die Codex-App +sendet nur den Combo-Namen als `model` an den normalen Responses-Endpunkt +`/v1/responses`. Es gibt keinen Sonderendpoint und keinen Codex-Modus-Schalter. + +`pro` führt keine lokalen Tools aus. Ein erzwungenes Tool macht dieses Combo-Ziel +inkompatibel; bei optionalen Tools läuft der Turn read-only und meldet diese +Einschränkung als Commentary. + +## Sicherheitsmodell + +- Der native Pfad verlangt einen Responses-Request, einen erkannten Codex-Client + sowie zusammenpassende Thread- und Turn-Identitäten. +- Workspace, Sandbox, Approval-Policy und Toolkatalog stammen aus der nativen + Codex-Hülle. Freier Prompttext ist dafür keine Autorität. +- ChatGPT erhält pro Turn nur eine kurzlebige Capability. Der MCP-Broker akzeptiert + ausschließlich Tools, die Codex in genau diesem Turn angeboten hat. +- Das automatische Bestätigen von „Allow once“ gibt nur den Tool-Wunsch an Codex + zurück. Codex allein entscheidet über Freigabe und Ausführung. +- Vor dem ersten Output darf die Combo auf ein anderes kompatibles Ziel fallen. + Danach bleiben Provider, Modell, Connection und Browserturn bis zum Abschluss + gepinnt. +- Cookies, Runtime-Keys, Storage-State und Capability-Tokens erscheinen nicht in + Providerantworten oder Request-Logs. + +## Headless VPS und Docker + +Bei npm-, systemd- und PM2-Betrieb erkennt OmniRoute übliche Chrome- und +Chromium-Pfade. Alternativ kann `CHATGPT_WEB_CODEX_CHROME_PATH` gesetzt werden. + +Das Docker-Profil `web` startet `chatgpt-web-codex-browser` im internen +Compose-Netz. Sein CDP-Port wird nicht auf dem Host veröffentlicht. Das geschützte +Profilvolume bleibt getrennt vom OmniRoute-Datenvolume und der Browser erhält +ausreichend Shared Memory. Der interne CDP-Proxy lauscht nur im Compose-Netz auf +Port `9223`; Chrome selbst bleibt im Sidecar an Loopback gebunden. + +Eine Supervisor-Lease unter `DATA_DIR` verhindert, dass mehrere OmniRoute-Prozesse +denselben Tunnel- und Brokerzustand besitzen. Ein Konflikt erscheint im Doctor. + +## Interaktive Wiederherstellung + +Der normale Pfad ist vollständig headless. Wenn ChatGPT eine interaktive +Anmeldung oder Challenge verlangt, kann die bestehende VNC-Browser-Infrastruktur +als Recovery-Weg verwendet werden. Browser-UI und CDP dürfen dabei nur über +Loopback, eine authentifizierte Managementverbindung oder einen SSH-Tunnel +erreichbar sein; noVNC bleibt im normalen Betrieb deaktiviert. + +## WebSocket-Fallback + +Enthält eine Combo `ChatGPT Web (Codex)`, fordert die Responses-WebSocket-Brücke +vor der Upstream-Verbindung den HTTP/SSE-Fallback an. Die eigentliche Übertragung +erfolgt dann über `/v1/responses`. diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index f1099872bd..44e99c88b4 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -1300,3 +1300,16 @@ Used by `src/lib/vncSession/manifest.ts` to configure Docker-based headless Chro | `OMNIROUTE_VNC_READY_MS` | `45000` | `src/lib/vncSession/manifest.ts` | Browser readiness timeout (ms). | | `OMNIROUTE_VNC_HARVEST_MS` | `20000` | `src/lib/vncSession/manifest.ts` | Harvest/cleanup timeout (ms). | | `VIBEPROXY_DATA_DIR` | _(unset)_ | `open-sse/services/notionThreadSessions.ts` | Directory for Notion thread session persistence. | + +### ChatGPT Web (Codex) + +Globale Defaults für den headless Browser und den ausgehenden Tool-Tunnel. Im Dashboard gesetzte Connection-Werte haben Vorrang. + +| Variable | Default | Source File | Description | +| ------------------------------------ | -------------------------------- | ------------------------------------------------ | --------------------------------------------------------------------------- | +| `CHATGPT_WEB_CODEX_CHROME_PATH` | _(auto-detect)_ | `open-sse/executors/chatgpt-web-codex.ts` | Expliziter Chrome-/Chromium-Pfad für npm-, systemd- und PM2-Betrieb. | +| `CHROME_PATH` | _(auto-detect)_ | `open-sse/executors/chatgpt-web-codex.ts` | Gemeinsamer Fallback für einen expliziten Chrome-/Chromium-Pfad. | +| `CHATGPT_WEB_CODEX_CDP_URL` | _(unset)_ | `open-sse/executors/chatgpt-web-codex.ts` | Interner CDP-Endpunkt; Docker verwendet den Sidecar auf Port `9223`. | +| `CHATGPT_WEB_CODEX_TUNNEL_ID` | _(unset)_ | `open-sse/executors/chatgpt-web-codex.ts` | Globale OpenAI-Tunnel-ID für lokale Codex-Tool-Runden. | +| `CHATGPT_WEB_CODEX_RUNTIME_KEY` | _(unset)_ | `open-sse/executors/chatgpt-web-codex.ts` | Globaler Tunnel Runtime-Key; niemals in Logs ausgeben. | +| `CHATGPT_WEB_CODEX_CONNECTOR_NAME` | _(unset)_ | `open-sse/executors/chatgpt-web-codex.ts` | Name des ChatGPT-Custom-Connectors für die MCP-Brücke. | diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index 97007c8965..cc6483ceac 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -773,6 +773,29 @@ "stream": "https://chatgpt.com/backend-api/conversation" } }, + "chatgpt-web-codex": { + "format": "openai-responses", + "headers": { + "apiKey": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "nonStream": { + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "oauth": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + } + }, + "url": { + "nonStream": "https://chatgpt.com", + "stream": "https://chatgpt.com" + } + }, "chenzk": { "format": "openai", "headers": { @@ -5177,6 +5200,29 @@ "stream": "https://api.xiaomimimo.com/v1" } }, + "xiaomi-mimo-token-plan": { + "format": "openai", + "headers": { + "apiKey": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "nonStream": { + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "oauth": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + } + }, + "url": { + "nonStream": "https://token-plan-sgp.xiaomimimo.com/v1", + "stream": "https://token-plan-sgp.xiaomimimo.com/v1" + } + }, "yi": { "format": "openai", "headers": { diff --git a/tests/unit/chatcore-request-format.test.ts b/tests/unit/chatcore-request-format.test.ts index 064a2384f2..4ffa9fb722 100644 --- a/tests/unit/chatcore-request-format.test.ts +++ b/tests/unit/chatcore-request-format.test.ts @@ -10,7 +10,11 @@ import { resolveChatCoreRequestFormat } from "../../open-sse/handlers/chatCore/r import { shouldUseNativeCodexPassthrough } from "../../open-sse/handlers/chatCore/passthroughHelpers.ts"; import { FORMATS } from "../../open-sse/translator/formats.ts"; -const base = { body: { messages: [{ role: "user", content: "hi" }] }, provider: "openai", userAgent: "unit-test" }; +const base = { + body: { messages: [{ role: "user", content: "hi" }] }, + provider: "openai", + userAgent: "unit-test", +}; test("chat/completions endpoint → openai source, not a responses endpoint, no downgrade", () => { const r = resolveChatCoreRequestFormat({ @@ -93,6 +97,8 @@ test("nativeCodexPassthrough delegates to shouldUseNativeCodexPassthrough (codex provider: "codex", sourceFormat: r.sourceFormat, endpointPath: r.endpointPath, + body: { input: "x" }, + headers: new Headers(), }) ); }); diff --git a/tests/unit/chatgpt-web-codex-turn-pin.test.ts b/tests/unit/chatgpt-web-codex-turn-pin.test.ts new file mode 100644 index 0000000000..71a0dc6504 --- /dev/null +++ b/tests/unit/chatgpt-web-codex-turn-pin.test.ts @@ -0,0 +1,69 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + applyNativeCodexTurnPin, + clearNativeCodexTurnPinsForTests, + getNativeCodexTurnPin, + pinNativeCodexTurn, + revokeNativeCodexTurnPinsForConnection, +} from "../../open-sse/services/combo/nativeCodexTurnPin.ts"; +import type { ResolvedComboTarget } from "../../open-sse/services/combo/types.ts"; + +const body = { + client_metadata: { + "x-codex-turn-metadata": JSON.stringify({ thread_id: "thread-1", turn_id: "turn-1" }), + }, +}; + +function target(modelStr: string, provider: string): ResolvedComboTarget { + return { + kind: "model", + stepId: `step-${provider}`, + executionKey: `${provider}:${modelStr}`, + modelStr, + provider, + providerId: provider, + connectionId: null, + weight: 1, + label: null, + }; +} + +test("native continuation pins provider, model and connection", () => { + clearNativeCodexTurnPinsForTests(); + const pinnedTarget = target("chatgpt-web-codex/high", "chatgpt-web-codex"); + pinNativeCodexTurn({ + body, + comboName: "coding", + target: pinnedTarget, + connectionId: "connection-a", + }); + const pin = getNativeCodexTurnPin(body, "coding"); + assert.ok(pin); + assert.deepEqual(applyNativeCodexTurnPin([pinnedTarget], pin), [ + { ...pinnedTarget, connectionId: "connection-a", allowedConnectionIds: ["connection-a"] }, + ]); + assert.equal(revokeNativeCodexTurnPinsForConnection("connection-a"), 1); + assert.equal(getNativeCodexTurnPin(body, "coding"), null); +}); + +test("a pinned turn cannot silently move to another target", () => { + clearNativeCodexTurnPinsForTests(); + pinNativeCodexTurn({ + body, + comboName: "coding", + target: target("chatgpt-web-codex/high", "chatgpt-web-codex"), + connectionId: "connection-a", + }); + assert.throws( + () => + pinNativeCodexTurn({ + body, + comboName: "coding", + target: target("codex/gpt-5.6-sol", "codex"), + connectionId: "connection-b", + }), + /changed after output/ + ); +}); diff --git a/tests/unit/chatgpt-web-codex.test.ts b/tests/unit/chatgpt-web-codex.test.ts new file mode 100644 index 0000000000..c7ffb6319f --- /dev/null +++ b/tests/unit/chatgpt-web-codex.test.ts @@ -0,0 +1,239 @@ +import assert from "node:assert/strict"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; + +import { + hasNativeCodexTurnBinding, + isCodexOriginatedHeaders, + isVerifiedNativeCodexRequest, +} from "../../open-sse/config/codexIdentity.ts"; +import { chatgpt_web_codexProvider } from "../../open-sse/config/providers/registry/chatgpt-web-codex/index.ts"; +import { + decodeChatGptWebCodexSecrets, + encodeChatGptWebCodexSecrets, +} from "../../open-sse/executors/chatgpt-web-codex/credentials.ts"; +import { + reasoningEffortOf, + requireChatGptWebCodexRoute, +} from "../../open-sse/executors/chatgpt-web-codex/models.ts"; +import { + parseTunnelChecksum, + parseTunnelRuntimeStatus, +} from "../../open-sse/executors/chatgpt-web-codex/tunnelClient.ts"; +import { + callTurnBroker, + TurnBroker, +} from "../../open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/turn-broker.ts"; +import { + CHATGPT_INLINE_CONTEXT_MAX_CHARS, + compileChatGptWebPrompt, +} from "../../open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/prompt.ts"; +import type { CodexParsedRequest } from "../../open-sse/vendor/codex-chatgpt-web/types.ts"; + +test("registers the additive ChatGPT Web Codex provider and five fixed routes", () => { + assert.equal(chatgpt_web_codexProvider.id, "chatgpt-web-codex"); + assert.deepEqual( + chatgpt_web_codexProvider.models?.map((model) => model.id), + ["instant", "medium", "high", "extra-high", "pro"] + ); + assert.deepEqual( + ["instant", "medium", "high", "extra-high", "pro"].map( + (model) => requireChatGptWebCodexRoute(model).effort + ), + ["low", "medium", "high", "xhigh", "max"] + ); + assert.equal(requireChatGptWebCodexRoute("pro").pro, true); +}); + +test("explicit Responses reasoning effort is read for mismatch preflight", () => { + assert.equal(reasoningEffortOf({ reasoning: { effort: "high" } }), "high"); + assert.equal(reasoningEffortOf({ reasoning_effort: "xhigh" }), "xhigh"); +}); + +test("Codex detection requires originator or Codex User-Agent", () => { + assert.equal(isCodexOriginatedHeaders({ originator: "codex_cli_rs" }), true); + assert.equal(isCodexOriginatedHeaders({ "user-agent": "codex_app/1.0" }), true); + assert.equal(isCodexOriginatedHeaders({ "user-agent": "openai-node/4" }), false); +}); + +test("native ChatGPT Web Codex detection also requires thread and turn identity", () => { + const headers = { originator: "codex_cli_rs" }; + const body = { + client_metadata: { + "x-codex-turn-metadata": JSON.stringify({ + thread_id: "thread-1", + turn_id: "turn-1", + }), + }, + }; + assert.equal(hasNativeCodexTurnBinding(body), true); + assert.equal(isVerifiedNativeCodexRequest(body, headers), true); + assert.equal(isVerifiedNativeCodexRequest({}, headers), false); + assert.equal(isVerifiedNativeCodexRequest(body, { "user-agent": "openai-node/4" }), false); + assert.equal( + hasNativeCodexTurnBinding({ + client_metadata: { + "x-codex-turn-metadata": JSON.stringify({ turn_id: "turn-1" }), + }, + }), + false + ); +}); + +test("version two credentials contain storage state but not the raw Cookie", () => { + const encoded = encodeChatGptWebCodexSecrets({ + storageState: { cookies: [{ name: "session", value: "secret" }], origins: [] }, + runtimeKey: "runtime-secret", + }); + const parsed = JSON.parse(encoded) as Record; + assert.equal(parsed.version, 2); + assert.equal("cookie" in parsed, false); + assert.deepEqual(decodeChatGptWebCodexSecrets(encoded).storageState, parsed.storageState); +}); + +test("legacy Cookie credentials remain decodable for one-time validation", () => { + assert.equal( + decodeChatGptWebCodexSecrets( + JSON.stringify({ version: 1, cookie: "Cookie: session=value", runtimeKey: "key" }) + ).cookie, + "session=value" + ); +}); + +test("tunnel status is ready only when the process is running and healthy", () => { + const ready = parseTunnelRuntimeStatus( + JSON.stringify({ process_running: true, healthy: true, ready: true, runtime_state: "ready" }) + ); + assert.equal(ready.ok, true); + assert.equal( + parseTunnelRuntimeStatus(JSON.stringify({ process_running: false, healthy: true, ready: true })) + .ok, + false + ); +}); + +test("tunnel checksum parsing is pinned to the exact release asset", () => { + const checksum = "a".repeat(64); + assert.equal( + parseTunnelChecksum(`${checksum} tunnel-client.zip\n`, "tunnel-client.zip"), + checksum + ); + assert.throws( + () => parseTunnelChecksum(`${checksum} another.zip\n`, "tunnel-client.zip"), + /no valid entry/ + ); +}); + +test("turn broker holds a tool invocation and rejects wrong or duplicate results", async () => { + const root = mkdtempSync(join(tmpdir(), "omniroute-cgw-broker-")); + const socketPath = join(root, "runtime", "turn-broker.sock"); + const broker = TurnBroker.forSocket(socketPath); + try { + const token = await broker.register( + { + cwd: root, + roots: [root], + writableRoots: [root], + sandboxPolicy: { type: "dangerFullAccess" }, + tools: [ + { + name: "exec_command", + description: "Run a command", + parameters: { type: "object" }, + }, + ], + }, + 10_000 + ); + const claim = await callTurnBroker<{ bindingId: string }>(socketPath, { + method: "claim", + token, + }); + const invocation = callTurnBroker<{ content: unknown[] }>( + socketPath, + { + method: "invoke", + bindingId: claim.bindingId, + wireName: "exec_command", + arguments: { cmd: "pwd" }, + }, + 10_000 + ); + const [request] = await broker.nextToolBatch(token); + assert.ok(request); + assert.throws(() => broker.completeTool(token, "unknown-call", { content: [] }), /not pending/); + broker.completeTool(token, request.callId, { content: [{ type: "text", text: root }] }); + assert.deepEqual(await invocation, { content: [{ type: "text", text: root }] }); + assert.throws(() => broker.completeTool(token, request.callId, { content: [] }), /not pending/); + } finally { + await broker.close(); + rmSync(root, { recursive: true, force: true }); + } +}); + +test("revoking a turn rejects a pending connector invocation", async () => { + const root = mkdtempSync(join(tmpdir(), "omniroute-cgw-revoke-")); + const socketPath = join(root, "runtime", "turn-broker.sock"); + const broker = TurnBroker.forSocket(socketPath); + try { + const token = await broker.register( + { + cwd: root, + roots: [root], + writableRoots: [root], + sandboxPolicy: { type: "dangerFullAccess" }, + tools: [], + }, + 10_000 + ); + const claim = await callTurnBroker<{ bindingId: string }>(socketPath, { + method: "claim", + token, + }); + const invocation = callTurnBroker( + socketPath, + { + method: "invoke", + bindingId: claim.bindingId, + wireName: "exec_command", + arguments: { cmd: "sleep 30" }, + }, + 10_000 + ); + await broker.nextToolBatch(token); + broker.revoke(token); + await assert.rejects(invocation, /revoked/); + } finally { + await broker.close(); + rmSync(root, { recursive: true, force: true }); + } +}); + +function requestWithText(text: string): CodexParsedRequest { + return { + modelId: "gpt-5.6-sol", + context: { + messages: [{ role: "user", content: text, timestamp: 1 }], + }, + stream: true, + options: { reasoning: "high" }, + }; +} + +test("small contexts stay inline and large contexts become in-memory JSONL", () => { + const capabilities = { localToolsEnabled: false, proAvailable: true }; + const small = compileChatGptWebPrompt(requestWithText("hello"), capabilities); + assert.equal(small.contextAttachments.length, 0); + assert.match(small.text, //); + + const large = compileChatGptWebPrompt( + requestWithText("x".repeat(CHATGPT_INLINE_CONTEXT_MAX_CHARS + 1)), + capabilities + ); + assert.equal(large.contextAttachments.length, 1); + assert.equal(large.contextAttachments[0]?.mimeType, "application/x-ndjson"); + assert.match(large.contextAttachments[0]?.buffer.toString("utf8") || "", /"type":"manifest"/); + assert.doesNotMatch(large.text, /x{1000}/); +}); diff --git a/tests/unit/combo-context-window-filter.test.ts b/tests/unit/combo-context-window-filter.test.ts index 2f0bfa64cf..de37f821d9 100644 --- a/tests/unit/combo-context-window-filter.test.ts +++ b/tests/unit/combo-context-window-filter.test.ts @@ -291,6 +291,12 @@ test("native Responses context bypasses catalog overflow only for all-Codex pool }), null ); + assert.equal( + getKnownContextOverflow([target("codex/large"), target("chatgpt-web-codex/large")], body, { + clientManagedResponsesContext: true, + }), + null + ); assert.ok( getKnownContextOverflow([target("unit-known-context/large")], body, { clientManagedResponsesContext: true, diff --git a/tests/unit/pack-artifact-policy.test.ts b/tests/unit/pack-artifact-policy.test.ts index e311cdc048..05019aa7be 100644 --- a/tests/unit/pack-artifact-policy.test.ts +++ b/tests/unit/pack-artifact-policy.test.ts @@ -119,6 +119,7 @@ test("findMissingArtifactPaths flags missing root runtime files in the tarball", "dist/main-server-timeouts.mjs", "dist/open-sse/services/compression/engines/rtk/filters/generic-output.json", "dist/open-sse/services/compression/rules/en/filler.json", + "dist/open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/mcp-server.js", "dist/peer-stamp.mjs", "dist/responses-ws-proxy.mjs", "dist/server-ws.mjs", diff --git a/tests/unit/providers-constants-split.test.ts b/tests/unit/providers-constants-split.test.ts index fbae29f06e..3d4ef96361 100644 --- a/tests/unit/providers-constants-split.test.ts +++ b/tests/unit/providers-constants-split.test.ts @@ -1,7 +1,7 @@ // Characterization of the providers.ts catalog split (god-file decomposition): the host became a // barrel that re-exports 10 data catalogs now living under constants/providers/*, and APIKEY is // merged from 6 semantic family files (apikey/.ts). Locks: the public surface (every catalog -// + helpers still exported), the spread-merge integrity (194 APIKEY entries, no loss/dup), and that +// + helpers still exported), the spread-merge integrity (195 APIKEY entries, no loss/dup), and that // load-time Zod validation still runs. Pure-data move → behavior must be identical. // Count was 171 before obsolete provider removals (PR #6675: glhf/kluster/cablyai/inclusionai etc., // 171->167) plus #6126 (ClinePass dual-auth): the API-key-only APIKEY_PROVIDERS_GATEWAYS entry was @@ -16,7 +16,7 @@ // internlm/ant-ling, all regional) to 190, then #8161 (sarvam/writer/plamo — writer in frontier-labs, // sarvam+plamo in regional) to 193, then #8170 (inception/typhoon — inception in frontier-labs, // typhoon in regional) to 195, then Firecrawl dual search+fetch under SEARCH_PROVIDERS.firecrawl -// (removed specialty-media duplicate) to 194. +// (removed specialty-media duplicate) to 194, then the Xiaomi MiMo Token Plan brought it to 195. import { test } from "node:test"; import assert from "node:assert/strict"; @@ -45,12 +45,12 @@ test("barrel still exports every catalog + key helpers", () => { } }); -test("APIKEY_PROVIDERS merges the 6 family files into 194 entries (no loss / no dup)", async () => { +test("APIKEY_PROVIDERS merges the 6 family files into 195 entries (no loss / no dup)", async () => { const keys = Object.keys((P as Record).APIKEY_PROVIDERS); - assert.equal(keys.length, 194); - assert.equal(new Set(keys).size, 194, "duplicate keys after spread-merge"); + assert.equal(keys.length, 195); + assert.equal(new Set(keys).size, 195, "duplicate keys after spread-merge"); // the merged object's entry-count equals the sum of the 6 semantic family files; families are a - // strict partition (every provider in exactly one), so the sum must be exactly 194. + // strict partition (every provider in exactly one), so the sum must be exactly 195. const families: [string, string][] = [ ["gateways", "APIKEY_PROVIDERS_GATEWAYS"], ["frontier-labs", "APIKEY_PROVIDERS_FRONTIER"], @@ -70,7 +70,7 @@ test("APIKEY_PROVIDERS merges the 6 family files into 194 entries (no loss / no seen.add(k); } } - assert.equal(famTotal, 194, "families must partition all 194 providers"); + assert.equal(famTotal, 195, "families must partition all 195 providers"); }); test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => { diff --git a/tests/unit/quota-plan-registry.test.ts b/tests/unit/quota-plan-registry.test.ts index b2e4ec4276..bbcb1ae87b 100644 --- a/tests/unit/quota-plan-registry.test.ts +++ b/tests/unit/quota-plan-registry.test.ts @@ -64,8 +64,8 @@ test("getKnownPlan('') returns null", () => { assert.equal(getKnownPlan(""), null); }); -test("knownProviders() returns exactly 11 entries", () => { - assert.equal(knownProviders().length, 11); +test("knownProviders() returns exactly 12 entries", () => { + assert.equal(knownProviders().length, 12); }); test("knownProviders() includes the full registry set", () => {