diff --git a/.agents/workflows/generate-release.md b/.agents/workflows/generate-release.md index 3747c54f..9c2a78ee 100644 --- a/.agents/workflows/generate-release.md +++ b/.agents/workflows/generate-release.md @@ -176,24 +176,17 @@ Inform the user: > Run these steps only AFTER the user has merged the PR. -### 11. Pull main and create tag +### 11. Create Git Tag and GitHub Release (MANDATORY) + +// turbo ```bash git checkout main git pull origin main -git tag -a v2.x.y -m "Release v2.x.y" -``` - -### 12. Push tag to GitHub - -```bash +VERSION=$(node -p "require('./package.json').version") +git tag -a "v$VERSION" -m "Release v$VERSION" git push origin --tags -``` - -### 13. Create GitHub release - -```bash -gh release create v2.x.y --title "v2.x.y — summary" --notes "..." +gh release create "v$VERSION" --title "v$VERSION" --notes "OmniRoute v$VERSION Release" --target main ``` ### 14. 🐳 Trigger Docker Hub build (MANDATORY — keep npm and Docker in sync) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ae3a461..63580ae7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ --- +## [3.3.10] - 2026-03-31 + +### 🚀 Features + +- **Model Registry Update:** Injected `gpt-5.4-mini` into the Codex provider's array of models (#756) + +### 🐛 Bug Fixes + +- **Qwen Auth Routing:** Re-routed Qwen OAuth completions from the DashScope API to the Web Inference API (`chat.qwen.ai`), resolving authorization failures (#844, #807, #832) +- **Qwen Auto-Retry Loop:** Added targeted 429 Quota Exceeded backoff handling inside `chatCore` protecting burst requests +- **Codex OAuth Fallback:** Modern browser popup blocking no longer traps the user; it automatically falls back to manual URL entry (#808) +- **Claude Token Refresh:** Anthropic's strict `application/json` boundaries are now respected during token generation instead of encoded URLs (#836) +- **Codex Messages Schema:** Stripped purist `messages` injects from native passthrough requests to avoid structural rejections from the ChatGPT upstream (#806) +- **CLI Detection Size Limit:** Safely bumped the Node binary scanning upper bound from 100MB to 350MB, allowing heavy standalone tools like Claude Code (229MB) and OpenCode (153MB) to be correctly detected by the VPS runtime (#809) +- **Nvidia Header Conflicts:** Removed `prompt_cache_key` properties from upstream headers when calling non-Anthropic providers (#848) + +--- + ## [3.3.9] - 2026-03-31 ### 🐛 Bug Fixes diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 4d69ded5..7b38617d 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -264,6 +264,7 @@ export const REGISTRY: Record = { }, models: [ { id: "gpt-5.4", name: "GPT 5.4" }, + { id: "gpt-5.4-mini", name: "GPT 5.4 Mini" }, { id: "gpt-5.3-codex", name: "GPT 5.3 Codex" }, { id: "gpt-5.3-codex-xhigh", name: "GPT 5.3 Codex (xHigh)" }, { id: "gpt-5.3-codex-high", name: "GPT 5.3 Codex (High)" }, @@ -286,7 +287,7 @@ export const REGISTRY: Record = { alias: "qw", format: "openai", executor: "default", - baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions", + baseUrl: "https://chat.qwen.ai/api/v1/services/aigc/text-generation/generation", authType: "oauth", authHeader: "bearer", headers: { diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts index 7bf1ef62..24877df5 100644 --- a/open-sse/executors/codex.ts +++ b/open-sse/executors/codex.ts @@ -270,6 +270,11 @@ export class CodexExecutor extends BaseExecutor { // Ensure store is false (Codex requirement) body.store = false; + // Issue #806: Even for native passthrough, some clients (purist completions) might indiscriminately inject + // a `messages` or `prompt` array which the strict Codex Responses schema rejects. + delete body.messages; + delete body.prompt; + if (nativeCodexPassthrough) { return body; } diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 5c3312ab..29fa3d2f 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -959,7 +959,8 @@ export async function handleChatCore({ if ( targetFormat === FORMATS.OPENAI && !bodyToSend.prompt_cache_key && - Array.isArray(bodyToSend.messages) + Array.isArray(bodyToSend.messages) && + !["nvidia", "codex", "xai"].includes(provider) ) { const { generatePromptCacheKey } = await import("@/lib/promptCache"); const cacheKey = generatePromptCacheKey(bodyToSend.messages); @@ -968,18 +969,39 @@ export async function handleChatCore({ } } - const rawResult = await withRateLimit(provider, connectionId, modelToCall, () => - executor.execute({ - model: modelToCall, - body: bodyToSend, - stream, - credentials: getExecutionCredentials(), - signal: streamController.signal, - log, - extendedContext, - upstreamExtraHeaders: buildUpstreamHeadersForExecute(modelToCall), - }) - ); + const rawResult = await withRateLimit(provider, connectionId, modelToCall, async () => { + let attempts = 0; + const maxAttempts = provider === "qwen" ? 3 : 1; + + while (attempts < maxAttempts) { + const res = await executor.execute({ + model: modelToCall, + body: bodyToSend, + stream, + credentials: getExecutionCredentials(), + signal: streamController.signal, + log, + extendedContext, + upstreamExtraHeaders: buildUpstreamHeadersForExecute(modelToCall), + }); + + // Qwen 429 strict quota backoff (wait 1.5s, 3s and retry) + if (provider === "qwen" && res.response.status === 429 && attempts < maxAttempts - 1) { + const bodyPeek = await res.response + .clone() + .text() + .catch(() => ""); + if (bodyPeek.toLowerCase().includes("exceeded your current quota")) { + const delay = 1500 * (attempts + 1); + log?.warn?.("QWEN_RETRY", `Quota 429 hit. Retrying in ${delay}ms...`); + await new Promise((r) => setTimeout(r, delay)); + attempts++; + continue; + } + } + return res; + } + }); if (stream) return rawResult; diff --git a/src/shared/components/OAuthModal.tsx b/src/shared/components/OAuthModal.tsx index b4661299..565f14fd 100644 --- a/src/shared/components/OAuthModal.tsx +++ b/src/shared/components/OAuthModal.tsx @@ -225,7 +225,12 @@ export default function OAuthModal({ setAuthData({ ...serverData, redirectUri: serverData.redirectUri }); setStep("waiting"); - window.open(serverData.authUrl, "oauth_auth"); + popupRef.current = window.open(serverData.authUrl, "oauth_auth"); + + // If browser blocked the popup, switch to manual input step immediately + if (!popupRef.current) { + setStep("input"); + } setPolling(true); const maxAttempts = 150; diff --git a/src/shared/services/cliRuntime.ts b/src/shared/services/cliRuntime.ts index ff658bce..8127ce29 100644 --- a/src/shared/services/cliRuntime.ts +++ b/src/shared/services/cliRuntime.ts @@ -591,10 +591,11 @@ const checkKnownPath = async (commandPath: string) => { return { installed: false, commandPath: null, reason: "not_file" }; } - // CLI binaries should be > 30 bytes and < 100MB + // CLI binaries should be > 30 bytes and < 350MB // npm .cmd wrappers on Windows are ~300-500 bytes, JS wrappers on Linux can be ~44 bytes // Minimum catches empty/suspicious files while allowing legitimate thin wrappers - if (stat.size < 30 || stat.size > 100 * 1024 * 1024) { + // Many modern CLIs (like Claude Code and OpenCode) build as single ~150-250MB binaries + if (stat.size < 30 || stat.size > 350 * 1024 * 1024) { return { installed: false, commandPath: null, reason: "suspicious_size" }; } } catch (error) { @@ -787,7 +788,10 @@ export const getCliRuntimeStatus = async (toolId: string) => { }; } - const located = await locateCommandCandidate(commands, env, toolId); + const envCommand = String(process.env[tool.envBinKey] || "").trim(); + const hasEnvOverride = !!envCommand; + + const located = await locateCommandCandidate(commands, env, hasEnvOverride ? undefined : toolId); const command = located.command; if (!located.installed) { diff --git a/tests/unit/cli-runtime-detection.test.mjs b/tests/unit/cli-runtime-detection.test.mjs index cc1cec39..74206a03 100644 --- a/tests/unit/cli-runtime-detection.test.mjs +++ b/tests/unit/cli-runtime-detection.test.mjs @@ -15,7 +15,11 @@ const { getCliRuntimeStatus, CLI_TOOL_IDS } = // ─── Helpers ────────────────────────────────────────────────── function createTempDir() { - return fs.mkdtempSync(path.join(os.tmpdir(), "cli-test-")); + const testRoot = path.join(os.homedir(), ".omniroute-test-tmp"); + if (!fs.existsSync(testRoot)) { + fs.mkdirSync(testRoot, { recursive: true }); + } + return fs.mkdtempSync(path.join(testRoot, "cli-test-")); } function createFile(dir, name, content) { @@ -64,11 +68,11 @@ describe("Size threshold — checkKnownPath", () => { it("should detect files >= 30 bytes via env var", async () => { const prev = process.env.CLI_DROID_BIN; - // Create a valid 30-byte+ script + // Create a valid 30-byte+ script (using spaces/comments for padding, NO \r on linux) const content = process.platform === "win32" - ? "@echo off\r\necho 1.0.0\r\nexit 0\r\n" - : "#!/bin/sh\r\necho 1.0.0\r\nexit 0\r\n"; + ? "@echo off\r\necho 1.0.0\r\nREM PADDING_PADDIN\r\nexit 0\r\n" + : "#!/bin/sh\necho 1.0.0\n# PADDING_PADDING_PAD\nexit 0\n"; const script = createFile(tmpDir, "droid-valid", content); // Verify it's at least 30 bytes const stat = fs.statSync(script); @@ -87,10 +91,15 @@ describe("Size threshold — checkKnownPath", () => { it("should detect a valid CLI script (>= 30 bytes) via env var", async () => { const prev = process.env.CLI_DROID_BIN; + // Ensure the size stays > 30 bytes without \r\n on bash + const content = + process.platform === "win32" + ? "@echo off\r\necho 1.0.0\r\nREM PADDING_PAD\r\n" + : "#!/bin/sh\necho 1.0.0\n# PADDING_PADDING_PAD\n"; const script = process.platform === "win32" - ? createFile(tmpDir, "droid.cmd", "@echo off\necho 1.0.0\n") - : createFile(tmpDir, "droid", "#!/bin/sh\necho 1.0.0\n"); + ? createFile(tmpDir, "droid.cmd", content) + : createFile(tmpDir, "droid", content); process.env.CLI_DROID_BIN = script; try { diff --git a/tests/unit/t28-model-catalog-updates.test.mjs b/tests/unit/t28-model-catalog-updates.test.mjs index e4a38982..67750d0a 100644 --- a/tests/unit/t28-model-catalog-updates.test.mjs +++ b/tests/unit/t28-model-catalog-updates.test.mjs @@ -22,10 +22,10 @@ test("T28: antigravity static catalog includes Gemini 3.1 preview fallbacks", () assert.ok(staticIds.includes("gemini-3.1-flash-lite-preview")); }); -test("T28: qwen registry uses DashScope-compatible base URL", () => { +test("T28: qwen registry uses native chat.qwen.ai base URL", () => { assert.equal( REGISTRY.qwen.baseUrl, - "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions" + "https://chat.qwen.ai/api/v1/services/aigc/text-generation/generation" ); });