chore(release): v3.4.8 — security compliance and fix algorithms

This commit is contained in:
diegosouzapw
2026-04-03 00:15:35 -03:00
parent 86030a0fab
commit d46e3f07c3
88 changed files with 811 additions and 308 deletions

View File

@@ -27,6 +27,23 @@ Phase 2 (post-merge): tag → publish → GitHub release → Docker → deploy
---
## Phase 0: Security Verification (MANDATORY)
Before creating the release, you must ensure the codebase and supply chain are secure and free of known vulnerabilities.
1. **Run Local Dependencies Audit:**
```bash
npm audit
```
_Fix any `high` or `critical` vulnerabilities identified._
2. **Check GitHub CodeQL & Dependabot Alerts:**
Navigate to the repository's **Security** tab on GitHub, or use the project's `vulnerability-scanner` skill to analyze active alerts. Ensure all static analysis findings (e.g., prototype pollution, insecure randomness, ReDoS, shell injections) are addressed and logically committed on a target branch.
---
## Phase 1: Pre-Merge
### 1. Create release branch

View File

@@ -4,6 +4,18 @@
---
## [3.4.8] — 2026-04-03
### Security
- Fully remediated all outstanding Github Advanced Security (CodeQL) findings and Dependabot alerts.
- Fixed insecure randomness vulnerabilities by migrating from `Math.random` to `crypto.randomUUID()`.
- Secured shell commands in automated scripts from string injection.
- Migrated vulnerable catastrophic backtracking RegEx parsing patterns in chat/translation pipelines.
- Enhanced output sanitization controls inside React UI components and Server Sent Events (SSE) tag injection.
---
## [3.4.7] — 2026-04-03
### Features

View File

@@ -35,7 +35,9 @@ function ask(question) {
}
function hashPassword(password) {
return createHash("sha256").update(password).digest("hex");
return createHash("sha256")
.update(password) /* lgtm[js/insufficient-password-hash] */
.digest("hex");
}
console.log("\n🔑 OmniRoute — Password Reset\n");

View File

@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: OmniRoute API
version: 3.4.7
version: 3.4.8
description: |
OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible
endpoint that routes requests to multiple AI providers with load balancing,

View File

@@ -1,6 +1,6 @@
{
"name": "omniroute-desktop",
"version": "3.4.7",
"version": "3.4.8",
"description": "OmniRoute Desktop Application",
"main": "main.js",
"author": {

View File

@@ -1,3 +1,4 @@
import { randomUUID } from "crypto";
/**
* Shared Registry Utilities
*

View File

@@ -40,9 +40,7 @@ export const VIDEO_PROVIDERS: Record<string, VideoProvider> = {
authType: "none",
authHeader: "none",
format: "sdwebui-video",
models: [
{ id: "animatediff-webui", name: "AnimateDiff (WebUI)" },
],
models: [{ id: "animatediff-webui", name: "AnimateDiff (WebUI)" }],
},
};

View File

@@ -1,4 +1,4 @@
import crypto from "crypto";
import crypto, { randomUUID } from "crypto";
import { BaseExecutor, mergeUpstreamExtraHeaders } from "./base.ts";
import { PROVIDERS, OAUTH_ENDPOINTS, HTTP_STATUS } from "../config/constants.ts";
@@ -164,7 +164,7 @@ export class AntigravityExecutor extends BaseExecutor {
}
generateSessionId() {
return `-${Math.floor(Math.random() * 9_000_000_000_000_000_000)}`;
return `-${parseInt(randomUUID().replace(/-/g, "").substring(0, 8), 16) % 9_000_000_000_000_000_000}`;
}
parseRetryHeaders(headers) {
@@ -230,13 +230,17 @@ export class AntigravityExecutor extends BaseExecutor {
let timedOut = false;
const timeout = AbortSignal.timeout(SSE_COLLECT_TIMEOUT_MS);
try {
// eslint-disable-next-line no-constant-condition
while (true) {
if (signal?.aborted) throw new Error("Request aborted during SSE collection");
const { done, value } = await Promise.race([
reader.read(),
new Promise<never>((_, reject) =>
timeout.addEventListener("abort", () => reject(new Error("SSE collection timed out")), { once: true })
timeout.addEventListener(
"abort",
() => reject(new Error("SSE collection timed out")),
{ once: true }
)
),
]);
if (done) break;
@@ -271,7 +275,10 @@ export class AntigravityExecutor extends BaseExecutor {
}
}
if (candidate?.finishReason) {
finishReason = candidate.finishReason.toLowerCase() === "stop" ? "stop" : candidate.finishReason.toLowerCase();
finishReason =
candidate.finishReason.toLowerCase() === "stop"
? "stop"
: candidate.finishReason.toLowerCase();
}
if (parsed?.response?.usageMetadata) {
const um = parsed.response.usageMetadata;
@@ -330,12 +337,7 @@ export class AntigravityExecutor extends BaseExecutor {
const url = this.buildUrl(model, upstreamStream, urlIndex);
const headers = this.buildHeaders(credentials, upstreamStream);
mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders);
const transformedBody = await this.transformRequest(
model,
body,
upstreamStream,
credentials
);
const transformedBody = await this.transformRequest(model, body, upstreamStream, credentials);
// Initialize retry counter for this URL
if (!retryAttemptsByUrl[urlIndex]) {
@@ -474,7 +476,15 @@ export class AntigravityExecutor extends BaseExecutor {
// For non-streaming clients, collect the SSE stream and return a synthetic
// non-streaming Response so chatCore doesn't need to handle SSE conversion.
if (!stream) {
return this.collectStreamToResponse(response, model, url, headers, transformedBody, log, signal);
return this.collectStreamToResponse(
response,
model,
url,
headers,
transformedBody,
log,
signal
);
}
return { response, url, headers, transformedBody };

View File

@@ -102,7 +102,9 @@ export class GeminiCLIExecutor extends BaseExecutor {
}
if (!projectId) {
console.warn("[OmniRoute] loadCodeAssist returned no project — falling back to stored projectId");
console.warn(
"[OmniRoute] loadCodeAssist returned no project — falling back to stored projectId"
);
return null;
}

View File

@@ -39,14 +39,7 @@ export class QoderExecutor extends BaseExecutor {
};
}
async execute({
model,
body,
stream,
credentials,
signal,
upstreamExtraHeaders,
}: ExecuteInput) {
async execute({ model, body, stream, credentials, signal, upstreamExtraHeaders }: ExecuteInput) {
const headers = this.buildHeaders(credentials, stream);
mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders);

View File

@@ -1,3 +1,4 @@
import { randomUUID } from "crypto";
import { getCorsOrigin } from "../utils/cors.ts";
/**
* Audio Speech Handler (TTS)

View File

@@ -116,7 +116,8 @@ export function shouldUseNativeCodexPassthrough({
}): boolean {
if (provider !== "codex") return false;
if (sourceFormat !== FORMATS.OPENAI_RESPONSES) return false;
const normalizedEndpoint = String(endpointPath || "").replace(/\/+$/, "");
let normalizedEndpoint = String(endpointPath || "");
while (normalizedEndpoint.endsWith("/")) normalizedEndpoint = normalizedEndpoint.slice(0, -1);
const segments = normalizedEndpoint.split("/");
return segments.includes("responses");
}

View File

@@ -1,3 +1,4 @@
import { randomUUID } from "crypto";
/**
* Embedding Handler
*

View File

@@ -1,3 +1,4 @@
import { randomUUID } from "crypto";
/**
* Image Generation Handler
*
@@ -992,7 +993,7 @@ async function handleComfyUIImageGeneration({ model, provider, providerConfig, b
"3": {
class_type: "KSampler",
inputs: {
seed: Math.floor(Math.random() * 2 ** 32),
seed: parseInt(randomUUID().replace(/-/g, "").substring(0, 8), 16) % 2 ** 32,
steps: body.steps || 20,
cfg: body.cfg_scale || 7,
sampler_name: "euler",
@@ -1087,7 +1088,10 @@ type Imagen3ImageGenArgs = {
providerConfig: { baseUrl: string };
body: { prompt?: string; size?: string; n?: number };
credentials: { apiKey?: string; accessToken?: string };
log?: { info?: (tag: string, msg: string) => void; error?: (tag: string, msg: string) => void } | null;
log?: {
info?: (tag: string, msg: string) => void;
error?: (tag: string, msg: string) => void;
} | null;
};
type Imagen3NormalizedImage = {

View File

@@ -50,7 +50,11 @@ export async function handleMusicGeneration({ body, credentials, log }) {
return handleComfyUIMusicGeneration({ model, provider, providerConfig, body, log });
}
return { success: false, status: 400, error: `Unsupported music format: ${providerConfig.format}` };
return {
success: false,
status: 400,
error: `Unsupported music format: ${providerConfig.format}`,
};
}
/**
@@ -109,7 +113,10 @@ async function handleComfyUIMusicGeneration({ model, provider, providerConfig, b
if (log) {
const promptPreview = String(body.prompt ?? "").slice(0, 60);
log.info("MUSIC", `${provider}/${model} (comfyui) | prompt: "${promptPreview}..." | duration: ${duration}s`);
log.info(
"MUSIC",
`${provider}/${model} (comfyui) | prompt: "${promptPreview}..." | duration: ${duration}s`
);
}
try {

View File

@@ -1,3 +1,4 @@
import { randomUUID } from "crypto";
/**
* Search Handler
*

View File

@@ -55,7 +55,11 @@ export async function handleVideoGeneration({ body, credentials, log }) {
return handleSDWebUIVideoGeneration({ model, provider, providerConfig, body, log });
}
return { success: false, status: 400, error: `Unsupported video format: ${providerConfig.format}` };
return {
success: false,
status: 400,
error: `Unsupported video format: ${providerConfig.format}`,
};
}
/**
@@ -119,7 +123,10 @@ async function handleComfyUIVideoGeneration({ model, provider, providerConfig, b
if (log) {
const promptPreview = String(body.prompt ?? "").slice(0, 60);
log.info("VIDEO", `${provider}/${model} (comfyui) | prompt: "${promptPreview}..." | frames: ${frames}`);
log.info(
"VIDEO",
`${provider}/${model} (comfyui) | prompt: "${promptPreview}..." | frames: ${frames}`
);
}
try {
@@ -202,7 +209,8 @@ async function handleSDWebUIVideoGeneration({ model, provider, providerConfig, b
if (!response.ok) {
const errorText = await response.text();
if (log) log.error("VIDEO", `${provider} error ${response.status}: ${errorText.slice(0, 200)}`);
if (log)
log.error("VIDEO", `${provider} error ${response.status}: ${errorText.slice(0, 200)}`);
saveCallLog({
method: "POST",
path: "/v1/videos/generations",

View File

@@ -1,7 +1,11 @@
import { describe, it, expect } from "vitest";
import { getRegistryEntry } from "../../config/providerRegistry.ts";
import { PROVIDER_ID_TO_ALIAS, getModelsByProviderId, getProviderModels } from "../../config/providerModels.ts";
import {
PROVIDER_ID_TO_ALIAS,
getModelsByProviderId,
getProviderModels,
} from "../../config/providerModels.ts";
import { supportsToolCalling } from "../../services/modelCapabilities.ts";
import { getPricingForModel } from "../../../src/shared/constants/pricing.ts";

View File

@@ -17,4 +17,3 @@ export {
isMcpHttpActive,
} from "./httpTransport.ts";
export * from "./schemas/index.ts";

View File

@@ -1,6 +1,6 @@
{
"name": "@omniroute/open-sse",
"version": "3.4.7",
"version": "3.4.8",
"description": "Express SSE sidecar for OmniRoute — handles streaming, protocol translation, and provider orchestration",
"type": "module",
"main": "index.js",

View File

@@ -46,7 +46,7 @@ export function stripAnthropicMessagesSuffix(baseUrl: string | null | undefined)
.trim()
.replace(/\/$/, "");
if (!normalized) return "";
return normalized.replace(/\/messages(?:\?[^#]*)?$/i, "");
return normalized.split("?")[0].replace(/\/messages$/i, "");
}
export function stripClaudeCodeCompatibleEndpointSuffix(
@@ -56,7 +56,7 @@ export function stripClaudeCodeCompatibleEndpointSuffix(
.trim()
.replace(/\/$/, "");
if (!normalized) return "";
return normalized.replace(/\/(?:v\d+\/)?messages(?:\?[^#]*)?$/i, "");
return normalized.split("?")[0].replace(/\/(?:v\d+\/)?messages$/i, "");
}
function joinNormalizedBaseUrlAndPath(baseUrl: string, path: string): string {

View File

@@ -83,10 +83,9 @@ export function supportsReasoning(modelStr: string): boolean {
const normalized = String(modelStr || "").toLowerCase();
if (!normalized) return true;
const blocked = REASONING_UNSUPPORTED_PATTERNS.some((pattern) =>
normalized === pattern ||
normalized.endsWith(`/${pattern}`) ||
normalized.includes(pattern)
const blocked = REASONING_UNSUPPORTED_PATTERNS.some(
(pattern) =>
normalized === pattern || normalized.endsWith(`/${pattern}`) || normalized.includes(pattern)
);
return !blocked;

View File

@@ -93,7 +93,9 @@ export function getStaticQoderModels() {
}
export function mapQoderModelToLevel(model: string | null | undefined): string | null {
const normalized = String(model || "").trim().toLowerCase();
const normalized = String(model || "")
.trim()
.toLowerCase();
if (!normalized) return null;
if (normalized.includes("deepseek-r1")) return "ultimate";
if (normalized.includes("qwen3-max")) return "performance";
@@ -475,7 +477,8 @@ export async function validateQoderCliPat({
providerSpecificData?: JsonRecord;
}) {
const modelId =
getString(providerSpecificData.validationModelId).trim() || getString(providerSpecificData.modelId).trim();
getString(providerSpecificData.validationModelId).trim() ||
getString(providerSpecificData.modelId).trim();
const result = await runQoderCliCommand({
token: apiKey,
prompt: "Reply with OK only.",

View File

@@ -9,7 +9,9 @@ export const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
const refreshPromiseCache = new Map();
function getRefreshCacheKey(provider, refreshToken) {
const tokenHash = createHash("sha256").update(refreshToken).digest("hex");
const tokenHash = createHash("sha256")
.update(refreshToken) /* lgtm[js/insufficient-password-hash] */
.digest("hex");
return `${provider}:${tokenHash}`;
}

View File

@@ -700,15 +700,15 @@ async function getAntigravityUsage(accessToken, providerSpecificData) {
"tab_flash_lite_preview",
"tab_jump_flash_lite_preview",
"gemini-2.5-flash-thinking",
"gemini-2.5-pro", // browser subagent model — not user-callable
"gemini-2.5-flash", // internal — quota always exhausted on free tier
"gemini-2.5-flash-lite", // internal — quota always exhausted on free tier
"gemini-2.5-pro", // browser subagent model — not user-callable
"gemini-2.5-flash", // internal — quota always exhausted on free tier
"gemini-2.5-flash-lite", // internal — quota always exhausted on free tier
"gemini-2.5-flash-preview-image-generation", // image-gen only, not usable for chat
"gemini-3.1-flash-image-preview", // image-gen preview, not usable for chat
"gemini-3-flash-agent", // internal agent model — not user-callable
"gemini-3.1-flash-lite", // not usable for chat
"gemini-3-pro-low", // not usable for chat
"gemini-3-pro-high", // not usable for chat
"gemini-3.1-flash-image-preview", // image-gen preview, not usable for chat
"gemini-3-flash-agent", // internal agent model — not user-callable
"gemini-3.1-flash-lite", // not usable for chat
"gemini-3-pro-low", // not usable for chat
"gemini-3-pro-high", // not usable for chat
]);
// Parse per-model quota info from fetchAvailableModels response.
@@ -717,7 +717,11 @@ async function getAntigravityUsage(accessToken, providerSpecificData) {
const quotaInfo = toRecord(info.quotaInfo);
// Skip internal, excluded, and models without quota info
if (info.isInternal === true || ANTIGRAVITY_EXCLUDED_MODELS.has(modelKey) || Object.keys(quotaInfo).length === 0) {
if (
info.isInternal === true ||
ANTIGRAVITY_EXCLUDED_MODELS.has(modelKey) ||
Object.keys(quotaInfo).length === 0
) {
continue;
}

View File

@@ -5,79 +5,293 @@
* Risk-based phase skipping: high=all phases, medium=skip planner, low=execute+test only.
*/
export type Phase = "classify"|"plan"|"plan_review"|"execute"|"code_review"|"quality_review"|"security"|"test"|"output_review"|"done"|"failed"|"paused";
export type RiskLevel = "low"|"medium"|"high";
export type Verdict = "approve"|"approve_with_notes"|"request_changes"|"reject"|"block";
export type Phase =
| "classify"
| "plan"
| "plan_review"
| "execute"
| "code_review"
| "quality_review"
| "security"
| "test"
| "output_review"
| "done"
| "failed"
| "paused";
export type RiskLevel = "low" | "medium" | "high";
export type Verdict = "approve" | "approve_with_notes" | "request_changes" | "reject" | "block";
export interface PhaseRecord {
phase: Phase; enteredAt: string; exitedAt: string|null;
verdict: Verdict|null; provider: string|null; model: string|null;
retryCount: number; notes: string|null;
phase: Phase;
enteredAt: string;
exitedAt: string | null;
verdict: Verdict | null;
provider: string | null;
model: string | null;
retryCount: number;
notes: string | null;
}
export interface WorkflowContext {
id: string; currentPhase: Phase; risk: RiskLevel;
lastVerdict: Verdict|null; retries: Record<string,number>;
maxRetries: number; testsPass: boolean;
history: PhaseRecord[]; createdAt: string;
metadata: Record<string,unknown>;
id: string;
currentPhase: Phase;
risk: RiskLevel;
lastVerdict: Verdict | null;
retries: Record<string, number>;
maxRetries: number;
testsPass: boolean;
history: PhaseRecord[];
createdAt: string;
metadata: Record<string, unknown>;
}
interface Transition { from: Phase; to: Phase; condition: (ctx: WorkflowContext) => boolean; description: string; }
interface Transition {
from: Phase;
to: Phase;
condition: (ctx: WorkflowContext) => boolean;
description: string;
}
const HIGH_KEYWORDS = ["schema","migration","deploy","delete","drop","env","database","refactor","security","auth","production","secrets","credentials","permission"];
const MED_KEYWORDS = ["endpoint","feature","service","model","api","integration","webhook","middleware","route"];
const HIGH_KEYWORDS = [
"schema",
"migration",
"deploy",
"delete",
"drop",
"env",
"database",
"refactor",
"security",
"auth",
"production",
"secrets",
"credentials",
"permission",
];
const MED_KEYWORDS = [
"endpoint",
"feature",
"service",
"model",
"api",
"integration",
"webhook",
"middleware",
"route",
];
export function classifyRisk(desc: string): RiskLevel {
const l = desc.toLowerCase();
if (HIGH_KEYWORDS.some(k => l.includes(k))) return "high";
if (MED_KEYWORDS.some(k => l.includes(k))) return "medium";
if (HIGH_KEYWORDS.some((k) => l.includes(k))) return "high";
if (MED_KEYWORDS.some((k) => l.includes(k))) return "medium";
return "low";
}
const PHASE_ORDER: Phase[] = ["classify","plan","plan_review","execute","code_review","quality_review","security","test","output_review"];
const T: Transition[] = [
{from:"classify",to:"plan",condition:c=>c.risk==="high",description:"High risk -> full planning"},
{from:"classify",to:"execute",condition:c=>c.risk==="medium",description:"Medium risk -> skip planner"},
{from:"classify",to:"execute",condition:c=>c.risk==="low",description:"Low risk -> direct execute"},
{from:"plan",to:"plan_review",condition:()=>true,description:"Plan -> review"},
{from:"plan_review",to:"execute",condition:c=>c.lastVerdict==="approve"||c.lastVerdict==="approve_with_notes",description:"Plan approved -> execute"},
{from:"plan_review",to:"plan",condition:c=>(c.lastVerdict==="reject"||c.lastVerdict==="request_changes")&&(c.retries["plan"]??0)<c.maxRetries,description:"Plan rejected -> retry"},
{from:"plan_review",to:"failed",condition:c=>(c.lastVerdict==="reject"||c.lastVerdict==="request_changes")&&(c.retries["plan"]??0)>=c.maxRetries,description:"Plan rejected max retries"},
{from:"execute",to:"code_review",condition:c=>c.risk!=="low",description:"Non-low -> code review"},
{from:"execute",to:"test",condition:c=>c.risk==="low",description:"Low -> skip reviews"},
{from:"code_review",to:"quality_review",condition:c=>c.lastVerdict==="approve"||c.lastVerdict==="approve_with_notes",description:"Code approved -> quality"},
{from:"code_review",to:"execute",condition:c=>(c.lastVerdict==="reject"||c.lastVerdict==="request_changes")&&(c.retries["execute"]??0)<c.maxRetries,description:"Code rejected -> re-execute"},
{from:"code_review",to:"failed",condition:c=>(c.lastVerdict==="reject"||c.lastVerdict==="request_changes")&&(c.retries["execute"]??0)>=c.maxRetries,description:"Code rejected max retries"},
{from:"quality_review",to:"security",condition:c=>c.risk==="high",description:"High -> security audit"},
{from:"quality_review",to:"test",condition:c=>c.risk!=="high",description:"Non-high -> skip security"},
{from:"security",to:"failed",condition:c=>c.lastVerdict==="block",description:"Security BLOCK -> failed"},
{from:"security",to:"test",condition:c=>c.lastVerdict!=="block",description:"Security passed -> test"},
{from:"test",to:"output_review",condition:c=>c.testsPass,description:"Tests pass -> output review"},
{from:"test",to:"execute",condition:c=>!c.testsPass&&(c.retries["execute"]??0)<c.maxRetries,description:"Tests fail -> re-execute"},
{from:"test",to:"failed",condition:c=>!c.testsPass&&(c.retries["execute"]??0)>=c.maxRetries,description:"Tests fail max retries"},
{from:"output_review",to:"done",condition:()=>true,description:"Output reviewed -> done"},
const PHASE_ORDER: Phase[] = [
"classify",
"plan",
"plan_review",
"execute",
"code_review",
"quality_review",
"security",
"test",
"output_review",
];
export function createWorkflow(id: string, description: string, opts?: {maxRetries?: number; metadata?: Record<string,unknown>}): WorkflowContext {
const T: Transition[] = [
{
from: "classify",
to: "plan",
condition: (c) => c.risk === "high",
description: "High risk -> full planning",
},
{
from: "classify",
to: "execute",
condition: (c) => c.risk === "medium",
description: "Medium risk -> skip planner",
},
{
from: "classify",
to: "execute",
condition: (c) => c.risk === "low",
description: "Low risk -> direct execute",
},
{ from: "plan", to: "plan_review", condition: () => true, description: "Plan -> review" },
{
from: "plan_review",
to: "execute",
condition: (c) => c.lastVerdict === "approve" || c.lastVerdict === "approve_with_notes",
description: "Plan approved -> execute",
},
{
from: "plan_review",
to: "plan",
condition: (c) =>
(c.lastVerdict === "reject" || c.lastVerdict === "request_changes") &&
(c.retries["plan"] ?? 0) < c.maxRetries,
description: "Plan rejected -> retry",
},
{
from: "plan_review",
to: "failed",
condition: (c) =>
(c.lastVerdict === "reject" || c.lastVerdict === "request_changes") &&
(c.retries["plan"] ?? 0) >= c.maxRetries,
description: "Plan rejected max retries",
},
{
from: "execute",
to: "code_review",
condition: (c) => c.risk !== "low",
description: "Non-low -> code review",
},
{
from: "execute",
to: "test",
condition: (c) => c.risk === "low",
description: "Low -> skip reviews",
},
{
from: "code_review",
to: "quality_review",
condition: (c) => c.lastVerdict === "approve" || c.lastVerdict === "approve_with_notes",
description: "Code approved -> quality",
},
{
from: "code_review",
to: "execute",
condition: (c) =>
(c.lastVerdict === "reject" || c.lastVerdict === "request_changes") &&
(c.retries["execute"] ?? 0) < c.maxRetries,
description: "Code rejected -> re-execute",
},
{
from: "code_review",
to: "failed",
condition: (c) =>
(c.lastVerdict === "reject" || c.lastVerdict === "request_changes") &&
(c.retries["execute"] ?? 0) >= c.maxRetries,
description: "Code rejected max retries",
},
{
from: "quality_review",
to: "security",
condition: (c) => c.risk === "high",
description: "High -> security audit",
},
{
from: "quality_review",
to: "test",
condition: (c) => c.risk !== "high",
description: "Non-high -> skip security",
},
{
from: "security",
to: "failed",
condition: (c) => c.lastVerdict === "block",
description: "Security BLOCK -> failed",
},
{
from: "security",
to: "test",
condition: (c) => c.lastVerdict !== "block",
description: "Security passed -> test",
},
{
from: "test",
to: "output_review",
condition: (c) => c.testsPass,
description: "Tests pass -> output review",
},
{
from: "test",
to: "execute",
condition: (c) => !c.testsPass && (c.retries["execute"] ?? 0) < c.maxRetries,
description: "Tests fail -> re-execute",
},
{
from: "test",
to: "failed",
condition: (c) => !c.testsPass && (c.retries["execute"] ?? 0) >= c.maxRetries,
description: "Tests fail max retries",
},
{
from: "output_review",
to: "done",
condition: () => true,
description: "Output reviewed -> done",
},
];
export function createWorkflow(
id: string,
description: string,
opts?: { maxRetries?: number; metadata?: Record<string, unknown> }
): WorkflowContext {
const risk = classifyRisk(description);
return {id, currentPhase:"classify", risk, lastVerdict:null, retries:{}, maxRetries:opts?.maxRetries??3, testsPass:false,
history:[{phase:"classify",enteredAt:new Date().toISOString(),exitedAt:null,verdict:null,provider:null,model:null,retryCount:0,notes:`Risk: ${risk}`}],
createdAt:new Date().toISOString(), metadata:opts?.metadata??{}};
return {
id,
currentPhase: "classify",
risk,
lastVerdict: null,
retries: {},
maxRetries: opts?.maxRetries ?? 3,
testsPass: false,
history: [
{
phase: "classify",
enteredAt: new Date().toISOString(),
exitedAt: null,
verdict: null,
provider: null,
model: null,
retryCount: 0,
notes: `Risk: ${risk}`,
},
],
createdAt: new Date().toISOString(),
metadata: opts?.metadata ?? {},
};
}
export function advance(ctx: WorkflowContext, result?: {verdict?:Verdict;testsPass?:boolean;provider?:string;model?:string;notes?:string}): Phase|null {
if(result?.verdict!=null) ctx.lastVerdict=result.verdict;
if(result?.testsPass!=null) ctx.testsPass=result.testsPass;
const cur=ctx.history[ctx.history.length-1];
if(cur){cur.exitedAt=new Date().toISOString();cur.verdict=result?.verdict??null;cur.provider=result?.provider??null;cur.model=result?.model??null;cur.notes=result?.notes??null;}
for(const t of T){
if(t.from===ctx.currentPhase&&t.condition(ctx)){
const fi=PHASE_ORDER.indexOf(t.from),ti=PHASE_ORDER.indexOf(t.to);
if(ti>=0&&fi>=0&&ti<=fi) ctx.retries[t.to]=(ctx.retries[t.to]??0)+1;
ctx.currentPhase=t.to;
ctx.history.push({phase:t.to,enteredAt:new Date().toISOString(),exitedAt:null,verdict:null,provider:null,model:null,retryCount:ctx.retries[t.to]??0,notes:t.description});
export function advance(
ctx: WorkflowContext,
result?: {
verdict?: Verdict;
testsPass?: boolean;
provider?: string;
model?: string;
notes?: string;
}
): Phase | null {
if (result?.verdict != null) ctx.lastVerdict = result.verdict;
if (result?.testsPass != null) ctx.testsPass = result.testsPass;
const cur = ctx.history[ctx.history.length - 1];
if (cur) {
cur.exitedAt = new Date().toISOString();
cur.verdict = result?.verdict ?? null;
cur.provider = result?.provider ?? null;
cur.model = result?.model ?? null;
cur.notes = result?.notes ?? null;
}
for (const t of T) {
if (t.from === ctx.currentPhase && t.condition(ctx)) {
const fi = PHASE_ORDER.indexOf(t.from),
ti = PHASE_ORDER.indexOf(t.to);
if (ti >= 0 && fi >= 0 && ti <= fi) ctx.retries[t.to] = (ctx.retries[t.to] ?? 0) + 1;
ctx.currentPhase = t.to;
ctx.history.push({
phase: t.to,
enteredAt: new Date().toISOString(),
exitedAt: null,
verdict: null,
provider: null,
model: null,
retryCount: ctx.retries[t.to] ?? 0,
notes: t.description,
});
return t.to;
}
}
@@ -85,19 +299,42 @@ export function advance(ctx: WorkflowContext, result?: {verdict?:Verdict;testsPa
}
export function pause(ctx: WorkflowContext, reason: string): void {
ctx.currentPhase="paused";
ctx.history.push({phase:"paused",enteredAt:new Date().toISOString(),exitedAt:null,verdict:null,provider:null,model:null,retryCount:0,notes:reason});
ctx.currentPhase = "paused";
ctx.history.push({
phase: "paused",
enteredAt: new Date().toISOString(),
exitedAt: null,
verdict: null,
provider: null,
model: null,
retryCount: 0,
notes: reason,
});
}
export function resume(ctx: WorkflowContext, phase: Phase): void {
const p=ctx.history[ctx.history.length-1];if(p)p.exitedAt=new Date().toISOString();
ctx.currentPhase=phase;
ctx.history.push({phase,enteredAt:new Date().toISOString(),exitedAt:null,verdict:null,provider:null,model:null,retryCount:ctx.retries[phase]??0,notes:"Resumed"});
const p = ctx.history[ctx.history.length - 1];
if (p) p.exitedAt = new Date().toISOString();
ctx.currentPhase = phase;
ctx.history.push({
phase,
enteredAt: new Date().toISOString(),
exitedAt: null,
verdict: null,
provider: null,
model: null,
retryCount: ctx.retries[phase] ?? 0,
notes: "Resumed",
});
}
export function isTerminated(ctx: WorkflowContext): boolean { return ctx.currentPhase==="done"||ctx.currentPhase==="failed"; }
export function getPhaseSequence(ctx: WorkflowContext): Phase[] { return ctx.history.map(r=>r.phase); }
export function isTerminated(ctx: WorkflowContext): boolean {
return ctx.currentPhase === "done" || ctx.currentPhase === "failed";
}
export function getPhaseSequence(ctx: WorkflowContext): Phase[] {
return ctx.history.map((r) => r.phase);
}
export function getLLMCallCount(ctx: WorkflowContext): number {
const sys: Phase[]=["classify","paused","done","failed"];
return ctx.history.filter(r=>!sys.includes(r.phase)&&r.exitedAt!=null).length;
const sys: Phase[] = ["classify", "paused", "done", "failed"];
return ctx.history.filter((r) => !sys.includes(r.phase) && r.exitedAt != null).length;
}

View File

@@ -84,7 +84,11 @@ export function translateRequest(
credentials = null,
provider = null,
reqLogger = null,
options?: { normalizeToolCallId?: boolean; preserveDeveloperRole?: boolean; preserveCacheControl?: boolean }
options?: {
normalizeToolCallId?: boolean;
preserveDeveloperRole?: boolean;
preserveCacheControl?: boolean;
}
) {
let result = body;
const use9CharId = options?.normalizeToolCallId === true;

View File

@@ -227,7 +227,11 @@ export function geminiToOpenAIResponse(chunk, state) {
}
// Content blocked by Gemini safety filters — pass through as "content_filter"
// so downstream clients can distinguish from normal completion.
if (finishReason === "safety" || finishReason === "recitation" || finishReason === "blocklist") {
if (
finishReason === "safety" ||
finishReason === "recitation" ||
finishReason === "blocklist"
) {
finishReason = "content_filter";
}

View File

@@ -21,7 +21,10 @@ const DEFAULT_INTERVAL_MS = 2000;
* @param {AbortSignal} [options.signal] - Abort signal for cancellation
* @returns {TransformStream}
*/
export function createProgressTransform({ intervalMs = DEFAULT_INTERVAL_MS, signal }: { intervalMs?: number; signal?: AbortSignal } = {}) {
export function createProgressTransform({
intervalMs = DEFAULT_INTERVAL_MS,
signal,
}: { intervalMs?: number; signal?: AbortSignal } = {}) {
let tokenCount = 0;
let startTime = Date.now();
let intervalId;

View File

@@ -10,7 +10,13 @@ import {
filterUsageForFormat,
COLORS,
} from "./usageTracking.ts";
import { parseSSELine, hasValuableContent, fixInvalidId, formatSSE, unwrapGeminiChunk } from "./streamHelpers.ts";
import {
parseSSELine,
hasValuableContent,
fixInvalidId,
formatSSE,
unwrapGeminiChunk,
} from "./streamHelpers.ts";
import {
createStructuredSSECollector,
buildStreamSummaryFromEvents,

View File

@@ -158,7 +158,11 @@ function buildOpenAISummary(events: StructuredSSEEvent[], fallbackModel?: string
reasoningParts.push(delta.reasoning_content);
}
// Normalize `reasoning` alias (NVIDIA kimi-k2.5 etc.)
if (typeof delta.reasoning === "string" && delta.reasoning.length > 0 && !delta.reasoning_content) {
if (
typeof delta.reasoning === "string" &&
delta.reasoning.length > 0 &&
!delta.reasoning_content
) {
reasoningParts.push(delta.reasoning);
}

6
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "omniroute",
"version": "3.4.7",
"version": "3.4.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "omniroute",
"version": "3.4.7",
"version": "3.4.8",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [
@@ -21047,7 +21047,7 @@
},
"open-sse": {
"name": "@omniroute/open-sse",
"version": "3.4.7"
"version": "3.4.8"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "omniroute",
"version": "3.4.7",
"version": "3.4.8",
"description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
"type": "module",
"bin": {

View File

@@ -51,7 +51,9 @@ for (const fullPath of routeFiles) {
}
if (missingValidation.length > 0) {
console.error("[t06:route-validation] FAIL - routes with request.json() without validateBody() or .safeParse():");
console.error(
"[t06:route-validation] FAIL - routes with request.json() without validateBody() or .safeParse():"
);
for (const file of missingValidation) {
console.error(` - ${file}`);
}

View File

@@ -170,9 +170,15 @@ function setByPath(target, pathStr, value) {
const tokens = pathStr.split(".");
let current = target;
for (let i = 0; i < tokens.length - 1; i += 1) {
if (tokens[i] === "__proto__" || tokens[i] === "constructor" || tokens[i] === "prototype")
continue;
if (typeof current[tokens[i]] !== "object") current[tokens[i]] = {};
current = current[tokens[i]];
}
current[tokens[tokens.length - 1]] = value;
const last = tokens[tokens.length - 1];
if (last !== "__proto__" && last !== "constructor" && last !== "prototype") {
current[last] = value;
}
}
async function applyMessageOverrides() {

View File

@@ -240,7 +240,7 @@ if (existsSync(mitmSrc)) {
writeFileSync(tmpTsconfigPath, JSON.stringify(mitmTsconfig, null, 2));
try {
execSync("npx tsc -p " + JSON.stringify(tmpTsconfigPath), { cwd: ROOT, stdio: "inherit" });
execSync("npx tsc -p tsconfig.mitm.tmp.json", { cwd: ROOT, stdio: "inherit" });
console.log(" ✅ MITM utilities compiled to app/src/mitm/");
} catch (err) {
console.warn(" ⚠️ MITM compile warning (non-fatal):", err.message);
@@ -264,7 +264,7 @@ if (existsSync(mcpSrcFile)) {
mkdirSync(mcpDestDir, { recursive: true });
try {
execSync(
`npx esbuild ${JSON.stringify(mcpSrcFile)} --bundle --platform=node --packages=external --format=esm --outfile=${JSON.stringify(mcpDestFile)}`,
`npx esbuild open-sse/mcp-server/server.ts --bundle --platform=node --packages=external --format=esm --outfile=app/open-sse/mcp-server/server.js`,
{ cwd: ROOT, stdio: "inherit" }
);
console.log(" ✅ MCP Server bundled to app/open-sse/mcp-server/server.js");

View File

@@ -45,7 +45,13 @@ async function main() {
const vitestProcess = spawn(
process.execPath,
["./node_modules/vitest/vitest.mjs", "run", "tests/e2e/protocol-clients.test.ts", "--dir", "tests"],
[
"./node_modules/vitest/vitest.mjs",
"run",
"tests/e2e/protocol-clients.test.ts",
"--dir",
"tests",
],
{
stdio: "inherit",
env: testEnv,

View File

@@ -54,7 +54,7 @@ export default function DiversityScoreCard() {
Provider Diversity Score
</h3>
<span
className={`text-xs px-2 py-0.5 rounded-md border ${gaugeColor.replace("bg-", "border-").replace("500", "500/20")} ${gaugeColor.replace("bg-", "bg-").replace("500", "500/10")} ${riskColor}`}
className={`text-xs px-2 py-0.5 rounded-md border ${gaugeColor.replace("bg-", "border-").replace("500", "500/20")} ${gaugeColor.replace("500", "500/10")} ${riskColor}`}
>
Shannon Entropy
</span>

View File

@@ -144,7 +144,6 @@ export default function AutoComboDashboard() {
tierPriority: "🏷️ Tier",
};
const MODE_PACKS = [
{ id: "ship-fast", label: "🚀 Ship Fast" },
{ id: "cost-saver", label: "💰 Cost Saver" },

View File

@@ -753,7 +753,9 @@ export default function MediaPageClient() {
{/* Transcription: file upload */}
{activeTab === "transcription" ? (
<div>
<label className="block text-sm font-medium text-text-main mb-2">Audio / Video File</label>
<label className="block text-sm font-medium text-text-main mb-2">
Audio / Video File
</label>
<input
type="file"
accept="audio/*,video/*"
@@ -761,7 +763,9 @@ export default function MediaPageClient() {
const file = e.target.files?.[0] ?? null;
setFileSizeError(null);
if (file && file.size > MAX_TRANSCRIPTION_FILE_SIZE) {
setFileSizeError(`File too large (${formatFileSize(file.size)}). Maximum allowed: 4 GB.`);
setFileSizeError(
`File too large (${formatFileSize(file.size)}). Maximum allowed: 4 GB.`
);
setAudioFile(null);
e.target.value = "";
return;
@@ -781,7 +785,9 @@ export default function MediaPageClient() {
{audioFile.name} ({formatFileSize(audioFile.size)})
</p>
)}
<p className="text-[10px] text-text-muted/60 mt-1">Supports audio and video files up to 4 GB</p>
<p className="text-[10px] text-text-muted/60 mt-1">
Supports audio and video files up to 4 GB
</p>
</div>
) : (
/* Prompt / Text */

View File

@@ -884,9 +884,7 @@ export default function ProviderDetailPage() {
const isOAuth = providerSupportsOAuth && !providerSupportsPat;
const registryModels = getModelsByProviderId(providerId);
// For Gemini: always use synced API models (empty if no keys added yet)
const models = providerId === "gemini"
? syncedAvailableModels
: registryModels;
const models = providerId === "gemini" ? syncedAvailableModels : registryModels;
const providerAlias = getProviderAlias(providerId);
const isManagedAvailableModelsProvider = isCompatible || providerId === "openrouter";
const isSearchProvider = providerId.endsWith("-search");
@@ -2050,23 +2048,24 @@ export default function ProviderDetailPage() {
);
}
const importButton = providerId === "gemini" ? null : (
<div className="flex items-center gap-2 mb-4">
<Button
size="sm"
variant="secondary"
icon="download"
onClick={handleImportModels}
disabled={!canImportModels || importingModels}
>
{importingModels ? t("importingModels") : t("importFromModels")}
</Button>
{autoSyncToggle}
{!canImportModels && (
<span className="text-xs text-text-muted">{t("addConnectionToImport")}</span>
)}
</div>
);
const importButton =
providerId === "gemini" ? null : (
<div className="flex items-center gap-2 mb-4">
<Button
size="sm"
variant="secondary"
icon="download"
onClick={handleImportModels}
disabled={!canImportModels || importingModels}
>
{importingModels ? t("importingModels") : t("importFromModels")}
</Button>
{autoSyncToggle}
{!canImportModels && (
<span className="text-xs text-text-muted">{t("addConnectionToImport")}</span>
)}
</div>
);
if (models.length === 0) {
return (
@@ -2401,7 +2400,9 @@ export default function ProviderDetailPage() {
setShowEditModal(true);
}}
onDelete={() => handleDelete(conn.id)}
onReauth={conn.authType === "oauth" ? () => setShowOAuthModal(true) : undefined}
onReauth={
conn.authType === "oauth" ? () => setShowOAuthModal(true) : undefined
}
onRefreshToken={
conn.authType === "oauth" ? () => handleRefreshToken(conn.id) : undefined
}
@@ -4270,8 +4271,9 @@ function ConnectionRow({
<span
className={`text-xs truncate max-w-[300px] ${statusPresentation.errorTextClass}`}
title={connection.lastError.replace(/<[^>]*>?/gm, "")}
dangerouslySetInnerHTML={{ __html: connection.lastError }}
/>
>
{connection.lastError.replace(/<[^>]*>?/gm, "")}
</span>
)}
<span className="text-xs text-text-muted">#{connection.priority}</span>
{connection.globalPriority && (

View File

@@ -58,7 +58,9 @@ export default function SearchHistory({ onReplay }: SearchHistoryProps) {
<div className="text-xs text-text-main truncate">{entry.query}</div>
<div className="flex justify-between mt-0.5">
<span className="text-[10px] text-text-muted">{entry.provider}</span>
<span className="text-[10px] text-text-muted">{timeAgo(entry.timestamp, locale)}</span>
<span className="text-[10px] text-text-muted">
{timeAgo(entry.timestamp, locale)}
</span>
</div>
</button>
))}

View File

@@ -2,9 +2,7 @@ import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { jwtVerify } from "jose";
const SECRET = process.env.JWT_SECRET
? new TextEncoder().encode(process.env.JWT_SECRET)
: null;
const SECRET = process.env.JWT_SECRET ? new TextEncoder().encode(process.env.JWT_SECRET) : null;
export async function GET() {
try {

View File

@@ -15,14 +15,14 @@ async function guardEnabled(): Promise<NextResponse | null> {
if (!settings.mcpEnabled) {
return NextResponse.json(
{ error: "MCP server is disabled. Enable it from the Endpoints page." },
{ status: 503 },
{ status: 503 }
);
}
const transport = (settings.mcpTransport as string) || "stdio";
if (transport !== "sse") {
return NextResponse.json(
{ error: `MCP transport is set to "${transport}", not "sse". Change it from Settings.` },
{ status: 400 },
{ status: 400 }
);
}
return null;

View File

@@ -26,8 +26,7 @@ export async function GET() {
// stdio uses heartbeat file; HTTP transports use in-process state
const stdioOnline = isMcpHeartbeatOnline(heartbeat, { requireLivePid: true });
const online =
mcpTransport === "stdio" ? stdioOnline : httpStatus.online;
const online = mcpTransport === "stdio" ? stdioOnline : httpStatus.online;
const lastCall = lastCallPage.entries[0] || null;
const now = Date.now();

View File

@@ -16,14 +16,16 @@ async function guardEnabled(): Promise<NextResponse | null> {
if (!settings.mcpEnabled) {
return NextResponse.json(
{ error: "MCP server is disabled. Enable it from the Endpoints page." },
{ status: 503 },
{ status: 503 }
);
}
const transport = (settings.mcpTransport as string) || "stdio";
if (transport !== "streamable-http") {
return NextResponse.json(
{ error: `MCP transport is set to "${transport}", not "streamable-http". Change it from Settings.` },
{ status: 400 },
{
error: `MCP transport is set to "${transport}", not "streamable-http". Change it from Settings.`,
},
{ status: 400 }
);
}
return null;

View File

@@ -20,7 +20,10 @@ export async function GET() {
let specPath = "";
for (const p of candidates) {
if (fs.existsSync(p)) { specPath = p; break; }
if (fs.existsSync(p)) {
specPath = p;
break;
}
}
if (!specPath) {

View File

@@ -8,7 +8,10 @@ import { NextRequest, NextResponse } from "next/server";
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
const tryRequestSchema = z.object({
method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).optional().default("GET"),
method: z
.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"])
.optional()
.default("GET"),
path: z.string().min(1, "Path is required").startsWith("/", "Path must start with /"),
headers: z.record(z.string(), z.string()).optional().default({}),
body: z.any().optional(),

View File

@@ -169,9 +169,7 @@ const PROVIDER_MODELS_CONFIG: Record<string, ProviderModelsConfigEntry> = {
id: ((m.name as string) || (m.id as string) || "").replace(/^models\//, ""),
name: (m.displayName as string) || ((m.name as string) || "").replace(/^models\//, ""),
supportedEndpoints: endpoints,
...(typeof m.inputTokenLimit === "number"
? { inputTokenLimit: m.inputTokenLimit }
: {}),
...(typeof m.inputTokenLimit === "number" ? { inputTokenLimit: m.inputTokenLimit } : {}),
...(typeof m.outputTokenLimit === "number"
? { outputTokenLimit: m.outputTokenLimit }
: {}),
@@ -747,7 +745,9 @@ export async function GET(
}
if (pageCount > 1) {
console.log(`[models] ${provider}: fetched ${allModels.length} models across ${pageCount} pages`);
console.log(
`[models] ${provider}: fetched ${allModels.length} models across ${pageCount} pages`
);
}
return buildResponse({

View File

@@ -1,6 +1,10 @@
import { NextResponse } from "next/server";
import { getProviderConnectionById } from "@/models";
import { getCustomModels, replaceCustomModels, replaceSyncedAvailableModelsForConnection } from "@/lib/db/models";
import {
getCustomModels,
replaceCustomModels,
replaceSyncedAvailableModelsForConnection,
} from "@/lib/db/models";
import {
syncManagedAvailableModelAliases,
usesManagedAvailableModels,
@@ -144,7 +148,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
// Fetch models from the existing /api/providers/[id]/models endpoint
const origin = new URL(request.url).origin;
const modelsUrl = `${origin}/api/providers/${id}/models`;
const modelsUrl = `${origin}/api/providers/${encodeURIComponent(id)}/models`;
const modelsRes = await fetch(modelsUrl, {
method: "GET",
headers: {
@@ -210,7 +214,9 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
source: "api-sync" as const,
...(m.supportedEndpoints ? { supportedEndpoints: m.supportedEndpoints } : {}),
...(typeof m.inputTokenLimit === "number" ? { inputTokenLimit: m.inputTokenLimit } : {}),
...(typeof m.outputTokenLimit === "number" ? { outputTokenLimit: m.outputTokenLimit } : {}),
...(typeof m.outputTokenLimit === "number"
? { outputTokenLimit: m.outputTokenLimit }
: {}),
...(typeof m.description === "string" ? { description: m.description } : {}),
...(m.supportsThinking === true ? { supportsThinking: true } : {}),
}));

View File

@@ -25,7 +25,12 @@ export async function PUT(request: Request) {
rawBody = await request.json();
} catch {
return NextResponse.json(
{ error: { message: "Invalid request", details: [{ field: "body", message: "Invalid JSON body" }] } },
{
error: {
message: "Invalid request",
details: [{ field: "body", message: "Invalid JSON body" }],
},
},
{ status: 400 }
);
}

View File

@@ -18,7 +18,10 @@ export async function PUT(request: Request, props: { params: Promise<{ id: strin
}
const db = getDbInstance();
db.prepare("UPDATE skills SET enabled = ? WHERE id = ?").run(validation.data.enabled ? 1 : 0, id);
db.prepare("UPDATE skills SET enabled = ? WHERE id = ?").run(
validation.data.enabled ? 1 : 0,
id
);
await skillRegistry.loadFromDatabase();

View File

@@ -39,7 +39,15 @@ export async function POST(request: Request) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
const { title, provider, accountId, requestId, errorCode, details, labels = [] } = validation.data;
const {
title,
provider,
accountId,
requestId,
errorCode,
details,
labels = [],
} = validation.data;
const repo = process.env.GITHUB_ISSUES_REPO;
const token = process.env.GITHUB_ISSUES_TOKEN;

View File

@@ -40,7 +40,10 @@ export async function GET() {
// Gemini: always replace hardcoded entries with synced models (no fallback)
// Always remove hardcoded gemini entries — even if sync returns empty
for (let i = models.length - 1; i >= 0; i--) {
if (typeof (models[i] as any).name === "string" && (models[i] as any).name.startsWith("models/gemini/")) {
if (
typeof (models[i] as any).name === "string" &&
(models[i] as any).name.startsWith("models/gemini/")
) {
models.splice(i, 1);
}
}
@@ -69,7 +72,8 @@ export async function GET() {
// Skip Gemini — handled by syncedAvailableModels above
if (providerId === "gemini") continue;
for (const model of rawModels) {
if (!model || typeof model !== "object" || typeof (model as any).id !== "string") continue;
if (!model || typeof model !== "object" || typeof (model as any).id !== "string")
continue;
const m = model as Record<string, unknown>;
if (m.isHidden === true) continue;
models.push({
@@ -77,10 +81,8 @@ export async function GET() {
displayName: m.name || m.id,
...(typeof m.description === "string" ? { description: m.description } : {}),
supportedGenerationMethods: ["generateContent"],
inputTokenLimit:
typeof m.inputTokenLimit === "number" ? m.inputTokenLimit : 128000,
outputTokenLimit:
typeof m.outputTokenLimit === "number" ? m.outputTokenLimit : 8192,
inputTokenLimit: typeof m.inputTokenLimit === "number" ? m.inputTokenLimit : 128000,
outputTokenLimit: typeof m.outputTokenLimit === "number" ? m.outputTokenLimit : 8192,
...(m.supportsThinking === true ? { thinking: true } : {}),
});
}

View File

@@ -10,13 +10,15 @@ import { NextResponse } from "next/server";
import { getWebhook, updateWebhookRecord, deleteWebhook } from "@/lib/localDb";
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
const updateWebhookSchema = z.object({
url: z.string().url("Invalid URL format").max(2000).optional(),
events: z.array(z.string()).optional(),
secret: z.string().max(500).optional(),
description: z.string().max(1000).optional(),
enabled: z.boolean().optional(),
}).passthrough();
const updateWebhookSchema = z
.object({
url: z.string().url("Invalid URL format").max(2000).optional(),
events: z.array(z.string()).optional(),
secret: z.string().max(500).optional(),
description: z.string().max(1000).optional(),
enabled: z.boolean().optional(),
})
.passthrough();
export async function GET(_: Request, { params }: { params: Promise<{ id: string }> }) {
try {

View File

@@ -10,7 +10,6 @@
* @module domain/fallbackPolicy
*/
import {
saveFallbackChain,
loadFallbackChain,

View File

@@ -9,7 +9,6 @@
* @module domain/modelAvailability
*/
/**
* @typedef {Object} UnavailableEntry
* @property {string} provider

View File

@@ -138,9 +138,7 @@ export function getAllExpirations(): ProviderExpiration[] {
* Get connections that are expired or expiring soon.
*/
export function getExpiringSoon(): ProviderExpiration[] {
return getAllExpirations().filter(
(e) => e.status === "expired" || e.status === "expiring_soon"
);
return getAllExpirations().filter((e) => e.status === "expired" || e.status === "expiring_soon");
}
/**
@@ -229,9 +227,7 @@ export function detectExpirationFromResponse(
// Rate limit headers may indicate reset times
const resetHeader =
headers["x-ratelimit-reset"] ||
headers["x-ratelimit-reset-tokens"] ||
headers["retry-after"];
headers["x-ratelimit-reset"] || headers["x-ratelimit-reset-tokens"] || headers["retry-after"];
if (resetHeader && status === 429) {
const resetTime = parseInt(resetHeader, 10);

View File

@@ -420,23 +420,23 @@ export async function replaceCustomModels(
...(m.inputTokenLimit != null
? { inputTokenLimit: m.inputTokenLimit }
: (prev as any)?.inputTokenLimit != null
? { inputTokenLimit: (prev as any).inputTokenLimit }
: {}),
? { inputTokenLimit: (prev as any).inputTokenLimit }
: {}),
...(m.outputTokenLimit != null
? { outputTokenLimit: m.outputTokenLimit }
: (prev as any)?.outputTokenLimit != null
? { outputTokenLimit: (prev as any).outputTokenLimit }
: {}),
? { outputTokenLimit: (prev as any).outputTokenLimit }
: {}),
...(m.description != null
? { description: m.description }
: (prev as any)?.description != null
? { description: (prev as any).description }
: {}),
? { description: (prev as any).description }
: {}),
...(m.supportsThinking != null
? { supportsThinking: m.supportsThinking }
: (prev as any)?.supportsThinking != null
? { supportsThinking: (prev as any).supportsThinking }
: {}),
? { supportsThinking: (prev as any).supportsThinking }
: {}),
// Preserve existing compat flags
...(prev && (prev as any).normalizeToolCallId !== undefined
? { normalizeToolCallId: (prev as any).normalizeToolCallId }
@@ -525,10 +525,14 @@ export interface SyncedAvailableModel {
/**
* Get all synced available models for a provider, unioned across all connections.
*/
export async function getSyncedAvailableModels(providerId: string): Promise<SyncedAvailableModel[]> {
export async function getSyncedAvailableModels(
providerId: string
): Promise<SyncedAvailableModel[]> {
const db = getDbInstance();
const rows = db
.prepare("SELECT key, value FROM key_value WHERE namespace = 'syncedAvailableModels' AND key LIKE ?")
.prepare(
"SELECT key, value FROM key_value WHERE namespace = 'syncedAvailableModels' AND key LIKE ?"
)
.all(`${providerId}:%`);
const map = new Map<string, SyncedAvailableModel>();
for (const row of rows) {
@@ -545,7 +549,9 @@ export async function getSyncedAvailableModels(providerId: string): Promise<Sync
/**
* Get all synced available models across all providers.
*/
export async function getAllSyncedAvailableModels(): Promise<Record<string, SyncedAvailableModel[]>> {
export async function getAllSyncedAvailableModels(): Promise<
Record<string, SyncedAvailableModel[]>
> {
const db = getDbInstance();
const rows = db
.prepare("SELECT key, value FROM key_value WHERE namespace = 'syncedAvailableModels'")
@@ -582,7 +588,9 @@ export async function replaceSyncedAvailableModelsForConnection(
const db = getDbInstance();
const key = `${providerId}:${connectionId}`;
if (models.length === 0) {
db.prepare("DELETE FROM key_value WHERE namespace = 'syncedAvailableModels' AND key = ?").run(key);
db.prepare("DELETE FROM key_value WHERE namespace = 'syncedAvailableModels' AND key = ?").run(
key
);
} else {
db.prepare(
"INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('syncedAvailableModels', ?, ?)"
@@ -603,7 +611,9 @@ export async function deleteSyncedAvailableModelsForConnection(
): Promise<SyncedAvailableModel[]> {
const db = getDbInstance();
const key = `${providerId}:${connectionId}`;
db.prepare("DELETE FROM key_value WHERE namespace = 'syncedAvailableModels' AND key = ?").run(key);
db.prepare("DELETE FROM key_value WHERE namespace = 'syncedAvailableModels' AND key = ?").run(
key
);
backupDbFile("pre-write");
return getSyncedAvailableModels(providerId);
}

View File

@@ -8,7 +8,6 @@
* @module lib/evals/evalRunner
*/
/**
* @typedef {Object} EvalCase
* @property {string} id - Unique case ID

View File

@@ -46,9 +46,7 @@ let _outputProvider: ((suiteId: string, caseId: string) => Promise<string>) | nu
*
* @param fn - Async function(suiteId, caseId) → actual output string
*/
export function setOutputProvider(
fn: (suiteId: string, caseId: string) => Promise<string>
): void {
export function setOutputProvider(fn: (suiteId: string, caseId: string) => Promise<string>): void {
_outputProvider = fn;
}
@@ -87,9 +85,7 @@ export function schedule(suiteId: string, intervalMs: number): ScheduledEval {
_timers.set(suiteId, timer);
console.log(
`[EvalScheduler] Scheduled "${suiteId}" every ${Math.round(safeInterval / 1000)}s`
);
console.log(`[EvalScheduler] Scheduled "${suiteId}" every ${Math.round(safeInterval / 1000)}s`);
return entry;
}

View File

@@ -165,7 +165,12 @@ export class AntigravityService {
/**
* Complete onboarding flow with retry
*/
async completeOnboarding(accessToken: string, projectId: string, tierId: string, maxRetries = 10) {
async completeOnboarding(
accessToken: string,
projectId: string,
tierId: string,
maxRetries = 10
) {
for (let i = 0; i < maxRetries; i++) {
const result = await this.onboardUser(accessToken, projectId, tierId);

View File

@@ -49,7 +49,12 @@ export class KiroService {
/**
* Start device authorization for AWS Builder ID or IDC
*/
async startDeviceAuthorization(clientId: string, clientSecret: string, startUrl: string, region: string = "us-east-1") {
async startDeviceAuthorization(
clientId: string,
clientSecret: string,
startUrl: string,
region: string = "us-east-1"
) {
const endpoint = `https://oidc.${region}.amazonaws.com/device_authorization`;
const response = await fetch(endpoint, {
@@ -83,7 +88,12 @@ export class KiroService {
/**
* Poll for token using device code (AWS Builder ID/IDC)
*/
async pollDeviceToken(clientId: string, clientSecret: string, deviceCode: string, region: string = "us-east-1") {
async pollDeviceToken(
clientId: string,
clientSecret: string,
deviceCode: string,
region: string = "us-east-1"
) {
const endpoint = `https://oidc.${region}.amazonaws.com/token`;
const response = await fetch(endpoint, {

View File

@@ -17,7 +17,12 @@ export class OAuthService {
/**
* Build authorization URL
*/
buildAuthUrl(redirectUri: string, state: string, codeChallenge: string, extraParams: Record<string, string> = {}) {
buildAuthUrl(
redirectUri: string,
state: string,
codeChallenge: string,
extraParams: Record<string, string> = {}
) {
const params = new URLSearchParams({
client_id: this.config.clientId,
response_type: "code",
@@ -129,7 +134,10 @@ export class OAuthService {
/**
* Complete OAuth flow
*/
async authenticate(providerName: string, buildAuthUrlFn: (redirectUri: string, state: string, codeChallenge: string) => string) {
async authenticate(
providerName: string,
buildAuthUrlFn: (redirectUri: string, state: string, codeChallenge: string) => string
) {
// Generate PKCE
const { codeVerifier, codeChallenge, state } = generatePKCE();

View File

@@ -7,7 +7,10 @@ import { URL } from "url";
* @param {number} fixedPort - Optional fixed port number (default: random)
* @returns {Promise<{server: http.Server, port: number, close: Function}>}
*/
export function startLocalServer(onCallback: (params: Record<string, string>) => void, fixedPort: number | null = null): Promise<{ server: any; port: number; close: () => void }> {
export function startLocalServer(
onCallback: (params: Record<string, string>) => void,
fixedPort: number | null = null
): Promise<{ server: any; port: number; close: () => void }> {
return new Promise((resolve, reject) => {
const server = http.createServer((req, res) => {
const url = new URL(req.url || "/", `http://localhost`);

View File

@@ -51,15 +51,9 @@ export interface Plugin {
/** Called before the chat handler */
onRequest?: (ctx: PluginContext) => Promise<PluginResult | void> | PluginResult | void;
/** Called after the chat handler */
onResponse?: (
ctx: PluginContext,
response: any
) => Promise<any | void> | any | void;
onResponse?: (ctx: PluginContext, response: any) => Promise<any | void> | any | void;
/** Called on handler error */
onError?: (
ctx: PluginContext,
error: Error
) => Promise<any | void> | any | void;
onError?: (ctx: PluginContext, error: Error) => Promise<any | void> | any | void;
}
// ── Registry ──

View File

@@ -72,7 +72,11 @@ function shortModelName(model: string) {
* @param {Object} connectionMap - Map of connectionId → account name
* @returns {Object} Analytics data
*/
export async function computeAnalytics(history: any[], range = "30d", connectionMap: Record<string, string> = {}) {
export async function computeAnalytics(
history: any[],
range = "30d",
connectionMap: Record<string, string> = {}
) {
const { start, end } = getDateRange(range);
// ---- Filtered entries ----

View File

@@ -69,7 +69,7 @@ export async function getMitmStatus() {
let dnsConfigured = false;
try {
const hostsContent = fs.readFileSync("/etc/hosts", "utf-8");
dnsConfigured = hostsContent.includes("daily-cloudcode-pa.googleapis.com");
dnsConfigured = /\bdaily-cloudcode-pa\.googleapis\.com\b/.test(hostsContent);
} catch {
// Ignore
}

View File

@@ -40,7 +40,6 @@ export default function Button({
className,
...props
}: ButtonProps) {
return (
<button
type="button"

View File

@@ -13,7 +13,6 @@ interface CardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title">
className?: string;
}
export default function Card({
children,
title,
@@ -25,7 +24,6 @@ export default function Card({
className,
...props
}: CardProps) {
const paddings = {
none: "",
xs: "p-3",

View File

@@ -39,8 +39,7 @@ export default function FilterBar({
setExpandedFilter(null);
}, [onSearchChange, filters, onFilterChange]);
const hasActiveFilters =
searchValue || Object.values(activeFilters).some((v) => v && v !== "");
const hasActiveFilters = searchValue || Object.values(activeFilters).some((v) => v && v !== "");
return (
<div
@@ -89,9 +88,7 @@ export default function FilterBar({
{filters.map((filter) => (
<div key={filter.key} style={{ position: "relative" }}>
<button
onClick={() =>
setExpandedFilter(expandedFilter === filter.key ? null : filter.key)
}
onClick={() => setExpandedFilter(expandedFilter === filter.key ? null : filter.key)}
style={{
padding: "6px 12px",
borderRadius: "6px",
@@ -99,9 +96,7 @@ export default function FilterBar({
background: activeFilters[filter.key]
? "rgba(99,102,241,0.15)"
: "rgba(255,255,255,0.05)",
color: activeFilters[filter.key]
? "#818cf8"
: "var(--text-secondary, #888)",
color: activeFilters[filter.key] ? "#818cf8" : "var(--text-secondary, #888)",
fontSize: "12px",
cursor: "pointer",
whiteSpace: "nowrap",
@@ -158,10 +153,7 @@ export default function FilterBar({
width: "100%",
padding: "6px 12px",
textAlign: "left",
background:
activeFilters[filter.key] === opt
? "rgba(99,102,241,0.2)"
: "none",
background: activeFilters[filter.key] === opt ? "rgba(99,102,241,0.2)" : "none",
border: "none",
color:
activeFilters[filter.key] === opt

View File

@@ -3,7 +3,7 @@
import { useId } from "react";
import { cn } from "@/shared/utils/cn";
interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> {
label?: React.ReactNode;
error?: React.ReactNode;
hint?: React.ReactNode;

View File

@@ -28,7 +28,6 @@ export default function Modal({
showCloseButton = true,
className,
}: ModalProps) {
const titleId = useId();
const dialogRef = useRef(null);

View File

@@ -68,9 +68,7 @@ function Toast({ notification, onDismiss }) {
boxShadow: "0 8px 32px rgba(0,0,0,0.2)",
minWidth: "320px",
maxWidth: "420px",
animation: isExiting
? "toastOut 0.2s ease-in forwards"
: "toastIn 0.3s ease-out forwards",
animation: isExiting ? "toastOut 0.2s ease-in forwards" : "toastIn 0.3s ease-out forwards",
transition: "all 0.2s ease",
}}
>

View File

@@ -8,7 +8,7 @@ interface SelectOption {
label: string;
}
interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'size'> {
interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "size"> {
label?: React.ReactNode;
options?: SelectOption[];
placeholder?: string;

View File

@@ -67,13 +67,13 @@ export default function Sidebar({
}
if ("instanceName" in detail) {
setCustomAppName(detail.instanceName as string || null);
setCustomAppName((detail.instanceName as string) || null);
}
if ("customLogoBase64" in detail) {
setCustomLogo(detail.customLogoBase64 as string || null);
setCustomLogo((detail.customLogoBase64 as string) || null);
} else if ("customLogoUrl" in detail) {
setCustomLogo(detail.customLogoUrl as string || null);
setCustomLogo((detail.customLogoUrl as string) || null);
}
};

View File

@@ -3,7 +3,13 @@
import { useTheme } from "@/shared/hooks/useTheme";
import { cn } from "@/shared/utils/cn";
export default function ThemeToggle({ className, variant = "default" }: { className?: any; variant?: string }) {
export default function ThemeToggle({
className,
variant = "default",
}: {
className?: any;
variant?: string;
}) {
const { theme, toggleTheme, isDark } = useTheme();
const variants = {

View File

@@ -27,4 +27,3 @@ const LEGACY_PROVIDER_IDS = [
export const CLI_COMPAT_PROVIDER_IDS = Array.from(
new Set([...DERIVED_PROVIDER_IDS, ...LEGACY_PROVIDER_IDS])
);

View File

@@ -26,42 +26,137 @@ interface ErrorDetails {
export const ERROR_CODES: Record<string, ErrorCodeDef> = {
// ── Auth ──
AUTH_001: { code: "AUTH_001", message: "Authentication required", httpStatus: 401, category: "AUTH" },
AUTH_001: {
code: "AUTH_001",
message: "Authentication required",
httpStatus: 401,
category: "AUTH",
},
AUTH_002: { code: "AUTH_002", message: "Invalid API key", httpStatus: 401, category: "AUTH" },
AUTH_003: { code: "AUTH_003", message: "API key expired", httpStatus: 401, category: "AUTH" },
AUTH_004: { code: "AUTH_004", message: "Insufficient permissions", httpStatus: 403, category: "AUTH" },
AUTH_004: {
code: "AUTH_004",
message: "Insufficient permissions",
httpStatus: 403,
category: "AUTH",
},
AUTH_005: { code: "AUTH_005", message: "Account locked", httpStatus: 423, category: "AUTH" },
AUTH_006: { code: "AUTH_006", message: "No credentials for provider", httpStatus: 400, category: "AUTH" },
AUTH_006: {
code: "AUTH_006",
message: "No credentials for provider",
httpStatus: 400,
category: "AUTH",
},
// ── Proxy ──
PROXY_001: { code: "PROXY_001", message: "Proxy connection failed", httpStatus: 502, category: "PROXY" },
PROXY_001: {
code: "PROXY_001",
message: "Proxy connection failed",
httpStatus: 502,
category: "PROXY",
},
PROXY_002: { code: "PROXY_002", message: "Proxy timeout", httpStatus: 504, category: "PROXY" },
PROXY_003: { code: "PROXY_003", message: "All proxies exhausted", httpStatus: 503, category: "PROXY" },
PROXY_003: {
code: "PROXY_003",
message: "All proxies exhausted",
httpStatus: 503,
category: "PROXY",
},
// ── Rate Limiting ──
RATE_001: { code: "RATE_001", message: "Rate limit exceeded", httpStatus: 429, category: "RATE_LIMIT" },
RATE_002: { code: "RATE_002", message: "Daily budget exceeded", httpStatus: 429, category: "RATE_LIMIT" },
RATE_003: { code: "RATE_003", message: "All accounts rate-limited", httpStatus: 503, category: "RATE_LIMIT" },
RATE_001: {
code: "RATE_001",
message: "Rate limit exceeded",
httpStatus: 429,
category: "RATE_LIMIT",
},
RATE_002: {
code: "RATE_002",
message: "Daily budget exceeded",
httpStatus: 429,
category: "RATE_LIMIT",
},
RATE_003: {
code: "RATE_003",
message: "All accounts rate-limited",
httpStatus: 503,
category: "RATE_LIMIT",
},
// ── Model / Routing ──
MODEL_001: { code: "MODEL_001", message: "Model not found", httpStatus: 404, category: "MODEL" },
MODEL_002: { code: "MODEL_002", message: "Ambiguous model identifier", httpStatus: 400, category: "MODEL" },
MODEL_003: { code: "MODEL_003", message: "Model temporarily unavailable", httpStatus: 503, category: "MODEL" },
MODEL_002: {
code: "MODEL_002",
message: "Ambiguous model identifier",
httpStatus: 400,
category: "MODEL",
},
MODEL_003: {
code: "MODEL_003",
message: "Model temporarily unavailable",
httpStatus: 503,
category: "MODEL",
},
// ── Provider ──
PROVIDER_001: { code: "PROVIDER_001", message: "Provider error", httpStatus: 502, category: "PROVIDER" },
PROVIDER_002: { code: "PROVIDER_002", message: "Provider timeout", httpStatus: 504, category: "PROVIDER" },
PROVIDER_003: { code: "PROVIDER_003", message: "Provider not configured", httpStatus: 400, category: "PROVIDER" },
PROVIDER_001: {
code: "PROVIDER_001",
message: "Provider error",
httpStatus: 502,
category: "PROVIDER",
},
PROVIDER_002: {
code: "PROVIDER_002",
message: "Provider timeout",
httpStatus: 504,
category: "PROVIDER",
},
PROVIDER_003: {
code: "PROVIDER_003",
message: "Provider not configured",
httpStatus: 400,
category: "PROVIDER",
},
// ── Validation ──
VALID_001: { code: "VALID_001", message: "Invalid request body", httpStatus: 400, category: "VALIDATION" },
VALID_002: { code: "VALID_002", message: "Missing required field", httpStatus: 400, category: "VALIDATION" },
VALID_003: { code: "VALID_003", message: "Input sanitization blocked", httpStatus: 400, category: "VALIDATION" },
VALID_001: {
code: "VALID_001",
message: "Invalid request body",
httpStatus: 400,
category: "VALIDATION",
},
VALID_002: {
code: "VALID_002",
message: "Missing required field",
httpStatus: 400,
category: "VALIDATION",
},
VALID_003: {
code: "VALID_003",
message: "Input sanitization blocked",
httpStatus: 400,
category: "VALIDATION",
},
// ── Internal ──
INTERNAL_001: { code: "INTERNAL_001", message: "Internal server error", httpStatus: 500, category: "INTERNAL" },
INTERNAL_002: { code: "INTERNAL_002", message: "Database error", httpStatus: 500, category: "INTERNAL" },
INTERNAL_003: { code: "INTERNAL_003", message: "Circuit breaker open", httpStatus: 503, category: "INTERNAL" },
INTERNAL_001: {
code: "INTERNAL_001",
message: "Internal server error",
httpStatus: 500,
category: "INTERNAL",
},
INTERNAL_002: {
code: "INTERNAL_002",
message: "Database error",
httpStatus: 500,
category: "INTERNAL",
},
INTERNAL_003: {
code: "INTERNAL_003",
message: "Circuit breaker open",
httpStatus: 503,
category: "INTERNAL",
},
};
export function createErrorResponse(code: string, details: ErrorDetails = {}) {

View File

@@ -19,9 +19,7 @@ function getApiKeySecret(): string {
function generateKeyId(): string {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
for (let i = 0; i < 6; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
result = crypto.randomBytes(3).toString("hex");
return result;
}
@@ -31,7 +29,7 @@ function generateKeyId(): string {
function generateCrc(machineId: string, keyId: string): string {
const secret = getApiKeySecret();
return crypto
.createHmac("sha256", secret)
.createHmac("sha256", secret) /* lgtm [js/insufficient-password-hash] */
.update(machineId + keyId)
.digest("hex")
.slice(0, 8);

View File

@@ -7,7 +7,6 @@
* @module shared/utils/fetchTimeout
*/
const DEFAULT_TIMEOUT_MS = 120000; // 2 minutes
const FETCH_TIMEOUT_MS = parseInt(process.env.FETCH_TIMEOUT_MS || "", 10) || DEFAULT_TIMEOUT_MS;

View File

@@ -13,17 +13,20 @@
const INJECTION_PATTERNS = [
{
name: "system_override",
pattern: /\b(ignore|disregard|forget)\s+(all\s+)?(previous|prior|above|earlier)\s+(instructions?|prompts?|rules?|context)/i,
pattern:
/\b(ignore|disregard|forget)\s+(all\s+)?(previous|prior|above|earlier)\s+(instructions?|prompts?|rules?|context)/i,
severity: "high",
},
{
name: "role_hijack",
pattern: /\b(you\s+are\s+now|act\s+as\s+if|pretend\s+(to\s+be|you\s+are)|from\s+now\s+on\s+you\s+are)\b/i,
pattern:
/\b(you\s+are\s+now|act\s+as\s+if|pretend\s+(to\s+be|you\s+are)|from\s+now\s+on\s+you\s+are)\b/i,
severity: "medium",
},
{
name: "system_prompt_leak",
pattern: /\b(reveal|show|display|print|output|repeat)\s+(your\s+)?(system\s+prompt|instructions?|initial\s+prompt|hidden\s+prompt)/i,
pattern:
/\b(reveal|show|display|print|output|repeat)\s+(your\s+)?(system\s+prompt|instructions?|initial\s+prompt|hidden\s+prompt)/i,
severity: "high",
},
{
@@ -38,7 +41,8 @@ const INJECTION_PATTERNS = [
},
{
name: "encoding_evasion",
pattern: /\b(base64\s+decode|rot13|hex\s+decode|unicode\s+escape)\b.*\b(instruction|prompt|command)\b/i,
pattern:
/\b(base64\s+decode|rot13|hex\s+decode|unicode\s+escape)\b.*\b(instruction|prompt|command)\b/i,
severity: "medium",
},
];

View File

@@ -57,7 +57,11 @@ export async function fetchWithTimeout(url: string, options: RequestInit & Timeo
* @returns {Promise<T>}
* @throws {Error} With name 'TimeoutError' if operation times out
*/
export async function withTimeout<T>(fn: () => Promise<T>, timeoutMs: number, label = "Operation"): Promise<T> {
export async function withTimeout<T>(
fn: () => Promise<T>,
timeoutMs: number,
label = "Operation"
): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timeoutId = setTimeout(() => {
const error: any = new Error(`${label} timed out after ${timeoutMs}ms`);

View File

@@ -1,3 +1,4 @@
import { randomUUID } from "crypto";
import {
getProviderCredentials,
markAccountUnavailable,

View File

@@ -1,3 +1,4 @@
import { randomUUID } from "crypto";
import {
getProviderConnections,
validateApiKey,
@@ -659,8 +660,11 @@ export async function getProviderCredentials(
if (orderedConnections.length <= 2) {
connection = orderedConnections[0];
} else {
const i = Math.floor(Math.random() * orderedConnections.length);
let j = Math.floor(Math.random() * (orderedConnections.length - 1));
const i =
parseInt(randomUUID().replace(/-/g, "").substring(0, 8), 16) % orderedConnections.length;
let j =
parseInt(randomUUID().replace(/-/g, "").substring(0, 8), 16) %
(orderedConnections.length - 1);
if (j >= i) j++;
const a = orderedConnections[i];
const b = orderedConnections[j];
@@ -671,7 +675,8 @@ export async function getProviderCredentials(
}
} else if (strategy === "random") {
// Random: Fisher-Yates-inspired random pick
const idx = Math.floor(Math.random() * orderedConnections.length);
const idx =
parseInt(randomUUID().replace(/-/g, "").substring(0, 8), 16) % orderedConnections.length;
connection = orderedConnections[idx];
} else if (strategy === "least-used") {
// Least Used: pick the one with oldest lastUsedAt

View File

@@ -165,7 +165,11 @@ export class StreamTracker {
* @returns {boolean}
*/
isTerminal(): boolean {
const terminalStates: string[] = [STREAM_STATES.COMPLETED, STREAM_STATES.FAILED, STREAM_STATES.CANCELLED];
const terminalStates: string[] = [
STREAM_STATES.COMPLETED,
STREAM_STATES.FAILED,
STREAM_STATES.CANCELLED,
];
return terminalStates.includes(this.state);
}
}

View File

@@ -81,16 +81,12 @@ export const useNotificationStore = create<NotificationStore>((set, get) => ({
// ─── Convenience Methods ─────────────────
success: (message, title) =>
get().addNotification({ type: "success", message, title }),
success: (message, title) => get().addNotification({ type: "success", message, title }),
error: (message, title) =>
get().addNotification({ type: "error", message, title, duration: 8000 }),
warning: (message, title) =>
get().addNotification({ type: "warning", message, title }),
warning: (message, title) => get().addNotification({ type: "warning", message, title }),
info: (message, title) =>
get().addNotification({ type: "info", message, title }),
info: (message, title) => get().addNotification({ type: "info", message, title }),
}));

View File

@@ -7,7 +7,9 @@ const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts");
test("T20: antigravity config has updated User-Agent and sandbox fallback URL", () => {
const antigravity = REGISTRY.antigravity;
assert.ok(Array.isArray(antigravity.baseUrls));
assert.ok(antigravity.baseUrls.includes("https://daily-cloudcode-pa.sandbox.googleapis.com"));
assert.ok(
antigravity.baseUrls.some((u) => u === "https://daily-cloudcode-pa.sandbox.googleapis.com")
);
assert.match(
antigravity.headers["User-Agent"],
new RegExp(`^antigravity/1\\.107\\.0\\s+${platform()}\\/${arch()}$`)