diff --git a/open-sse/config/appConstants.js b/open-sse/config/appConstants.js
index 3a96be56..09f3ecff 100644
--- a/open-sse/config/appConstants.js
+++ b/open-sse/config/appConstants.js
@@ -146,6 +146,16 @@ export const LOAD_CODE_ASSIST_METADATA = {
export const CLAUDE_SYSTEM_PROMPT = "You are Claude Code, Anthropic's official CLI for Claude.";
export const ANTIGRAVITY_DEFAULT_SYSTEM = "You are Antigravity, a powerful agentic AI coding assistant designed by the Google Deepmind team working on Advanced Agentic Coding.You are pair programming with a USER to solve their coding task. The task may require creating a new codebase, modifying or debugging an existing codebase, or simply answering a question.**Absolute paths only****Proactiveness**";
+// Proactive token refresh lead times per provider (ms)
+export const REFRESH_LEAD_MS = {
+ codex: 5 * 24 * 60 * 60 * 1000, // 5 days
+ claude: 4 * 60 * 60 * 1000, // 4 hours
+ iflow: 24 * 60 * 60 * 1000, // 24 hours
+ qwen: 20 * 60 * 1000, // 20 minutes
+ "kimi-coding": 5 * 60 * 1000, // 5 minutes
+ antigravity: 5 * 60 * 1000, // 5 minutes
+};
+
// OAuth endpoints
export const OAUTH_ENDPOINTS = {
google: {
diff --git a/open-sse/config/codexInstructions.js b/open-sse/config/codexInstructions.js
index b903ac06..75b691dd 100644
--- a/open-sse/config/codexInstructions.js
+++ b/open-sse/config/codexInstructions.js
@@ -1,5 +1,4 @@
// Default instructions for Codex models
-// Source: CLIProxyAPI internal/misc/codex_instructions/
export const CODEX_DEFAULT_INSTRUCTIONS = `You are Codex, based on GPT-5. You are running as a coding agent in the Codex CLI on a user's computer.
diff --git a/open-sse/executors/codex.js b/open-sse/executors/codex.js
index b8842474..e7033623 100644
--- a/open-sse/executors/codex.js
+++ b/open-sse/executors/codex.js
@@ -150,7 +150,7 @@ export class CodexExecutor extends BaseExecutor {
// Priority: explicit reasoning.effort > reasoning_effort param > model suffix > default (medium)
if (!body.reasoning) {
- const effort = body.reasoning_effort || modelEffort || 'medium';
+ const effort = body.reasoning_effort || modelEffort || 'low';
body.reasoning = { effort, summary: "auto" };
} else if (!body.reasoning.summary) {
body.reasoning.summary = "auto";
diff --git a/open-sse/executors/qwen.js b/open-sse/executors/qwen.js
index 2cadc414..2c9d48ae 100644
--- a/open-sse/executors/qwen.js
+++ b/open-sse/executors/qwen.js
@@ -1,7 +1,7 @@
import { platform, arch } from "os";
import { DefaultExecutor } from "./default.js";
-/** portal.qwen.ai — aligned with CLIProxyAPI qwen_executor */
+/** portal.qwen.ai */
const qwenCodeVersion = "0.13.2";
const qwenStainless = {
runtimeVersion: "v22.17.0",
diff --git a/open-sse/services/projectId.js b/open-sse/services/projectId.js
index b7aa3002..f9a24e1a 100644
--- a/open-sse/services/projectId.js
+++ b/open-sse/services/projectId.js
@@ -1,7 +1,6 @@
/**
* Project ID Service - Fetch and cache real Project IDs from Google Cloud Code API
*
- * Reference: CLIProxyAPI internal/auth/antigravity/auth.go (FetchProjectID + OnboardUser)
*
* Instead of generating random project IDs (e.g. "useful-spark-a1b2c"),
* this service fetches the real Project ID bound to the authenticated user's account.
diff --git a/open-sse/services/tokenRefresh.js b/open-sse/services/tokenRefresh.js
index 36099868..040b6e00 100644
--- a/open-sse/services/tokenRefresh.js
+++ b/open-sse/services/tokenRefresh.js
@@ -1,9 +1,14 @@
import { PROVIDERS } from "../config/providers.js";
-import { OAUTH_ENDPOINTS, GITHUB_COPILOT } from "../config/appConstants.js";
+import { OAUTH_ENDPOINTS, GITHUB_COPILOT, REFRESH_LEAD_MS } from "../config/appConstants.js";
-// Token expiry buffer (refresh if expires within 5 minutes)
+// Default token expiry buffer (refresh if expires within 5 minutes)
export const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
+// Get provider-specific refresh lead time, falls back to default buffer
+export function getRefreshLeadMs(provider) {
+ return REFRESH_LEAD_MS[provider] || TOKEN_EXPIRY_BUFFER_MS;
+}
+
/**
* Refresh OAuth access token using refresh token
*/
diff --git a/open-sse/translator/helpers/geminiHelper.js b/open-sse/translator/helpers/geminiHelper.js
index 24737e79..623b4ea9 100644
--- a/open-sse/translator/helpers/geminiHelper.js
+++ b/open-sse/translator/helpers/geminiHelper.js
@@ -1,7 +1,6 @@
// Gemini helper functions for translator
// Unsupported JSON Schema constraints that should be removed for Antigravity
-// Reference: CLIProxyAPI/internal/util/gemini_schema.go (removeUnsupportedKeywords)
export const UNSUPPORTED_SCHEMA_CONSTRAINTS = [
// Basic constraints (not supported by Gemini API)
"minLength", "maxLength", "exclusiveMinimum", "exclusiveMaximum",
@@ -270,7 +269,6 @@ function flattenTypeArrays(obj) {
}
// Clean JSON Schema for Antigravity API compatibility - removes unsupported keywords recursively
-// Reference: CLIProxyAPI/internal/util/gemini_schema.go
export function cleanJSONSchemaForAntigravity(schema) {
if (!schema || typeof schema !== "object") return schema;
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js
index 616068ef..19b970e4 100644
--- a/src/app/(dashboard)/dashboard/providers/[id]/page.js
+++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js
@@ -865,7 +865,7 @@ export default function ProviderDetailPage() {
{/* Thinking config */}
- {thinkingConfig && (
+ {/* {thinkingConfig && (
Thinking
- )}
+ )} */}
{/* Round Robin toggle */}
Round Robin
diff --git a/src/app/api/oauth/[provider]/[action]/route.js b/src/app/api/oauth/[provider]/[action]/route.js
index 660f8030..3e496221 100644
--- a/src/app/api/oauth/[provider]/[action]/route.js
+++ b/src/app/api/oauth/[provider]/[action]/route.js
@@ -7,6 +7,7 @@ import {
pollForToken
} from "@/lib/oauth/providers";
import { createProviderConnection } from "@/models";
+import { startCodexProxy, stopCodexProxy } from "@/lib/oauth/utils/server";
/**
* Dynamic OAuth API Route
@@ -30,6 +31,26 @@ export async function GET(request, { params }) {
return NextResponse.json(authData);
}
+ if (action === "start-proxy") {
+ if (provider !== "codex") {
+ return NextResponse.json({ error: "Proxy only supported for codex" }, { status: 400 });
+ }
+ const appPort = searchParams.get("app_port");
+ if (!appPort) {
+ return NextResponse.json({ error: "Missing app_port" }, { status: 400 });
+ }
+ const result = await startCodexProxy(Number(appPort));
+ return NextResponse.json(result);
+ }
+
+ if (action === "stop-proxy") {
+ if (provider !== "codex") {
+ return NextResponse.json({ error: "Proxy only supported for codex" }, { status: 400 });
+ }
+ stopCodexProxy();
+ return NextResponse.json({ success: true });
+ }
+
if (action === "device-code") {
const providerData = getProvider(provider);
if (providerData.flowType !== "device_code") {
diff --git a/src/lib/oauth/utils/server.js b/src/lib/oauth/utils/server.js
index e3013fc6..21ee9b2d 100644
--- a/src/lib/oauth/utils/server.js
+++ b/src/lib/oauth/utils/server.js
@@ -114,3 +114,71 @@ export function waitForCallback(timeoutMs = 300000) {
});
}
+// Singleton proxy server for Codex OAuth callback on fixed port
+let codexProxyServer = null;
+let codexProxyTimeout = null;
+
+const CODEX_PROXY_TIMEOUT_MS = 300000; // 5 minutes
+
+/**
+ * Start a proxy server on Codex fixed port (1455) that redirects callback to the app port.
+ * Returns { success: true } if started, or { success: false } if port is busy.
+ */
+export function startCodexProxy(appPort) {
+ return new Promise((resolve) => {
+ // Already running
+ if (codexProxyServer) {
+ resolve({ success: true });
+ return;
+ }
+
+ const CODEX_PORT = 1455;
+ const server = http.createServer((req, res) => {
+ const url = new URL(req.url, "http://localhost");
+
+ if (url.pathname === "/callback" || url.pathname === "/auth/callback") {
+ // Redirect to app port with all query params preserved
+ const redirectUrl = `http://localhost:${appPort}/callback${url.search}`;
+ res.writeHead(302, { Location: redirectUrl });
+ res.end();
+
+ // Auto-close after redirect
+ stopCodexProxy();
+ return;
+ }
+
+ res.writeHead(404);
+ res.end("Not found");
+ });
+
+ server.listen(CODEX_PORT, "127.0.0.1", () => {
+ codexProxyServer = server;
+ // Auto-cleanup after timeout
+ codexProxyTimeout = setTimeout(() => stopCodexProxy(), CODEX_PROXY_TIMEOUT_MS);
+ resolve({ success: true });
+ });
+
+ server.on("error", (err) => {
+ if (err.code === "EADDRINUSE") {
+ resolve({ success: false, reason: "port_busy" });
+ } else {
+ resolve({ success: false, reason: err.message });
+ }
+ });
+ });
+}
+
+/**
+ * Stop the Codex proxy server and cleanup
+ */
+export function stopCodexProxy() {
+ if (codexProxyTimeout) {
+ clearTimeout(codexProxyTimeout);
+ codexProxyTimeout = null;
+ }
+ if (codexProxyServer) {
+ codexProxyServer.close();
+ codexProxyServer = null;
+ }
+}
+
diff --git a/src/mitm/manager.js b/src/mitm/manager.js
index 047208da..897145ab 100644
--- a/src/mitm/manager.js
+++ b/src/mitm/manager.js
@@ -40,8 +40,8 @@ const MITM_PORT = 443;
const MITM_WIN_NODE_PORT = 8443;
const PID_FILE = path.join(MITM_DIR, ".mitm.pid");
-const MITM_MAX_RESTARTS = 2;
-const MITM_RESTART_DELAYS_MS = [5000, 10000];
+const MITM_MAX_RESTARTS = 5;
+const MITM_RESTART_DELAYS_MS = [5000, 10000, 20000, 30000, 60000];
const MITM_RESTART_RESET_MS = 60000;
let mitmRestartCount = 0;
@@ -323,8 +323,7 @@ async function scheduleMitmRestart(apiKey) {
if (aliveMs >= MITM_RESTART_RESET_MS) mitmRestartCount = 0;
if (mitmRestartCount >= MITM_MAX_RESTARTS) {
- err("Max restart attempts reached. Auto-disabling MITM.");
- await saveMitmSettings(false, null);
+ err("Max restart attempts reached. Giving up.");
return;
}
diff --git a/src/shared/components/OAuthModal.js b/src/shared/components/OAuthModal.js
index 475b7663..6fb36096 100644
--- a/src/shared/components/OAuthModal.js
+++ b/src/shared/components/OAuthModal.js
@@ -155,14 +155,23 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
}
// Authorization code flow - build redirect URI (some providers require fixed ports)
+ const appPort = window.location.port || (window.location.protocol === "https:" ? "443" : "80");
let redirectUri;
+ let codexProxyActive = false;
+
if (provider === "codex") {
- // Codex requires fixed port 1455
+ // Try to start proxy on fixed port 1455 → redirect callback to app port
+ try {
+ const proxyRes = await fetch(`/api/oauth/codex/start-proxy?app_port=${appPort}`);
+ const proxyData = await proxyRes.json();
+ codexProxyActive = proxyData.success;
+ } catch {
+ codexProxyActive = false;
+ }
+ // Always use fixed port 1455 as redirect_uri (Codex requirement)
redirectUri = "http://localhost:1455/auth/callback";
} else {
- // Use app's current port for OAuth callback
- const port = window.location.port || (window.location.protocol === "https:" ? "443" : "80");
- redirectUri = `http://localhost:${port}/callback`;
+ redirectUri = `http://localhost:${appPort}/callback`;
}
// Build authorize URL, optionally passing provider-specific metadata (e.g. gitlab clientId)
@@ -177,16 +186,21 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
setAuthData({ ...data, redirectUri });
- // For Codex or non-localhost: use manual input mode
- if (provider === "codex" || !isLocalhost) {
+ if (provider === "codex" && codexProxyActive) {
+ // Proxy active: callback will redirect to app port automatically
+ setStep("waiting");
+ popupRef.current = window.open(data.authUrl, "oauth_popup", "width=600,height=700");
+ if (!popupRef.current) {
+ setStep("input");
+ }
+ } else if (!isLocalhost || provider === "codex") {
+ // Non-localhost or proxy failed: manual input mode
setStep("input");
window.open(data.authUrl, "_blank");
} else {
// Localhost (non-Codex): Open popup and wait for message
setStep("waiting");
popupRef.current = window.open(data.authUrl, "oauth_popup", "width=600,height=700");
-
- // Check if popup was blocked
if (!popupRef.current) {
setStep("input");
}
@@ -209,8 +223,11 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
pollingAbortRef.current = false;
startOAuthFlow();
} else if (!isOpen) {
- // Abort polling when modal closes
+ // Abort polling and cleanup proxy when modal closes
pollingAbortRef.current = true;
+ if (provider === "codex") {
+ fetch("/api/oauth/codex/stop-proxy").catch(() => {});
+ }
}
}, [isOpen, provider, startOAuthFlow]);
@@ -319,10 +336,13 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
}
};
- // Clear session on modal close
+ // Clear session on modal close + cleanup proxy
const handleClose = useCallback(() => {
+ if (provider === "codex") {
+ fetch("/api/oauth/codex/stop-proxy").catch(() => {});
+ }
onClose();
- }, [onClose]);
+ }, [onClose, provider]);
if (!provider || !providerInfo) return null;
diff --git a/src/sse/services/tokenRefresh.js b/src/sse/services/tokenRefresh.js
index 88da437c..b6e88381 100644
--- a/src/sse/services/tokenRefresh.js
+++ b/src/sse/services/tokenRefresh.js
@@ -20,7 +20,8 @@ import {
refreshTokenByProvider as _refreshTokenByProvider,
formatProviderCredentials as _formatProviderCredentials,
getAllAccessTokens as _getAllAccessTokens,
- refreshKiroToken as _refreshKiroToken
+ refreshKiroToken as _refreshKiroToken,
+ getRefreshLeadMs as _getRefreshLeadMs
} from "open-sse/services/tokenRefresh.js";
export const TOKEN_EXPIRY_BUFFER_MS = BUFFER_MS;
@@ -196,10 +197,12 @@ export async function checkAndRefreshToken(provider, credentials) {
const now = Date.now();
const remaining = expiresAt - now;
- if (remaining < TOKEN_EXPIRY_BUFFER_MS) {
+ const refreshLead = _getRefreshLeadMs(provider);
+ if (remaining < refreshLead) {
log.info("TOKEN_REFRESH", "Token expiring soon, refreshing proactively", {
provider,
expiresIn: Math.round(remaining / 1000),
+ refreshLeadMs: refreshLead,
});
const newCreds = await getAccessToken(provider, creds);
diff --git a/tests/unit/codex-refresh-token.test.js b/tests/unit/codex-refresh-token.test.js
new file mode 100644
index 00000000..838c5fbf
--- /dev/null
+++ b/tests/unit/codex-refresh-token.test.js
@@ -0,0 +1,83 @@
+/**
+ * Unit tests for Codex (OpenAI) refresh token mechanism
+ *
+ * Verifies that:
+ * - Early refresh lead times are configured per provider (synced with CLIProxyAPI)
+ * - New refresh_token from response is persisted (token rotation)
+ * - Falls back to old refresh_token when server doesn't return new one
+ */
+
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+
+const originalFetch = global.fetch;
+
+describe("Codex Refresh Token", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ afterEach(() => {
+ global.fetch = originalFetch;
+ });
+
+ describe("refreshCodexToken", () => {
+ it("should return new refresh_token when server provides one (token rotation)", async () => {
+ global.fetch = vi.fn().mockResolvedValue({
+ ok: true,
+ json: () => Promise.resolve({
+ access_token: "new-access",
+ refresh_token: "rotated-refresh-token",
+ expires_in: 3600,
+ }),
+ });
+
+ const { refreshCodexToken } = await import("../../open-sse/services/tokenRefresh.js");
+ const result = await refreshCodexToken("old-refresh-token", null);
+
+ expect(result.refreshToken).toBe("rotated-refresh-token");
+ expect(result.accessToken).toBe("new-access");
+ });
+
+ it("should keep old refresh_token when server does not return new one", async () => {
+ global.fetch = vi.fn().mockResolvedValue({
+ ok: true,
+ json: () => Promise.resolve({
+ access_token: "new-access",
+ expires_in: 3600,
+ }),
+ });
+
+ const { refreshCodexToken } = await import("../../open-sse/services/tokenRefresh.js");
+ const result = await refreshCodexToken("old-refresh-token", null);
+
+ expect(result.refreshToken).toBe("old-refresh-token");
+ });
+ });
+
+ describe("getRefreshLeadMs (early refresh config)", () => {
+ it("should return provider-specific lead time for OAuth providers", async () => {
+ const { getRefreshLeadMs } = await import("../../open-sse/services/tokenRefresh.js");
+
+ // Synced with CLIProxyAPI refresh_registry
+ expect(getRefreshLeadMs("codex")).toBe(5 * 24 * 60 * 60 * 1000); // 5 days
+ expect(getRefreshLeadMs("claude")).toBe(4 * 60 * 60 * 1000); // 4 hours
+ expect(getRefreshLeadMs("iflow")).toBe(24 * 60 * 60 * 1000); // 24 hours
+ expect(getRefreshLeadMs("qwen")).toBe(20 * 60 * 1000); // 20 minutes
+ expect(getRefreshLeadMs("kimi-coding")).toBe(5 * 60 * 1000); // 5 minutes
+ expect(getRefreshLeadMs("antigravity")).toBe(5 * 60 * 1000); // 5 minutes
+ });
+
+ it("should fallback to default buffer for unknown providers", async () => {
+ const { getRefreshLeadMs, TOKEN_EXPIRY_BUFFER_MS } = await import("../../open-sse/services/tokenRefresh.js");
+
+ expect(getRefreshLeadMs("unknown-provider")).toBe(TOKEN_EXPIRY_BUFFER_MS);
+ expect(getRefreshLeadMs("openai")).toBe(TOKEN_EXPIRY_BUFFER_MS);
+ });
+
+ it("codex lead should be greater than default buffer", async () => {
+ const { getRefreshLeadMs, TOKEN_EXPIRY_BUFFER_MS } = await import("../../open-sse/services/tokenRefresh.js");
+
+ expect(getRefreshLeadMs("codex")).toBeGreaterThan(TOKEN_EXPIRY_BUFFER_MS);
+ });
+ });
+});