mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-24 08:32:13 +03:00
Compare commits
3 Commits
fix/12958-
...
fix/13232-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bd058824b | ||
|
|
a24562ece3 | ||
|
|
f0d2d34ee5 |
@@ -1 +0,0 @@
|
||||
- **fix(providers):** GitLab Duo Retest and chat requests now fall back to the public Code Suggestions endpoint for ANY `direct_access` 403 (not only the "direct connections are disabled" tenant-config message), and surface the real upstream error body instead of a generic "Access denied" when both endpoints reject the token (#12958) — thanks @Rahulsharma0810
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(sse):** classify a missing Playwright Chromium install on the Z.ai web transport as an actionable 503 host/config cooldown instead of a generic 502 that trips the provider circuit breaker (#13232) — thanks @oleksandr1811
|
||||
18
open-sse/executors/browserExecutableCheck.ts
Normal file
18
open-sse/executors/browserExecutableCheck.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Shared classification for browser-backed executors: distinguishes a missing Playwright
|
||||
* Chromium binary (`chromium.launch: Executable doesn't exist at ...`) from a transient upstream
|
||||
* fault. This is a host/config problem, not something a retry loop can fix, so executors must
|
||||
* NOT surface it as a plain retryable 5xx (which marks the account unavailable / trips the
|
||||
* provider circuit breaker). Originally added for `gemini-web.ts` (#3516); extracted here so
|
||||
* every browser-backed executor (Gemini Web, Z.ai Web, ...) can share the same detection.
|
||||
*/
|
||||
export function isMissingBrowserExecutable(message: string): boolean {
|
||||
if (!message) return false;
|
||||
const lower = message.toLowerCase();
|
||||
return (
|
||||
lower.includes("executable doesn't exist") ||
|
||||
lower.includes("executablenotfound") ||
|
||||
lower.includes("playwright install") ||
|
||||
(lower.includes("chromium") && lower.includes("download"))
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
import { BaseExecutor, type ExecuteInput } from "./base.ts";
|
||||
import { buildErrorBody, sanitizeErrorMessage } from "../utils/error.ts";
|
||||
import { isMissingBrowserExecutable } from "./browserExecutableCheck.ts";
|
||||
import { normalizeGeminiCookieInput } from "../utils/geminiCookies.ts";
|
||||
import { prepareToolMessages } from "../translator/webTools.ts";
|
||||
import { buildToolModeResponse } from "./chatgptWebTools.ts";
|
||||
@@ -27,22 +28,12 @@ import {
|
||||
|
||||
const GEMINI_URL = "https://gemini.google.com/app";
|
||||
|
||||
/**
|
||||
* Whether an error came from Playwright failing to launch because the browser binary is not
|
||||
* installed (`chromium.launch: Executable doesn't exist at ...`). This is a host/config
|
||||
* problem, not a transient upstream fault, so the executor must NOT surface it as a retryable
|
||||
* 500 (which marks the account unavailable and loops / trips the provider breaker). See #3516.
|
||||
*/
|
||||
export function isMissingBrowserExecutable(message: string): boolean {
|
||||
if (!message) return false;
|
||||
const lower = message.toLowerCase();
|
||||
return (
|
||||
lower.includes("executable doesn't exist") ||
|
||||
lower.includes("executablenotfound") ||
|
||||
lower.includes("playwright install") ||
|
||||
(lower.includes("chromium") && lower.includes("download"))
|
||||
);
|
||||
}
|
||||
// Re-exported for backward compatibility: some tests/callers import this classification helper
|
||||
// from gemini-web.ts, its original home (#3516). The implementation now lives in
|
||||
// browserExecutableCheck.ts so other browser-backed executors (e.g. zai-web.ts, #13232) can
|
||||
// share it without importing this whole executor module.
|
||||
export { isMissingBrowserExecutable } from "./browserExecutableCheck.ts";
|
||||
|
||||
const GEMINI_USER_AGENT =
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36";
|
||||
|
||||
|
||||
@@ -599,17 +599,12 @@ export class GitlabExecutor extends BaseExecutor {
|
||||
};
|
||||
}
|
||||
|
||||
// #12958: any direct_access 403 (not only GitLab's exact "direct connections
|
||||
// are disabled" tenant-config message) is recoverable via the public
|
||||
// completions fallback — mirrors the 401 branch above and the connection-test
|
||||
// path's shouldFallbackToPublicCodeSuggestions() contract.
|
||||
if (response.status === 403 && input.log) {
|
||||
input.log.warn(
|
||||
"GITLAB-DUO",
|
||||
isGitLabDirectAccessDisabled(response.status, bodyText)
|
||||
? "direct_access exchange rejected (403, direct connections disabled); falling back to public completions endpoint"
|
||||
: `direct_access exchange rejected (403); falling back to public completions endpoint. Body: ${bodyText.slice(0, 500)}`
|
||||
);
|
||||
if (response.status === 403 && !isGitLabDirectAccessDisabled(response.status, bodyText)) {
|
||||
return {
|
||||
target: null,
|
||||
credentials,
|
||||
errorResponse: toOpenAIError(403, "GitLab Duo direct access scope is unavailable"),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
makeZaiChunkEmitter,
|
||||
} from "./zai-web/stream.ts";
|
||||
import { browserBackedChat } from "../services/browserBackedChat.ts";
|
||||
import { isMissingBrowserExecutable } from "./browserExecutableCheck.ts";
|
||||
import { CursorImageError, resolveCursorImages } from "../utils/cursorImages.ts";
|
||||
import {
|
||||
makeExecutorErrorResult as makeErrorResult,
|
||||
@@ -424,9 +425,26 @@ export class ZaiWebExecutor extends BaseExecutor {
|
||||
try {
|
||||
result = await browserBackedChat(buildZaiBrowserChatOptions({ ...input, attachments }));
|
||||
} catch (error) {
|
||||
const message = sanitizeErrorMessage(
|
||||
error instanceof Error ? error.message : "browser transport unavailable"
|
||||
);
|
||||
const rawMessage = error instanceof Error ? error.message : "browser transport unavailable";
|
||||
// #13232: a missing Playwright browser binary is a host/config problem, not a transient
|
||||
// upstream fault (same class as #3516 in gemini-web.ts). Surface an actionable message and
|
||||
// tag it with the connection-cooldown hint so accountFallback skips the whole-provider
|
||||
// circuit breaker (502/500 would trip it) and applies a short, non-exponential cooldown
|
||||
// instead.
|
||||
if (isMissingBrowserExecutable(rawMessage)) {
|
||||
return {
|
||||
errorResult: makeErrorResult(
|
||||
503,
|
||||
"Z.ai requires the Playwright Chromium browser, which is not installed. " +
|
||||
"Run `npx playwright install chromium` on the host (or rebuild the Docker image " +
|
||||
"with browsers).",
|
||||
input.body,
|
||||
ZAI_CHAT_URL,
|
||||
{ "X-Omni-Fallback-Hint": "connection_cooldown" }
|
||||
),
|
||||
};
|
||||
}
|
||||
const message = sanitizeErrorMessage(rawMessage);
|
||||
return {
|
||||
errorResult: makeErrorResult(
|
||||
502,
|
||||
|
||||
@@ -1134,7 +1134,8 @@ export function makeExecutorErrorResult(
|
||||
status: number,
|
||||
message: string,
|
||||
body: unknown,
|
||||
url: string
|
||||
url: string,
|
||||
extraResponseHeaders?: Record<string, string>
|
||||
) {
|
||||
return {
|
||||
response: new Response(
|
||||
@@ -1145,7 +1146,10 @@ export function makeExecutorErrorResult(
|
||||
code: `HTTP_${status}`,
|
||||
},
|
||||
}),
|
||||
{ status, headers: { "Content-Type": "application/json" } }
|
||||
{
|
||||
status,
|
||||
headers: { "Content-Type": "application/json", ...extraResponseHeaders },
|
||||
}
|
||||
),
|
||||
url,
|
||||
headers: {} as Record<string, string>,
|
||||
|
||||
@@ -239,16 +239,6 @@ function isTokenExpired(connection: any) {
|
||||
return expiresAt <= Date.now() + buffer;
|
||||
}
|
||||
|
||||
// #12958: GitLab's own `direct_access` 403 JSON body (e.g. `{"error":"insufficient_scope"}`)
|
||||
// is safe operator-facing diagnostic text — it is not a stack trace and does not echo the
|
||||
// token — but is capped and stripped of control characters defensively before it reaches
|
||||
// the stored/surfaced error message, per docs/security/ERROR_SANITIZATION.md.
|
||||
function sanitizeUpstreamBodyText(bodyText: string): string {
|
||||
const collapsed = bodyText.replace(/[\r\n\t | ||||