mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 02:02:13 +03:00
Document and test ChatGPT Web integration
This commit is contained in:
12
.env.example
12
.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
|
||||
|
||||
26
THIRD_PARTY_NOTICES.md
Normal file
26
THIRD_PARTY_NOTICES.md
Normal file
@@ -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.
|
||||
94
docs/providers/CHATGPT_WEB_CODEX.md
Normal file
94
docs/providers/CHATGPT_WEB_CODEX.md
Normal file
@@ -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`.
|
||||
@@ -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. |
|
||||
|
||||
@@ -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 <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"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 <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"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": {
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
69
tests/unit/chatgpt-web-codex-turn-pin.test.ts
Normal file
69
tests/unit/chatgpt-web-codex-turn-pin.test.ts
Normal file
@@ -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/
|
||||
);
|
||||
});
|
||||
239
tests/unit/chatgpt-web-codex.test.ts
Normal file
239
tests/unit/chatgpt-web-codex.test.ts
Normal file
@@ -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<string, unknown>;
|
||||
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, /<codex_context_json>/);
|
||||
|
||||
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}/);
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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/<family>.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<string, object>).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", () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user