Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

- **analytics (subscription cost):** flat-rate providers now show **$0** in cost analytics instead of an inflated per-token estimate. Subscription / coding-plan providers (every cookie-web provider — ChatGPT Web, grok-web, … — plus the dedicated **Minimax Coding**, **Kimi Coding**, **GLM Coding**, **Alibaba Coding Plan**, and **Xiaomi MiMo** plans) bill a flat fee, not per token, yet still carry per-token pricing rows used for estimates — so the analytics dashboard over-reported their cost. A new flat-rate classifier (`src/lib/usage/flatRateProviders.ts`) is consulted by the analytics surfaces (analytics route, usage stats, usage analytics) via an opt-in `flatRateAsZero` cost option, so those providers read $0 while **budget / quota / routing keep estimating unchanged**. Deliberately NOT zeroed: `codex`/`cx` (OmniRoute actively tracks Codex token cost — Fast-tier multipliers, GPT-5.x pricing — and Codex can be a metered account), `byteplus` (metered ModelArk), `minimax-cn` (metered China API). Regression guard: `tests/unit/flat-rate-cost-5552.test.ts`. ([#5552](https://github.com/diegosouzapw/OmniRoute/issues/5552))

- **mcp (RTK):** expose the RTK tool-output **learn/discover** workflow as two new MCP tools so an agent can grow the RTK filter catalog without leaving the protocol. `omniroute_rtk_discover` analyzes recently captured raw tool output (`discoverRepeatedNoise` / `suggestFilter`) and returns candidate noise patterns plus a suggested filter; `omniroute_rtk_learn` lists the captured command samples (`listRtkCommandSamples`) and resolves a command to its RTK filter id (`commandToId`). Both are read-only (scope `read:compression`), wrap the existing RTK discovery primitives (no new logic in the engine), and log to the MCP audit trail. Regression guard: `tests/unit/compression/rtk-mcp-tools.test.ts` (4). gaps v3.8.42 — T07.

### 🔧 Bug Fixes

- **providers (OpenAI-compatible):** Codex MCP / `tool_search` deferred discovery (and `apply_patch`) now works through a **Custom OpenAI-compatible provider**. When such a provider received a Responses-API-shaped request that carried MCP / `tool_search` tools, OmniRoute downgraded it to `/chat/completions`, which drops the deferred tool-discovery mechanism — so the MCP namespaces never surfaced to the model and `apply_patch` was mis-handled as a JSON tool. The executor now detects a Responses-shaped request (`input` / `previous_response_id` / `max_output_tokens` / `reasoning`) that carries `namespace` / `tool_search*` tools and routes it to the upstream `/responses` endpoint natively instead of downgrading (it can also be forced via `providerSpecificData._omnirouteForceResponsesUpstream`). This is a distinct code path from the official Codex OAuth backend (#3033 / #4539, which the earlier fix never touched). Regression guard: `tests/unit/executor-default-base.test.ts`. Thanks to @KooshaPari for the fix. ([#5483](https://github.com/diegosouzapw/OmniRoute/issues/5483))
Expand Down
68 changes: 68 additions & 0 deletions open-sse/mcp-server/tools/compressionTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,12 @@ import {
compressionComboStatsInput,
} from "../schemas/tools.ts";
import { handleCcrRetrieve } from "../../services/compression/engines/ccr/index.ts";
import {
listRtkCommandSamples,
discoverRepeatedNoise,
suggestFilter,
commandToId,
} from "../../services/compression/engines/rtk/index.ts";
import { resolveCallerScopeContext } from "../scopeEnforcement.ts";

const ccrRetrieveInput = z.object({
Expand Down Expand Up @@ -313,6 +319,48 @@ export async function handleCompressionComboStats(
};
}

// T07 — RTK learn/discover exposed via MCP (read-only; suggestions only). Mines the opt-in
// raw-output sample store, exactly like the /api/context/rtk/{discover,learn} routes.
const rtkDiscoverInput = z.object({
limit: z.number().int().positive().max(2000).optional().describe("Max samples to scan (default 500)"),
});

const rtkLearnInput = z.object({
command: z.string().min(1).max(500).describe("The command to learn an RTK filter draft for"),
limit: z.number().int().positive().max(2000).optional().describe("Max samples to scan (default 500)"),
});

function resolveSampleLimit(limit?: number): number {
if (!Number.isFinite(limit) || !limit || limit <= 0) return 500;
return Math.min(2000, Math.floor(limit));
}

export async function handleRtkDiscover(
args: z.infer<typeof rtkDiscoverInput>
): Promise<{ sampleCount: number; candidates: ReturnType<typeof discoverRepeatedNoise> }> {
const start = Date.now();
const samples = listRtkCommandSamples({ limit: resolveSampleLimit(args.limit) });
const candidates = discoverRepeatedNoise(samples);
const result = { sampleCount: samples.length, candidates };
await logToolCall("omniroute_rtk_discover", args, result, Date.now() - start, true);
return result;
}

export async function handleRtkLearn(
args: z.infer<typeof rtkLearnInput>
): Promise<{ command: string; sampleCount: number; filter: ReturnType<typeof suggestFilter> }> {
const start = Date.now();
const command = args.command.trim();
const targetId = commandToId(command);
const matching = listRtkCommandSamples({ limit: resolveSampleLimit(args.limit) }).filter(
(sample) => commandToId(sample.command) === targetId
);
const filter = suggestFilter(command, matching);
const result = { command, sampleCount: matching.length, filter };
await logToolCall("omniroute_rtk_learn", args, result, Date.now() - start, true);
return result;
}

export const compressionTools = {
omniroute_compression_status: {
name: "omniroute_compression_status",
Expand Down Expand Up @@ -369,4 +417,24 @@ export const compressionTools = {
return handleCcrRetrieve(args, callerId === "anonymous" ? undefined : callerId);
},
},
omniroute_rtk_discover: {
name: "omniroute_rtk_discover",
description:
"Mine the opt-in RTK raw-output sample store for recurring noise lines and return them " +
"as ranked candidates the operator can turn into strip/collapse filters. Read-only; " +
"suggestions only. Scope: read:compression.",
scopes: ["read:compression"],
inputSchema: rtkDiscoverInput,
handler: (args: z.infer<typeof rtkDiscoverInput>) => handleRtkDiscover(args),
},
omniroute_rtk_learn: {
name: "omniroute_rtk_learn",
description:
"Suggest an RTK filter draft for a specific command, learned from that command's captured " +
"outputs in the opt-in raw-output sample store. Read-only; returns a draft for the operator " +
"to review and save. Scope: read:compression.",
scopes: ["read:compression"],
inputSchema: rtkLearnInput,
handler: (args: z.infer<typeof rtkLearnInput>) => handleRtkLearn(args),
},
};
79 changes: 79 additions & 0 deletions tests/unit/compression/rtk-mcp-tools.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, it, beforeEach, after } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

// T07 — omniroute_rtk_discover / omniroute_rtk_learn MCP tools (read-only; audited).

const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omni-rtk-mcp-"));
process.env.DATA_DIR = TEST_DATA_DIR;

const core = await import("../../../src/lib/db/core.ts");
const { handleRtkDiscover, handleRtkLearn } = await import(
"../../../open-sse/mcp-server/tools/compressionTools.ts"
);
const { maybePersistRtkRawOutput } = await import(
"../../../open-sse/services/compression/engines/rtk/rawOutput.ts"
);
const { getRecentAuditEntries } = await import("../../../open-sse/mcp-server/audit.ts");

const NOISE = [
"Welcome to Gradle 8.5!",
"> Task :app:compileJava UP-TO-DATE",
"Downloading https://repo1.maven.org/foo.jar",
"BUILD SUCCESSFUL in 12s",
].join("\n");

function seedSamples() {
// retention "always" forces a capture even on a successful (non-failure) output.
maybePersistRtkRawOutput(NOISE, { retention: "always", command: "gradle build" });
maybePersistRtkRawOutput(NOISE, { retention: "always", command: "gradle build" });
}

beforeEach(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
core.getDbInstance(); // run migrations → mcp_tool_audit table exists
});

after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});

describe("RTK MCP tools (T07)", () => {
it("omniroute_rtk_discover returns ranked noise candidates from the sample store", async () => {
seedSamples();
const result = await handleRtkDiscover({ limit: 100 });
assert.equal(typeof result.sampleCount, "number");
assert.ok(result.sampleCount >= 2, `expected the 2 seeded samples, got ${result.sampleCount}`);
assert.ok(Array.isArray(result.candidates));
});

it("omniroute_rtk_learn returns a filter draft for a specific command", async () => {
seedSamples();
const result = await handleRtkLearn({ command: "gradle build", limit: 100 });
assert.equal(result.command, "gradle build");
assert.ok(result.sampleCount >= 1, "expected at least one matching sample");
assert.ok(result.filter && typeof result.filter === "object", "expected a suggested filter draft");
});

it("returns an empty/baseline result with no samples (no throw)", async () => {
const discover = await handleRtkDiscover({});
assert.equal(discover.sampleCount, 0);
assert.deepEqual(discover.candidates, []);
const learn = await handleRtkLearn({ command: "npm test" });
assert.equal(learn.sampleCount, 0);
});

it("both tools are audited in mcp_tool_audit", async () => {
await handleRtkDiscover({});
await handleRtkLearn({ command: "gradle build" });
const entries = await getRecentAuditEntries(20);
const names = entries.map((e) => e.toolName);
assert.ok(names.includes("omniroute_rtk_discover"), "discover must be audited");
assert.ok(names.includes("omniroute_rtk_learn"), "learn must be audited");
});
});
Loading