feat(codex): Cursor compatibility + Next.js 16 proxy migration

- Force streaming for Codex/OpenAI models to fix non-streaming bug
- Strip unsupported params (user, metadata, stream_options, prompt_cache_retention)
- Force response translation from openai-responses to openai format
- Migrate middleware.js to proxy.js for Next.js 16
- Use webpack explicitly in dev/build scripts
- Updated Codex User-Agent
This commit is contained in:
Hellodebasishsahu 2026-01-31 18:22:10 +05:30
parent 814e02ae31
commit e9b0a73ece
6 changed files with 49 additions and 36 deletions

View file

@ -39,9 +39,9 @@ export const PROVIDERS = {
baseUrl: "https://chatgpt.com/backend-api/codex/responses",
format: "openai-responses", // Use OpenAI Responses API format (reuse translator)
headers: {
"Version": "0.21.0",
"Version": "0.92.0",
"Openai-Beta": "responses=experimental",
"User-Agent": "codex_cli_rs/0.50.0 (Mac OS 26.0.1; arm64)"
"User-Agent": "codex-cli/0.92.0 (Windows 10.0.26100; x64)"
},
// OpenAI OAuth configuration
clientId: "app_EMoamEEZ73f0CkXaXp7hrann",

View file

@ -32,6 +32,11 @@ export class CodexExecutor extends BaseExecutor {
delete body.top_logprobs;
delete body.n;
delete body.seed;
delete body.max_tokens;
delete body.user; // Cursor sends this but Codex doesn't support it
delete body.prompt_cache_retention; // Cursor sends this but Codex doesn't support it
delete body.metadata; // Cursor sends this but Codex doesn't support it
delete body.stream_options; // Cursor sends this but Codex doesn't support it
return body;
}

View file

@ -253,7 +253,9 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
const alias = PROVIDER_ID_TO_ALIAS[provider] || provider;
const modelTargetFormat = getModelTargetFormat(alias, model);
const targetFormat = modelTargetFormat || getTargetFormat(provider);
const stream = body.stream !== false;
// Force streaming for OpenAI/Codex models (they don't support non-streaming mode properly)
const stream = (provider === 'openai' || provider === 'codex') ? true : (body.stream !== false);
// Create request logger for this session: sourceFormat_targetFormat_model
const reqLogger = await createRequestLogger(sourceFormat, targetFormat, model);
@ -459,8 +461,14 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
// Create transform stream with logger for streaming response
let transformStream;
if (needsTranslation(targetFormat, sourceFormat)) {
transformStream = createSSETransformStreamWithLogger(targetFormat, sourceFormat, provider, reqLogger, toolNameMap, model, connectionId);
// For Codex provider, always translate response from openai-responses to openai format
// This ensures clients like Cursor get the expected chat completions format
const needsCodexTranslation = (provider === 'codex' || provider === 'openai') && targetFormat === 'openai-responses';
if (needsCodexTranslation || needsTranslation(targetFormat, sourceFormat)) {
// For Codex, translate FROM openai-responses TO openai (client's expected format)
const responseSourceFormat = needsCodexTranslation ? 'openai-responses' : targetFormat;
const responseTargetFormat = needsCodexTranslation ? 'openai' : sourceFormat;
transformStream = createSSETransformStreamWithLogger(responseSourceFormat, responseTargetFormat, provider, reqLogger, toolNameMap, model, connectionId);
} else {
transformStream = createPassthroughStreamWithLogger(provider, reqLogger, model, connectionId);
}

View file

@ -4,8 +4,8 @@
"description": "9Router web dashboard",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"dev": "next dev --webpack",
"build": "next build --webpack",
"start": "next start"
},
"dependencies": {
@ -18,21 +18,21 @@
"jose": "^6.1.3",
"lowdb": "^7.0.1",
"monaco-editor": "^0.55.1",
"next": "^15.2.0",
"next": "^16.1.6",
"node-machine-id": "^1.1.12",
"open": "^10.1.0",
"ora": "^5.4.1",
"react": "19.2.1",
"react-dom": "19.2.1",
"socks-proxy-agent": "^8.0.4",
"undici": "^7.16.0",
"open": "^11.0.0",
"ora": "^9.1.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"socks-proxy-agent": "^8.0.5",
"undici": "^7.19.2",
"uuid": "^13.0.0",
"zustand": "^5.0.9"
"zustand": "^5.0.10"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.18",
"eslint": "^9",
"eslint-config-next": "16.0.10",
"eslint-config-next": "16.1.6",
"tailwindcss": "^4"
}
}

View file

@ -19,7 +19,7 @@ export default function APIPageClient({ machineId }) {
const [showCloudModal, setShowCloudModal] = useState(false);
const [showDisableModal, setShowDisableModal] = useState(false);
const [cloudSyncing, setCloudSyncing] = useState(false);
const [setCloudStatus] = useState(null);
const [cloudStatus, setCloudStatus] = useState(null);
const [syncStep, setSyncStep] = useState(""); // "syncing" | "verifying" | "disabling" | ""
const { copied, copy } = useCopyToClipboard();

View file

@ -5,7 +5,7 @@ const SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || "9router-default-secret-change-me"
);
export async function middleware(request) {
export async function proxy(request) {
const { pathname } = request.nextUrl;
// Protect all dashboard routes