Document and test ChatGPT Web integration

This commit is contained in:
Jan Leon
2026-07-30 06:56:19 +02:00
parent 5ead198eb4
commit 8aa9c6f710
12 changed files with 522 additions and 10 deletions

View File

@@ -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
View 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.

View 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`.

View File

@@ -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. |

View File

@@ -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": {

View File

@@ -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(),
})
);
});

View 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/
);
});

View 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}/);
});

View File

@@ -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,

View File

@@ -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",

View File

@@ -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", () => {

View File

@@ -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", () => {