Skip to content

feat(chaos): add Chaos Mode — multi-model parallel/collaborative execution#6679

Open
Moseyuh333 wants to merge 11 commits into
diegosouzapw:release/v3.8.47from
Moseyuh333:feat/chaos-mode
Open

feat(chaos): add Chaos Mode — multi-model parallel/collaborative execution#6679
Moseyuh333 wants to merge 11 commits into
diegosouzapw:release/v3.8.47from
Moseyuh333:feat/chaos-mode

Conversation

@Moseyuh333

Copy link
Copy Markdown
Contributor

Chaos Mode

Allow IDE or any OmniRoute API client to initiate multiple models simultaneously.
Each provider contributes one model — models run in parallel or collaborative mode
to complete a single task faster and more effectively.

Changes

  • DB: New column chaos_mode_enabled on api_keys table
  • API Keys: Create/PATCH routes support chaosModeEnabled toggle
  • Core library: src/lib/chaos/chaosConfig.ts — get/set/reset chaos config (persisted in settings table)
  • Config API: GET/PUT/DELETE /api/chaos/config — global chaos mode settings
  • Execution API: POST /api/skills/collect/chaos — requires Bearer token with chaosModeEnabled=true
  • Dashboard: /dashboard/chaos — full config UI (enable/disable, mode switch, timeout, provider overrides, test button)
  • Sidebar: Entry in Agentic Features section (blender icon)
  • API Key Editor: Chaos Mode toggle in permissions panel
  • i18n: Chaos config section in en.json

How to use

  1. Go to Dashboard » Agentic Features » Chaos Mode → enable + save
  2. Go to API Keys → enable Chaos Mode Access on your key
  3. Call the chaos endpoint with your key to run multiple models in parallel

Moseyuh333 and others added 6 commits July 4, 2026 18:54
Integrates Skill Collector functionality into OmniRoute:

Core Library (src/lib/skills/githubCollector.ts):
- GitHub API search with 20+ query strategies
- Heuristic scoring (0-1): stars, skill-file signals, keywords, topics
- Security scanning for blocked patterns
- Category inference & install path resolution

MCP Tools (open-sse/mcp-server/tools/githubSkillTools.ts):
- omniroute_github_skills_search
- omniroute_github_skills_scan
- omniroute_github_skills_install

REST API (src/app/api/github-skills/):
- GET /api/github-skills - search with filters
- POST /api/github-skills - install planning

Agent Skills Catalog:
- Added omni-github-skills entry (API, area: github-skills)
- Updated SkillCoverage: API 22→23
…, fix settings JSON parse

- Add buildErrorBody import to github-skills API route (was undefined at runtime)
- Add 34 unit tests for githubCollector: scoreRepo, scanText, inferCategory,
  resolveInstallPath, QUERY_STRATEGIES, INSTALL_TARGETS — all passing
- Fix settings.ts JSON.parse to handle non-JSON string values gracefully
…se guard + subsystem wiring

- MCP registration passes toolDef.scopes so the 3 github_skills tools gate on
  read:skills / write:skills under scope enforcement (was denied as
  tool_definition_missing).
- API GET error path routes through sanitizeErrorMessage + returns 500 (Hard Rule diegosouzapw#12).
- Realign agent-skills catalog/routes/generator/mcp count guards for the new
  omni-github-skills catalog entry (43→44 total, 22→23 api).

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
…detection

- Add GET /api/skills/collect/detect — detect installed coding CLIs
  using OmniRoute's CLI_TOOL_IDS + getCliRuntimeStatus, search GitHub
  for matching agent skills, auto-match to installed tools.
- Add POST /api/skills/collect/install — plan skill installation into
  detected coding tools (Codex, Claude, Copilot, OpenCode, Hermes...)
- Add skills/cli-skill-collector/SKILL.md — 4-step agent workflow
  (detect → review → install → verify)
- Add docker-compose.drive-d.yml — isolated Docker compose for D: drive
  with port 30129 and data under ./data
- Add start.sh — convenience launcher (PORT=30129, C: drive port 30128 untouched)
- Add Codex installer support to existing installer system
- Update skills/README.md with cli-skill-collector entry (CLI Skills: 21)
- detect: optional chain for nullable repo fields
- detect: distribute top skills evenly across unmatched tools
- install: remove unused imports (resolveInstallPath, INSTALL_TARGETS)
- install: add 9 missing tool paths (cursor, copilot, cline...)
- install: fix ~ expansion on Windows (USERPROFILE priority)
- install: validate non-empty strings in targets array
- SKILL.md: port 30128 → 30129 everywhere
- start.sh: non-blocking C drive check with timeout
…ution

- New DB column chaos_mode_enabled on api_keys table
- API key create/PATCH routes support chaosModeEnabled toggle
- Core library src/lib/chaos/chaosConfig.ts for persistent config
- API routes: GET/PUT/DELETE /api/chaos/config
- Chaos execution POST /api/skills/collect/chaos with key auth
- Dashboard page at /dashboard/chaos with full config UI
- Sidebar entry in Agentic Features section
- Chaos mode toggle in API Key editor permissions panel
- i18n keys for chaos config (en.json)
@Moseyuh333 Moseyuh333 requested a review from diegosouzapw as a code owner July 8, 2026 18:29
Copilot AI review requested due to automatic review settings July 8, 2026 18:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces 'Chaos Mode' for parallel or collaborative multi-model execution, integrates a built-in GitHub Skill Discovery tool, adds the Tencent Yuanbao and Requesty providers, updates Doubao Web to Dola Global, and implements a session-stickiness opt-out toggle. It also resolves several bugs, including Next.js standalone WebSocket daemon startup, pre-spawn port probes, and macOS certificate fingerprint checks. The reviewer identified several critical issues: an authorization bypass vulnerability in classify.ts that exposes /api/v1/ routes publicly; a missing Authorization header in the internal fetch for Chaos Mode; a discontinuous stars bonus calculation; missing array validation for targets in the GitHub skills API; a potential null pointer crash in ComboDefaultsTab.tsx; and an unprocessed trailing stream buffer in yuanbao-web.ts. Additionally, style guide violations were noted regarding the incorrect placement of sync-next-cycle.mjs and start.sh.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines 133 to 135
function isClassifiedAsPublic(path: string, method: string): boolean {
const isV1ApiPrefix = (p: string) =>
p === "/api/v1" || p === "/api/v1/" || p.startsWith("/api/v1/");
const filtered = PUBLIC_API_ROUTE_PREFIXES.filter((p) => p !== "/api/v1/");
if (filtered.some((prefix) => path.startsWith(prefix)) && !isV1ApiPrefix(path)) {
return true;
}
return matchesReadonlyPublic(path, method);
return isPublicApiRoute(path, method);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

Security Issue: Authorization Bypass for /api/v1/ Routes

By replacing the original isClassifiedAsPublic logic with a direct call to isPublicApiRoute(path, method), /api/v1/ routes (such as /api/v1/chat/completions) are now classified as PUBLIC for authorization purposes.

While /api/v1/ is bypassed from the global requireLogin middleware (so API clients can connect without a browser session), it must not be classified as PUBLIC in the authorization classifier, as it still requires API key validation. Classifying it as PUBLIC here bypasses crucial security and role-based access checks.

Please restore the explicit exclusion of /api/v1/ routes from being classified as public.

Suggested change
function isClassifiedAsPublic(path: string, method: string): boolean {
const isV1ApiPrefix = (p: string) =>
p === "/api/v1" || p === "/api/v1/" || p.startsWith("/api/v1/");
const filtered = PUBLIC_API_ROUTE_PREFIXES.filter((p) => p !== "/api/v1/");
if (filtered.some((prefix) => path.startsWith(prefix)) && !isV1ApiPrefix(path)) {
return true;
}
return matchesReadonlyPublic(path, method);
return isPublicApiRoute(path, method);
}
function isClassifiedAsPublic(path: string, method: string): boolean {
const isV1ApiPrefix = (p: string) =>
p === "/api/v1" || p === "/api/v1/" || p.startsWith("/api/v1/");
if (isV1ApiPrefix(path)) {
return false;
}
return isPublicApiRoute(path, method);
}

Comment on lines +95 to +120
async function dispatchToModel(
providerId: string,
providerName: string,
modelId: string,
messages: { role: string; content: string }[],
signal?: AbortSignal
): Promise<ModelResult> {
const start = performance.now();
try {
// Use provider:model format — OmniRoute's routing resolves this to
// the provider connection's actual model endpoint.
const model = modelId || providerId;

const body = {
model,
messages,
stream: false,
max_tokens: 4096,
};

const res = await fetch(`${OMNIROUTE_BASE}/v1/chat/completions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
signal: signal ?? AbortSignal.timeout(120_000),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Bug: Missing Authorization Header in Internal Fetch

The internal fetch request to ${OMNIROUTE_BASE}/v1/chat/completions does not pass any Authorization header.

On any secured OmniRoute instance (which is standard in production or when REQUIRE_API_KEY is enabled), this request will fail with 401 Unauthorized, completely breaking Chaos Mode.

Please update dispatchToModel to accept the client's bearerToken and forward it in the request headers.

async function dispatchToModel( 
  providerId: string,
  providerName: string,
  modelId: string,
  messages: { role: string; content: string }[],
  bearerToken: string,
  signal?: AbortSignal
): Promise<ModelResult> {
  const start = performance.now();
  try {
    const model = modelId || providerId;

    const body = { 
      model,
      messages,
      stream: false,
      max_tokens: 4096,
    };

    const res = await fetch(`${OMNIROUTE_BASE}/v1/chat/completions`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${bearerToken}`
      },
      body: JSON.stringify(body),
      signal: signal ?? AbortSignal.timeout(120_000),
    });

Comment on lines +201 to +208
// Stars bonus (logarithmic, capped for awesome lists)
if (isAwesome) stars = Math.min(stars, 1000);
if (stars > 20000) bonus = Math.min(0.7, stars / 30000);
else if (stars > 5000) bonus = stars / 15000;
else if (stars > 1000) bonus = stars / 10000;
else if (stars > 300) bonus = stars / 8000;
else if (stars > 100) bonus = stars / 12000;
else if (stars > 50) bonus = stars / 15000;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Bug: Discontinuous and Non-Monotonic Stars Bonus Calculation

The piecewise stars bonus calculation has severe discontinuities at the boundaries:

  • A repository with 20,000 stars gets a bonus of 20000 / 15000 = 1.33.
  • A repository with 20,001 stars gets a bonus of Math.min(0.7, 20001 / 30000) = 0.66.
  • A repository with 5,000 stars gets a bonus of 5000 / 10000 = 0.5.
  • A repository with 5,001 stars gets a bonus of 5001 / 15000 = 0.33.

This means repositories with slightly fewer stars can receive a significantly higher score.

Please replace this piecewise logic with a continuous, monotonic logarithmic formula.

Suggested change
// Stars bonus (logarithmic, capped for awesome lists)
if (isAwesome) stars = Math.min(stars, 1000);
if (stars > 20000) bonus = Math.min(0.7, stars / 30000);
else if (stars > 5000) bonus = stars / 15000;
else if (stars > 1000) bonus = stars / 10000;
else if (stars > 300) bonus = stars / 8000;
else if (stars > 100) bonus = stars / 12000;
else if (stars > 50) bonus = stars / 15000;
if (stars > 50) {
bonus = Math.min(0.7, Math.log10(stars / 50) / 4);
}

Comment on lines +58 to +74
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const {
repoName,
targets = ["hermes"],
description = "",
} = body as {
repoName?: string;
targets?: string[];
description?: string;
};

if (!repoName || typeof repoName !== "string") {
return NextResponse.json(buildErrorBody(400, "repoName is required"), { status: 400 });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Bug: Missing Validation for targets Array

The POST handler destructures targets from the request body and directly calls targets.map(...) without validating that targets is actually an array.

If a client sends a non-array value (e.g., a string or number), the server will crash with a TypeError (resulting in a 500 Internal Server Error) instead of returning a clean 400 Bad Request.

Please add a validation check to ensure targets is an array.

export async function POST(request: NextRequest) { 
  try {
    const body = await request.json();
    const {
      repoName,
      targets = ["hermes"],
      description = "",
    } = body as {
      repoName?: string;
      targets?: string[];
      description?: string;
    };

    if (!repoName || typeof repoName !== "string") {
      return NextResponse.json(buildErrorBody(400, "repoName is required"), { status: 400 });
    }

    if (!Array.isArray(targets)) {
      return NextResponse.json(buildErrorBody(400, "targets must be an array"), { status: 400 });
    }

    const { resolveInstallPath, INSTALL_TARGETS } = await import("@/lib/skills/githubCollector");

Comment on lines +173 to +176
disableSessionStickiness:
settingsData.disableSessionStickiness ??
comboData.comboDefaults?.disableSessionStickiness ??
prev.disableSessionStickiness,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Bug: Potential TypeError on Null settingsData

If settingsRes.ok is false, settingsData is set to null.

However, settingsData.disableSessionStickiness is accessed directly without optional chaining, which will throw a TypeError: Cannot read properties of null (reading 'disableSessionStickiness') and crash the component.

Please use optional chaining to safely access disableSessionStickiness.

Suggested change
disableSessionStickiness:
settingsData.disableSessionStickiness ??
comboData.comboDefaults?.disableSessionStickiness ??
prev.disableSessionStickiness,
disableSessionStickiness:
settingsData?.disableSessionStickiness ??
comboData.comboDefaults?.disableSessionStickiness ??
prev.disableSessionStickiness,

@@ -0,0 +1,195 @@
#!/usr/bin/env node

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Style Guide Violation: Incorrect Script Placement

According to the Repository Style Guide (Section 1: File Placement & Organization), all maintenance, debugging, generation, or experimental scripts must be placed strictly inside one of the following scripts/ subfolders:

  • build/
  • dev/
  • check/
  • docs/
  • i18n/
  • ad-hoc/

Creating a new scripts/release/ subfolder and placing sync-next-cycle.mjs inside it violates this rule. Please move this script to scripts/ad-hoc/ or another allowed subfolder.

References
  1. All scripts must be placed strictly inside allowed scripts/ subfolders (build/, dev/, check/, docs/, i18n/, ad-hoc/). (link)

Comment thread start.sh
@@ -0,0 +1,42 @@
#!/usr/bin/env bash

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Style Guide Violation: Loose Script in Project Root

According to the Repository Style Guide (Section 1: File Placement & Organization), loose scripts must never be dumped in the project root (/). The project root must only contain specific allowed configuration, dependency, documentation, and CI/CD files.

Please move start.sh to an allowed location, such as scripts/dev/ or scripts/ad-hoc/.

References
  1. Loose scripts must never be dumped in the project root. (link)

Comment on lines +474 to +504
async function collectYuanbaoResponse(
upstream: ReadableStream<Uint8Array>,
signal: AbortSignal | null | undefined
): Promise<{ content: string; reasoning: string }> {
const decoder = new TextDecoder();
const reader = upstream.getReader();
let buffer = "";
let content = "";
let reasoning = "";

try {
while (true) {
if (signal?.aborted) break;
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
const event = parseYuanbaoDataLine(line);
if (!event) continue;
if (event.type === "think" && event.content) reasoning += event.content;
else if (event.type === "text" && typeof event.msg === "string") content += event.msg;
}
}
} finally {
reader.releaseLock();
}

return { content, reasoning };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Bug: Unprocessed Trailing Stream Buffer

In collectYuanbaoResponse, the stream reader splits chunks by \n and pops the last line into buffer to handle partial lines.

However, when the stream ends (done is true), the loop breaks immediately, and any remaining text left in buffer (which represents the final line of the stream if it didn't end with a trailing newline) is completely ignored and never processed.

Please process the remaining buffer after the read loop completes.

async function collectYuanbaoResponse(
  upstream: ReadableStream<Uint8Array>,
  signal: AbortSignal | null | undefined
): Promise<{ content: string; reasoning: string }> {
  const decoder = new TextDecoder();
  const reader = upstream.getReader();
  let buffer = "";
  let content = "";
  let reasoning = "";

  try {
    while (true) {
      if (signal?.aborted) break;
      const { done, value } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split("\n");
      buffer = lines.pop() ?? "";
      for (const line of lines) {
        const event = parseYuanbaoDataLine(line);
        if (!event) continue;
        if (event.type === "think" && event.content) reasoning += event.content;
        else if (event.type === "text" && typeof event.msg === "string") content += event.msg;
      }
    }
    if (buffer) {
      const event = parseYuanbaoDataLine(buffer);
      if (event) {
        if (event.type === "think" && event.content) reasoning += event.content;
        else if (event.type === "text" && typeof event.msg === "string") content += event.msg;
      }
    }
  } finally {
    reader.releaseLock();
  }

=== Changes ===

1. NEW: src/lib/chaos/chaosExecutor.ts — shared execution engine
   - Removed ~150 lines of duplicate dispatch logic between two API routes
   - Single executeChaosRun() function used by both endpoints
   - Added concurrency limit (max 10 parallel requests)
   - Added proper TypeScript interfaces (ChaosRunInput, ChaosRunResult)
   - Added error logging throughout

2. FIX: src/app/api/skills/collect/chaos/route.ts
   - Was MISSING logger import (log.error was undefined at runtime)
   - Reduced from 388 lines → 142 lines by delegating to shared executor
   - Added maxTokens support in schema validation

3. REFACTOR: src/app/api/chaos/run/route.ts
   - Simplified to thin wrapper: auth + validate + delegate to executor
   - Added maxTokens support

4. ENHANCE: src/lib/chaos/chaosConfig.ts
   - Added maxTokens config field (256-128k, default 4096)
   - Persisted per-instance via settings table

5. ENHANCE: UI — ChaosConfigPageClient.tsx
   - Loads available providers from /api/models for dropdown autocomplete
   - Added datalist-based provider selector in overrides section
   - Added Max Tokens configuration input
   - Added expandable provider list showing all detected providers
   - Fixed duplicate override detection
@diegosouzapw diegosouzapw changed the base branch from main to release/v3.8.47 July 9, 2026 07:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants