mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 17:52:31 +03:00
Compare commits
3 Commits
maint/cher
...
maint/cher
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6428c85e66 | ||
|
|
ce08eae72d | ||
|
|
7ffa5e24fd |
26
.gitignore
vendored
26
.gitignore
vendored
@@ -72,6 +72,7 @@ yarn-error.log*
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.example
|
||||
!.env.devin-bridge.example
|
||||
!.env.homolog.example
|
||||
# Provider API keys (never commit)
|
||||
*.api-key
|
||||
@@ -171,7 +172,6 @@ config/quality/test-impact-map.json
|
||||
# GitNexus local index
|
||||
.gitnexus
|
||||
.worktrees
|
||||
bin/omniroute.mjs
|
||||
|
||||
# Consistent with .dockerignore / .npmignore
|
||||
.omc/
|
||||
@@ -201,12 +201,17 @@ scripts/i18n/_pending-keys.json
|
||||
.codegraph/
|
||||
|
||||
# Fumadocs generated source
|
||||
.source/
|
||||
/.source/
|
||||
|
||||
# Temporary local worktrees used to build unpublished npm tarballs
|
||||
/.deploy-build-*/
|
||||
|
||||
# AI agent local settings and configs
|
||||
.agents/
|
||||
.antigravitycli/
|
||||
.claude/
|
||||
!tests/fixtures/devin-bridge/e2e-workspace/.claude/
|
||||
!tests/fixtures/devin-bridge/e2e-workspace/.claude/**
|
||||
|
||||
# PR Reviews and local feedback files
|
||||
pr_reviews*.json
|
||||
@@ -233,7 +238,10 @@ omniroute.md
|
||||
|
||||
# mise configuration
|
||||
mise.toml
|
||||
_artifacts/ # release-green artifacts
|
||||
# release-green artifacts (.gitignore has no inline comments — a trailing
|
||||
# `# ...` becomes part of the pattern, so it must sit on its own line).
|
||||
# Already covered by /_*/ above; kept explicit for discoverability.
|
||||
_artifacts/
|
||||
.claude-flow/
|
||||
|
||||
# ESLint file cache (npm run lint --cache / complexity ratchets)
|
||||
@@ -243,6 +251,8 @@ _artifacts/ # release-green artifacts
|
||||
|
||||
# CI/local quality artifacts (eslint-results.json, quality-ratchet.md, etc.)
|
||||
.artifacts/
|
||||
# Isolated Devin bridge workspaces, evidence, and test databases
|
||||
.sandbox/
|
||||
|
||||
# Homologation E2E suite (npm run homolog) — real-environment credentials + report output
|
||||
.env.homolog
|
||||
@@ -250,8 +260,12 @@ tests/homolog/.auth/
|
||||
tests/homolog/ui/.auth/
|
||||
homolog-report/
|
||||
docker-compose.yml.bak
|
||||
.playwright-cli/
|
||||
# Playwright screenshot/log output. Today every artifact happens to land inside
|
||||
# output/**/.playwright-cli/ (covered above), but anything written directly to
|
||||
# output/ would otherwise show up as untracked.
|
||||
/output/
|
||||
|
||||
# _tasks e um repo git SEPARADO (ver AGENTS.md). A linha _tasks/ (com barra) NAO
|
||||
# ignora um SYMLINK chamado _tasks; /_tasks (ancorado) cobre arquivo/symlink/dir na raiz
|
||||
# e impede que um git add -A recapture o symlink (incidente 2026-08-08).
|
||||
# _tasks e um repo git SEPARADO (ver AGENTS.md). _tasks/ (com barra) NAO ignora um
|
||||
# SYMLINK _tasks; /_tasks (ancorado) cobre symlink/dir na raiz (incidente 2026-08-08).
|
||||
/_tasks
|
||||
|
||||
@@ -18,7 +18,7 @@ Unlike API-key providers, Web Cookie providers authenticate using the credential
|
||||
|
||||
Many authentication issues are caused by copying cookies from the wrong place.
|
||||
|
||||
## Do NOT copy from Cookie Storage
|
||||
## Do NOT copy from Cookie Storage
|
||||
|
||||
Most browsers expose stored cookies through:
|
||||
|
||||
@@ -36,7 +36,7 @@ Although these cookies look correct, they may be:
|
||||
|
||||
Using these values may cause authentication failures even if they appear valid.
|
||||
|
||||
## Copy from a Live Request
|
||||
## Copy from a Live Request
|
||||
|
||||
Instead, use the cookies from a successful request:
|
||||
|
||||
@@ -80,14 +80,14 @@ The exact credentials required depend on the provider.
|
||||
|
||||
Different websites store authentication differently. Some require only cookies, while others may require additional headers or tokens.
|
||||
|
||||
| Provider | Credential Format | Provider Guide |
|
||||
|----------|-------------------|----------------|
|
||||
| Claude Web | Full Cookie request header | `docs/providers/CLAUDE_WEB.md` |
|
||||
| ChatGPT Web | _(verify)_ | |
|
||||
| Gemini Web | _(verify)_ | |
|
||||
| Copilot Web | _(verify)_ | |
|
||||
| Grok Web | _(verify)_ | |
|
||||
| ... | ... | ... |
|
||||
| Provider | Credential Format | Provider Guide |
|
||||
| ----------- | -------------------------------------------------------------- | ------------------------------- |
|
||||
| Claude Web | Full Cookie request header | `docs/providers/CLAUDE_WEB.md` |
|
||||
| ChatGPT Web | Full Cookie header or `__Secure-next-auth.session-token` value | `docs/providers/CHATGPT_WEB.md` |
|
||||
| Gemini Web | _(verify)_ | |
|
||||
| Copilot Web | _(verify)_ | |
|
||||
| Grok Web | _(verify)_ | |
|
||||
| ... | ... | ... |
|
||||
|
||||
> Update this table as new Web Cookie providers are added or existing providers change their authentication requirements.
|
||||
|
||||
|
||||
143
docs/providers/CHATGPT_WEB.md
Normal file
143
docs/providers/CHATGPT_WEB.md
Normal file
@@ -0,0 +1,143 @@
|
||||
---
|
||||
title: "Providers — ChatGPT Web (session credentials via Cookie Editor)"
|
||||
version: 3.8.50
|
||||
lastUpdated: 2026-08-08
|
||||
---
|
||||
|
||||
# Providers — ChatGPT Web (Plus/Pro session credentials)
|
||||
|
||||
`chatgpt-web` (alias `cgpt-web`, display name **ChatGPT Web (Plus/Pro)**) sends OpenAI-format chat requests through an authenticated `chatgpt.com` browser session. It authenticates with the `__Secure-next-auth.session-token` cookie — **no API key required**.
|
||||
|
||||
> **New to Web Cookie providers?**
|
||||
>
|
||||
> Read **`docs/getting-started/WEB-COOKIE-GUIDE.md`** for the general setup process, limitations, and troubleshooting before following this provider-specific guide.
|
||||
|
||||
---
|
||||
|
||||
## 1. What credential does OmniRoute need?
|
||||
|
||||
Defined in `src/shared/constants/providers/web-cookie.ts` + `src/shared/providers/webSessionCredentials.ts`:
|
||||
|
||||
| Field | Value |
|
||||
| -------------------------- | ----------------------------------------------------------------------------- |
|
||||
| Provider id | `chatgpt-web` |
|
||||
| Credential name | `__Secure-next-auth.session-token` |
|
||||
| Accepts full Cookie header | ✅ yes |
|
||||
| Accepted storage keys | `cookie`, `sessionToken`, `session-token`, `__Secure-next-auth.session-token` |
|
||||
|
||||
Two paste formats both work:
|
||||
|
||||
- **Bare value** — just the token contents: `eyJhbGciOi...`
|
||||
- **Full Cookie header** — `__Secure-next-auth.session-token=eyJhbGciOi...; cf_clearance=...` (preferred — carries rotation/anti-bot cookies the executor needs)
|
||||
|
||||
---
|
||||
|
||||
## 2. Get the cookie fast with Cookie Editor
|
||||
|
||||
> **Why Cookie Editor instead of DevTools?** The repo's general guide insists you copy from a **live network request**, not cookie storage. Cookie Editor gives you both in one click: it can read the _live_ cookies for the domain (including `HttpOnly` ones DevTools hides) and export them as a ready-made Cookie header — no manual request-hunting, no stale-storage pitfalls.
|
||||
|
||||
### 2.1 Install and pin
|
||||
|
||||
1. Install **[Cookie-Editor](https://chromewebstore.google.com/detail/cookie-editor/hlkenndednhfkekhgcdicdfddnkalmdm)** (Moustachauve) in Chrome/Edge, or the Firefox equivalent.
|
||||
2. Pin it to the toolbar (right-click icon → Pin).
|
||||
|
||||
### 2.2 Update the extension's option settings (one-time)
|
||||
|
||||
Click the Cookie Editor icon → the **⚙️ Options** tab, then set:
|
||||
|
||||
| Option | Set to | Why |
|
||||
| ----------------------------------- | ------------------- | --------------------------------------------------------------------------------------- |
|
||||
| **Export format** | `Cookie header` | Produces a paste-ready `name=value; name=value` string — exactly what OmniRoute accepts |
|
||||
| **Show / include HttpOnly cookies** | ON | `__Secure-next-auth.session-token` is HttpOnly; hidden by default in some skins |
|
||||
| **Show expired cookies** | OFF | Keeps the list clean — expired tokens are useless |
|
||||
| **Domain filter behavior** | Active tab's domain | Auto-scopes to `chatgpt.com` when you open the popup there |
|
||||
|
||||
### 2.3 Copy the credential
|
||||
|
||||
1. Go to **https://chatgpt.com** and make sure you're **signed in with the Plus/Pro account** you want OmniRoute to use.
|
||||
2. Open a conversation and send at least one message (forces the session token to be live/refreshed).
|
||||
3. Click the **Cookie Editor** icon → the popup shows only `chatgpt.com` cookies.
|
||||
4. Find `__Secure-next-auth.session-token`. If it's split into chunks (`__Secure-next-auth.session-token.0`, `.1`, …), select **all** of them — OmniRoute's `nextAuthCookie.ts` merges rotated chunk families.
|
||||
5. Click **Export → Copy** (the icon in the header). With `Export format = Cookie header` this copies the full header in one click.
|
||||
|
||||
> **If the token is missing:** you're signed out, or the account has no active Plus/Pro subscription (chatgpt-web is a `subscriptionRisk: true` provider — free accounts won't authenticate).
|
||||
|
||||
---
|
||||
|
||||
## 3. Verify the required data (before pasting)
|
||||
|
||||
The repo's `WEB-COOKIE-GUIDE.md` mandates a live-request check. Do it once per session:
|
||||
|
||||
1. With chatgpt.com open, press **F12** → **Network** tab.
|
||||
2. Refresh the page, then send a chat message.
|
||||
3. Click the conversation request (e.g. `/backend-api/conversation` or the SSE stream) → **Headers** → **Request Headers** → **Cookie**.
|
||||
4. Confirm it contains `__Secure-next-auth.session-token=...` — **not** just `cf_clearance` or `__cf_bm`.
|
||||
|
||||
The value you copied in step 2.3 must match what the live request sends. If they differ, re-copy from Cookie Editor.
|
||||
|
||||
---
|
||||
|
||||
## 4. Add / update the credential in OmniRoute
|
||||
|
||||
### Dashboard (typical user path)
|
||||
|
||||
1. Open the OmniRoute dashboard → **Providers** → **Add Provider**.
|
||||
2. Search **ChatGPT Web (Plus/Pro)** (id `chatgpt-web`).
|
||||
3. Paste the copied cookie header into the credential field.
|
||||
4. Click **Test Connection**.
|
||||
5. Save.
|
||||
|
||||
> **Validation caveat:** per `WEB-COOKIE-GUIDE.md`, Test Connection only checks the format — it doesn't guarantee upstream auth (tracked as Issue #7857). If requests later 401, re-copy from a fresh live session (sessions rotate; the executor auto-merges `Set-Cookie` rotations, but expired tokens need a manual refresh).
|
||||
|
||||
### Bulk / session pools (many accounts)
|
||||
|
||||
For multiple ChatGPT sessions, use the bulk web-session import or session-pool endpoints:
|
||||
|
||||
- `POST /api/providers/bulk-web-session` — import many cookie credentials at once
|
||||
- `GET /api/session-pools` + `/api/session-pools/[provider]` — pool rotation across accounts
|
||||
|
||||
Each credential blob must carry the `__Secure-next-auth.session-token` value under one of the accepted storage keys (`cookie`, `sessionToken`, `session-token`, or the cookie's exact name).
|
||||
|
||||
### Renewing when the session expires
|
||||
|
||||
Web sessions expire on sign-out or server-side rotation. Re-run steps 2.3 + 4 whenever requests start failing with 401/403. There is no refresh token — the cookie **is** the credential.
|
||||
|
||||
---
|
||||
|
||||
## 5. Contributing: update docs / constants and open a PR
|
||||
|
||||
If you changed the credential contract (new storage key, new cookie name, changed hint) or are filling the docs gap, contribute it:
|
||||
|
||||
1. Update `src/shared/providers/webSessionCredentials.ts` (credential name / placeholder / storage keys) or `src/shared/constants/providers/web-cookie.ts` (`authHint`).
|
||||
2. Update this guide (`docs/providers/CHATGPT_WEB.md`) and the provider table in `docs/getting-started/WEB-COOKIE-GUIDE.md` (currently shows `_(verify)_` for ChatGPT Web).
|
||||
3. Update `.env.example` + `docs/reference/ENVIRONMENT.md` if you touched env vars, then run:
|
||||
```bash
|
||||
node scripts/check/check-env-doc-sync.mjs # must pass
|
||||
```
|
||||
4. Run the provider/unit tests:
|
||||
```bash
|
||||
npm run test:unit
|
||||
# targeted: tests/unit/chatgpt-web.test.ts (stealth path)
|
||||
```
|
||||
5. Commit with a Conventional Commit message (no `Co-Authored-By`), push to your fork, and open the PR against `main` (or `release/v3.8.49` per CONTRIBUTING.md):
|
||||
```bash
|
||||
git checkout -b docs/chatgpt-web-cookie-guide
|
||||
git add docs/providers/CHATGPT_WEB.md docs/getting-started/WEB-COOKIE-GUIDE.md
|
||||
git commit -m "docs(providers): add ChatGPT Web cookie credential guide"
|
||||
git push -u origin docs/chatgpt-web-cookie-guide
|
||||
gh pr create --base main --head docs/chatgpt-web-cookie-guide --title "docs(providers): ChatGPT Web session credential guide"
|
||||
```
|
||||
|
||||
> ⚠️ **Never commit a real cookie value.** All examples above are placeholders. If a test fixture needs a token, use a fake `eyJhbGciOi...` string.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
| -------------------------------- | -------------------------------------------- | --------------------------------------------------------- |
|
||||
| Cookie not in Cookie Editor | Signed out / not HttpOnly-visible | Sign in; enable HttpOnly display in options |
|
||||
| Token missing from live request | Free account, or request isn't authenticated | Use a Plus/Pro account; send a chat message first |
|
||||
| 401 after Test Connection passed | Expired/rotated session (Issue #7857) | Re-copy from a fresh live request |
|
||||
| Chunked token fails | Only one chunk pasted | Select all `__Secure-next-auth.session-token.*` chunks |
|
||||
| 403 from a different machine | Cloudflare-pinned session | Copy + use from the same browser profile/IP as the cookie |
|
||||
@@ -408,13 +408,12 @@ export class CliproxyapiExecutor extends BaseExecutor {
|
||||
|
||||
input.log?.info?.("CPA", `CLIProxyAPI → ${url} (model: ${input.model}, shape: ${shape})`);
|
||||
|
||||
// _toolNameMap and _namespaceToolIdentityMap are in-memory channels to
|
||||
// chatCore for response-side tool name restoration; never send them over
|
||||
// the wire.
|
||||
// _toolNameMap is an in-memory channel to chatCore for response-side
|
||||
// tool name restoration; never send it over the wire.
|
||||
const wireBody =
|
||||
transformedBody && typeof transformedBody === "object"
|
||||
? JSON.stringify(transformedBody, (key, value) =>
|
||||
key === "_toolNameMap" || key === "_namespaceToolIdentityMap" ? undefined : value
|
||||
key === "_toolNameMap" ? undefined : value
|
||||
)
|
||||
: JSON.stringify(transformedBody);
|
||||
|
||||
|
||||
@@ -207,6 +207,7 @@ import { stageTrace } from "./chatCore/stageTrace.ts";
|
||||
import { attachCompressionUsageReceiptAfterAnalytics as attachCompressionUsageReceiptAfterAnalyticsFor } from "./chatCore/compressionUsageReceipt.ts";
|
||||
import { prepareUpstreamBody } from "./chatCore/upstreamBody.ts";
|
||||
import { getQuotaScopeLabelForProvider } from "../services/antigravityQuotaFamily.ts";
|
||||
|
||||
import {
|
||||
getCallLogPipelineCaptureStreamChunks,
|
||||
getCallLogPipelineMaxSizeBytes,
|
||||
@@ -366,7 +367,9 @@ import {
|
||||
isTpmExhausted,
|
||||
isRpmExhausted,
|
||||
} from "../services/geminiRateLimitTracker.ts";
|
||||
|
||||
import { isSmallEnoughForSemanticCache } from "../utils/estimateSize.ts";
|
||||
|
||||
/**
|
||||
* Core chat handler - shared between SSE and Worker
|
||||
* Returns { success, response, status, error } for caller to handle fallback
|
||||
@@ -386,8 +389,10 @@ import { isSmallEnoughForSemanticCache } from "../utils/estimateSize.ts";
|
||||
* @param {boolean} options.isCombo - Whether this request is from a combo
|
||||
* @param {string} options.connectionId - Connection ID for settings lookup
|
||||
*/
|
||||
|
||||
// extractSystemRoleMessages extracted to chatCore/claudeSystemRole.ts (#3501); re-exported above so
|
||||
// existing importers (e.g. tests/unit/system-role-extraction.test.ts) keep resolving it from here.
|
||||
|
||||
export async function handleChatCore({
|
||||
body,
|
||||
modelInfo,
|
||||
@@ -423,6 +428,7 @@ export async function handleChatCore({
|
||||
/* fail open */
|
||||
}
|
||||
}
|
||||
|
||||
// Per-request model-routing metadata (first extracted slice of the request-setup phase).
|
||||
const { apiFormat, customModelTargetFormat, requestedModel } = resolveChatCoreRequestSetup(
|
||||
modelInfo,
|
||||
@@ -436,6 +442,7 @@ export async function handleChatCore({
|
||||
// (not Math.random) purely to satisfy CodeQL js/insecure-randomness — this id
|
||||
// is a log-correlation token, not a security secret.
|
||||
const traceId = globalThis.crypto.randomUUID().slice(0, 6);
|
||||
|
||||
// Emit request.started event for real-time dashboard
|
||||
setImmediate(() => {
|
||||
emit("request.started", {
|
||||
@@ -519,6 +526,7 @@ export async function handleChatCore({
|
||||
`long-running goal mode enabled: readinessMax=${agentGoalPolicy.readinessMaxTimeoutMs}ms streamRecovery=${agentGoalPolicy.streamRecoveryEnabled}`
|
||||
);
|
||||
}
|
||||
|
||||
let effectiveServiceTier: EffectiveServiceTier = "standard";
|
||||
// Codex service-tier resolvers extracted to chatCore/serviceTier.ts (#3501); bind the per-request
|
||||
// provider/credentials once and delegate so the existing call sites stay byte-identical.
|
||||
@@ -547,6 +555,7 @@ export async function handleChatCore({
|
||||
})
|
||||
).catch(() => {});
|
||||
};
|
||||
|
||||
// Key-health updater extracted to chatCore/keyHealth.ts (#3501); bind the per-request log once
|
||||
// and delegate so the existing call sites stay byte-identical.
|
||||
const recordKeyHealthStatus = (
|
||||
@@ -554,9 +563,11 @@ export async function handleChatCore({
|
||||
creds: Record<string, unknown> | null | undefined,
|
||||
transport?: string
|
||||
): void => recordKeyHealthStatusFor(status, creds, log, transport);
|
||||
|
||||
const persistCodexQuotaState = async (headers: Record<string, string> | null, status = 0) => {
|
||||
const currentConnectionId = getCurrentConnectionId();
|
||||
if (provider !== "codex" || !currentConnectionId || !headers) return;
|
||||
|
||||
try {
|
||||
const existingProviderData =
|
||||
credentials?.providerSpecificData && typeof credentials.providerSpecificData === "object"
|
||||
@@ -571,23 +582,28 @@ export async function handleChatCore({
|
||||
status,
|
||||
});
|
||||
if (!built) return;
|
||||
|
||||
if (built.exhaustionLog) {
|
||||
log?.debug?.("CODEX", built.exhaustionLog);
|
||||
}
|
||||
|
||||
// Invalidate the preflight cache for this connection so the next
|
||||
// isModelAvailable check fetches fresh quota data.
|
||||
if (status === 429) {
|
||||
invalidateCodexQuotaCache(currentConnectionId);
|
||||
}
|
||||
|
||||
await updateProviderConnection(currentConnectionId, {
|
||||
providerSpecificData: built.nextProviderData,
|
||||
});
|
||||
|
||||
credentials.providerSpecificData = built.nextProviderData;
|
||||
} catch (err) {
|
||||
const errMessage = err instanceof Error ? err.message : String(err);
|
||||
log?.debug?.("CODEX", `Failed to persist codex quota state: ${errMessage}`);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Phase 9.2: Idempotency check ──
|
||||
// Resolve the idempotency key once here and reuse it at the Phase 9.2 save site below,
|
||||
// rather than re-deriving it. (#3821-review LEDGER-6)
|
||||
@@ -606,11 +622,13 @@ export async function handleChatCore({
|
||||
if (idempotencyHit) {
|
||||
return idempotencyHit;
|
||||
}
|
||||
|
||||
// T07: Inject connectionId into credentials so executors can rotate API keys
|
||||
// using providerSpecificData.extraApiKeys (API Key Round-Robin feature)
|
||||
if (connectionId && credentials && !credentials.connectionId) {
|
||||
credentials.connectionId = connectionId;
|
||||
}
|
||||
|
||||
// Endpoint/format resolution extracted to chatCore/requestFormat.ts (#3501); pure derivation
|
||||
// from the inbound request, destructured so every downstream use stays byte-identical.
|
||||
const {
|
||||
@@ -2246,19 +2264,8 @@ export async function handleChatCore({
|
||||
// the latter is a Kiro/Claude passthrough alias channel with string values,
|
||||
// while namespace identities carry `{namespace, name}` for the #7936 response
|
||||
// seam. Extract first because Kiro merge may reuse `_toolNameMap` below.
|
||||
//
|
||||
// #9780 — prefer the dedicated channel: on a pivot the openai->claude/gemini
|
||||
// step publishes its own alias map on `_toolNameMap`, so that property alone
|
||||
// yields aliases here. The `_toolNameMap` read stays as the fallback for the
|
||||
// non-pivot producers (executors/base.ts, cliproxyapi.ts, antigravity).
|
||||
const namespaceIdentityMap = translatedBody._namespaceToolIdentityMap;
|
||||
const requestToolIdentityMap =
|
||||
namespaceIdentityMap instanceof Map
|
||||
? namespaceIdentityMap
|
||||
: translatedBody._toolNameMap instanceof Map
|
||||
? translatedBody._toolNameMap
|
||||
: null;
|
||||
delete translatedBody._namespaceToolIdentityMap;
|
||||
translatedBody._toolNameMap instanceof Map ? translatedBody._toolNameMap : null;
|
||||
delete translatedBody._toolNameMap;
|
||||
|
||||
// Kiro: sanitize tool schemas before dispatch. Kiro returns 400 "Improperly
|
||||
@@ -5018,6 +5025,7 @@ export async function handleChatCore({
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function isTokenExpiringSoon(expiresAt, bufferMs = 5 * 60 * 1000) {
|
||||
if (!expiresAt) return false;
|
||||
const expiresAtMs = new Date(expiresAt).getTime();
|
||||
|
||||
@@ -352,27 +352,7 @@ export function translateRequest(
|
||||
...(hasProvider ? { _provider: provider } : {}),
|
||||
}
|
||||
: credentials;
|
||||
// #9780 — carry the Responses namespace identity map across the pivot.
|
||||
// Target translators return a brand-new object (buildKiroPayload et
|
||||
// al.), dropping the non-enumerable property step 1 attached; the
|
||||
// #7936 seam then gets null and namespace sub-tool calls come back
|
||||
// flattened, which Codex rejects with `unsupported call: <name>`.
|
||||
const identityMap = (result as Record<string, unknown>)._namespaceToolIdentityMap;
|
||||
const translated = fromOpenAI(model, result, stream, translationCredentials);
|
||||
if (
|
||||
identityMap instanceof Map &&
|
||||
translated &&
|
||||
typeof translated === "object" &&
|
||||
!((translated as Record<string, unknown>)._namespaceToolIdentityMap instanceof Map)
|
||||
) {
|
||||
Object.defineProperty(translated, "_namespaceToolIdentityMap", {
|
||||
value: identityMap,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
result = translated;
|
||||
result = fromOpenAI(model, result, stream, translationCredentials);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -752,19 +752,8 @@ export function openaiResponsesToOpenAIRequest(
|
||||
delete result.prompt_cache_retention;
|
||||
|
||||
if (namespaceToolIdentityMap.size > 0) {
|
||||
// chatCore extracts and deletes these transient side channels before dispatch.
|
||||
// chatCore extracts and deletes this transient side channel before dispatch.
|
||||
// Non-enumerability keeps internal request metadata off the upstream wire.
|
||||
//
|
||||
// Two properties on purpose (#9780): `_toolNameMap` is also the alias
|
||||
// channel for openai-to-claude/gemini, which overwrite it on a pivot, so
|
||||
// the identity map needs a name of its own. `_toolNameMap` stays populated
|
||||
// for the existing consumers (executors/base.ts, cliproxyapi, antigravity).
|
||||
Object.defineProperty(result, "_namespaceToolIdentityMap", {
|
||||
value: namespaceToolIdentityMap,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
Object.defineProperty(result, "_toolNameMap", {
|
||||
value: namespaceToolIdentityMap,
|
||||
enumerable: false,
|
||||
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -31,7 +31,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"commander": "^15.0.0",
|
||||
"csv-stringify": "^6.7.0",
|
||||
"dompurify": "^3.4.13",
|
||||
"dompurify": "^3.4.12",
|
||||
"express": "^5.2.1",
|
||||
"fetch-socks": "^1.3.3",
|
||||
"fflate": "^0.8.3",
|
||||
@@ -17064,9 +17064,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.4.13",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz",
|
||||
"integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==",
|
||||
"version": "3.4.12",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz",
|
||||
"integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
@@ -27576,9 +27576,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.18",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||
"version": "3.3.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
||||
@@ -256,7 +256,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"commander": "^15.0.0",
|
||||
"csv-stringify": "^6.7.0",
|
||||
"dompurify": "^3.4.13",
|
||||
"dompurify": "^3.4.12",
|
||||
"express": "^5.2.1",
|
||||
"fetch-socks": "^1.3.3",
|
||||
"fflate": "^0.8.3",
|
||||
@@ -395,6 +395,7 @@
|
||||
]
|
||||
},
|
||||
"overrides": {
|
||||
"dompurify": "^3.4.12",
|
||||
"fast-xml-parser": "^5.10.1",
|
||||
"sharp": "^0.35.0",
|
||||
"postcss": "^8.5.18",
|
||||
@@ -453,10 +454,6 @@
|
||||
},
|
||||
"xmlbuilder2": {
|
||||
"js-yaml": "^4.3.1"
|
||||
},
|
||||
"nanoid": "^3.3.17",
|
||||
"monaco-editor": {
|
||||
"dompurify": "^3.4.13"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
// #9780 — the Responses namespace identity map must survive the hub-and-spoke
|
||||
// pivot in translator/index.ts. Step 1 flattens namespace sub-tools (#8295) and
|
||||
// records `{namespace, name}`; step 2 returns a new object and used to drop it,
|
||||
// leaving the #7936 seam with null and Codex rejecting `unsupported call`.
|
||||
// A naive copy-through is not an option: openai-to-claude/gemini publish their
|
||||
// own alias map on `_toolNameMap`, hence the dedicated channel asserted here.
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
await import("../../open-sse/translator/bootstrap.ts");
|
||||
const { translateRequest, initState } = await import("../../open-sse/translator/index.ts");
|
||||
const { openaiToOpenAIResponsesResponse } = await import(
|
||||
"../../open-sse/translator/response/openai-responses.ts"
|
||||
);
|
||||
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
|
||||
|
||||
type NamespaceIdentity = { namespace: string; name: string };
|
||||
|
||||
const NAMESPACE_REQUEST = {
|
||||
model: "any-model",
|
||||
instructions: "coding agent",
|
||||
input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "go" }] }],
|
||||
tools: [
|
||||
{
|
||||
type: "namespace",
|
||||
name: "functions",
|
||||
tools: [
|
||||
{
|
||||
name: "exec",
|
||||
description: "Run a shell command",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: { cmd: { type: "string" } },
|
||||
required: ["cmd"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function pivot(targetFormat: string): Record<string, unknown> {
|
||||
return translateRequest(
|
||||
"openai-responses",
|
||||
targetFormat,
|
||||
"any-model",
|
||||
structuredClone(NAMESPACE_REQUEST),
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function identityOf(body: Record<string, unknown>) {
|
||||
const map = body._namespaceToolIdentityMap;
|
||||
assert.ok(map instanceof Map, "expected a _namespaceToolIdentityMap after the pivot");
|
||||
return map as Map<string, NamespaceIdentity>;
|
||||
}
|
||||
|
||||
test("#9780: namespace identity survives the openai-responses -> kiro pivot", () => {
|
||||
const identity = identityOf(pivot("kiro"));
|
||||
|
||||
assert.equal(identity.size, 1);
|
||||
assert.deepEqual(identity.get("functions__exec"), { namespace: "functions", name: "exec" });
|
||||
});
|
||||
|
||||
test("#9780: namespace identity survives the openai-responses -> cursor pivot", () => {
|
||||
const identity = identityOf(pivot("cursor"));
|
||||
|
||||
assert.deepEqual(identity.get("functions__exec"), { namespace: "functions", name: "exec" });
|
||||
});
|
||||
|
||||
// Regression guard: these two appeared to "keep" a map before the fix, but it
|
||||
// was the alias map.
|
||||
for (const target of ["claude", "gemini"]) {
|
||||
test(`#9780: ${target} pivot keeps its alias map AND the namespace identity`, () => {
|
||||
const body = pivot(target);
|
||||
const identity = identityOf(body);
|
||||
|
||||
assert.deepEqual(identity.get("functions__exec"), { namespace: "functions", name: "exec" });
|
||||
|
||||
// The alias channel must be untouched: string values, not identities.
|
||||
const aliases = body._toolNameMap;
|
||||
assert.ok(aliases instanceof Map, `${target} must still publish its alias map`);
|
||||
for (const value of (aliases as Map<string, unknown>).values()) {
|
||||
assert.equal(typeof value, "string", `${target} alias values must stay strings`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Same-format requests are never flattened, so an absent map is correct here.
|
||||
test("#9780: same-format openai-responses request is not flattened at all", () => {
|
||||
const body = pivot("openai-responses");
|
||||
const tools = body.tools as Array<Record<string, unknown>>;
|
||||
|
||||
assert.equal(tools[0].type, "namespace");
|
||||
assert.equal((tools[0].tools as Array<{ name: string }>)[0].name, "exec");
|
||||
assert.equal(body._namespaceToolIdentityMap, undefined);
|
||||
});
|
||||
|
||||
test("#9780: the identity channel is non-enumerable and never serializes", () => {
|
||||
const body = pivot("kiro");
|
||||
|
||||
assert.ok(body._namespaceToolIdentityMap instanceof Map);
|
||||
assert.equal(
|
||||
Object.prototype.propertyIsEnumerable.call(body, "_namespaceToolIdentityMap"),
|
||||
false
|
||||
);
|
||||
assert.equal("_namespaceToolIdentityMap" in JSON.parse(JSON.stringify(body)), false);
|
||||
});
|
||||
|
||||
// End-to-end: request pivot + response seam, i.e. what the Codex adjudicator
|
||||
// actually receives. Before the fix every target emitted `functions__exec` with
|
||||
// no namespace, which is the reported `unsupported call`.
|
||||
for (const target of ["kiro", "cursor", "claude", "gemini"]) {
|
||||
test(`#9780: ${target} round-trip returns the declared name and its namespace`, () => {
|
||||
const body = pivot(target);
|
||||
const state = initState(FORMATS.OPENAI_RESPONSES) as Record<string, unknown>;
|
||||
state.requestToolIdentityMap = body._namespaceToolIdentityMap;
|
||||
|
||||
// The upstream echoes the flattened wire name (#8295).
|
||||
const events = openaiToOpenAIResponsesResponse(
|
||||
{
|
||||
id: "chatcmpl-9780",
|
||||
model: "any-model",
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
tool_calls: [
|
||||
{
|
||||
index: 0,
|
||||
id: "call_9780",
|
||||
type: "function",
|
||||
function: { name: "functions__exec", arguments: '{"cmd":"git status"}' },
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: "tool_calls",
|
||||
},
|
||||
],
|
||||
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
|
||||
},
|
||||
state
|
||||
) as Array<{ event: string; data: { item?: NamespaceIdentity } }>;
|
||||
|
||||
const added = events.find((e) => e.event === "response.output_item.added")?.data.item;
|
||||
assert.ok(added, "expected response.output_item.added");
|
||||
assert.deepEqual(
|
||||
{ name: added.name, namespace: added.namespace },
|
||||
{ name: "exec", namespace: "functions" }
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user