Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce2c30c437 | |||
| d56fae0a7b |
@@ -4,6 +4,26 @@
|
||||
|
||||
---
|
||||
|
||||
## [2.6.8] — 2026-03-17
|
||||
|
||||
> Sprint: Combo as Agent (system prompt + tool filter), Context Caching Protection, Auto-Update, Detailed Logs, MITM Kiro IDE.
|
||||
|
||||
### 🗄️ DB Migrations (zero-breaking — safe for existing users)
|
||||
|
||||
- **005_combo_agent_fields.sql**: `ALTER TABLE combos ADD COLUMN system_message TEXT DEFAULT NULL`, `tool_filter_regex TEXT DEFAULT NULL`, `context_cache_protection INTEGER DEFAULT 0`
|
||||
- **006_detailed_request_logs.sql**: New `request_detail_logs` table with 500-entry ring-buffer trigger, opt-in via settings toggle
|
||||
|
||||
### ✨ Features
|
||||
|
||||
- **feat(combo)**: System Message Override per Combo (#399 — `system_message` field replaces or injects system prompt before forwarding to provider)
|
||||
- **feat(combo)**: Tool Filter Regex per Combo (#399 — `tool_filter_regex` keeps only tools matching pattern; supports OpenAI + Anthropic formats)
|
||||
- **feat(combo)**: Context Caching Protection (#401 — `context_cache_protection` tags responses with `<omniModel>provider/model</omniModel>` and pins model for session continuity)
|
||||
- **feat(settings)**: Auto-Update via Settings (#320 — `GET /api/system/version` + `POST /api/system/update` — checks npm registry and updates in background with pm2 restart)
|
||||
- **feat(logs)**: Detailed Request Logs (#378 — captures full pipeline bodies at 4 stages: client request, translated request, provider response, client response — opt-in toggle, 64KB trim, 500-entry ring-buffer)
|
||||
- **feat(mitm)**: MITM Kiro IDE profile (#336 — `src/mitm/targets/kiro.ts` targets api.anthropic.com, reuses existing MITM infrastructure)
|
||||
|
||||
---
|
||||
|
||||
## [2.6.7] — 2026-03-17
|
||||
|
||||
> Sprint: SSE improvements, local provider_nodes extensions, proxy registry, Claude passthrough fixes.
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: OmniRoute API
|
||||
version: 2.6.7
|
||||
version: 2.6.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,
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as semaphore from "./rateLimitSemaphore.ts";
|
||||
import { getCircuitBreaker } from "../../src/shared/utils/circuitBreaker";
|
||||
import { fisherYatesShuffle, getNextFromDeck } from "../../src/shared/utils/shuffleDeck";
|
||||
import { parseModel } from "./model.ts";
|
||||
import { applyComboAgentMiddleware, injectModelTag } from "./comboAgentMiddleware.ts";
|
||||
|
||||
// Status codes that should mark semaphore + record circuit breaker failures
|
||||
const TRANSIENT_FOR_BREAKER = [429, 502, 503, 504];
|
||||
@@ -225,12 +226,49 @@ export async function handleComboChat({
|
||||
const strategy = combo.strategy || "priority";
|
||||
const models = combo.models || [];
|
||||
|
||||
// ── Combo Agent Middleware (#399 + #401) ────────────────────────────────
|
||||
// Apply system_message override, tool_filter_regex, and extract pinned model
|
||||
// from context caching tag. These are all opt-in per combo config.
|
||||
const { body: agentBody, pinnedModel } = applyComboAgentMiddleware(
|
||||
body,
|
||||
combo,
|
||||
"" // provider/model not yet known — resolved per-model in loop
|
||||
);
|
||||
body = agentBody;
|
||||
if (pinnedModel) {
|
||||
log.info("COMBO", `[#401] Context caching: pinned model=${pinnedModel}`);
|
||||
}
|
||||
// Wrap handleSingleModel to inject context caching tag on response (#401)
|
||||
const handleSingleModelWrapped = combo.context_cache_protection
|
||||
? async (b, modelStr) => {
|
||||
const res = await handleSingleModel(b, modelStr);
|
||||
// Inject tag only on success and only for non-streaming non-binary responses
|
||||
if (res.ok && !b.stream) {
|
||||
try {
|
||||
const json = await res.clone().json();
|
||||
const msgs = Array.isArray(json?.messages) ? json.messages : [];
|
||||
if (msgs.length > 0) {
|
||||
const tagged = injectModelTag(msgs, modelStr);
|
||||
return new Response(JSON.stringify({ ...json, messages: tagged }), {
|
||||
status: res.status,
|
||||
headers: res.headers,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
/* non-JSON or stream — skip tagging */
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
: handleSingleModel;
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Route to round-robin handler if strategy matches
|
||||
if (strategy === "round-robin") {
|
||||
return handleRoundRobinCombo({
|
||||
body,
|
||||
combo,
|
||||
handleSingleModel,
|
||||
handleSingleModel: handleSingleModelWrapped,
|
||||
isModelAvailable,
|
||||
log,
|
||||
settings,
|
||||
@@ -348,7 +386,7 @@ export async function handleComboChat({
|
||||
`Trying model ${i + 1}/${orderedModels.length}: ${modelStr}${retry > 0 ? ` (retry ${retry})` : ""}`
|
||||
);
|
||||
|
||||
const result = await handleSingleModel(body, modelStr);
|
||||
const result = await handleSingleModelWrapped(body, modelStr);
|
||||
|
||||
// Success — return response
|
||||
if (result.ok) {
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* comboAgentMiddleware.ts — Combo Agent Features
|
||||
*
|
||||
* Implements the "combo as agent" features from issues #399 and #401:
|
||||
*
|
||||
* 1. **System Message Override** (#399): If the combo defines a `system_message`,
|
||||
* it is injected as the first system message, replacing any existing system message.
|
||||
*
|
||||
* 2. **Tool Filter Regex** (#399): If the combo defines a `tool_filter_regex`,
|
||||
* only tools whose name matches the pattern are forwarded to the provider.
|
||||
*
|
||||
* 3. **Context Caching Protection** (#401): If the combo enables
|
||||
* `context_cache_protection`, the proxy:
|
||||
* a. On response: injects `<omniModel>provider/model</omniModel>` tag into
|
||||
* the first assistant message content string.
|
||||
* b. On request: scans the message history for the tag, and if found,
|
||||
* overrides the requested model with the pinned one.
|
||||
*
|
||||
* All features are opt-in per combo and backward compatible with existing setups.
|
||||
*/
|
||||
|
||||
interface ComboConfig {
|
||||
system_message?: string | null;
|
||||
tool_filter_regex?: string | null;
|
||||
context_cache_protection?: number | boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface Message {
|
||||
role?: string;
|
||||
content?: unknown;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// ── Context Caching Tag ─────────────────────────────────────────────────────
|
||||
|
||||
const CACHE_TAG_PATTERN = /<omniModel>([^<]+)<\/omniModel>/;
|
||||
|
||||
/**
|
||||
* Inject the model tag into the last assistant message (or append a new one).
|
||||
* Only modifies string content — does not touch array content to avoid breaking
|
||||
* Claude/Gemini multi-part message formats.
|
||||
*/
|
||||
export function injectModelTag(messages: Message[], providerModel: string): Message[] {
|
||||
// Remove any existing tag first to avoid duplication on context compaction
|
||||
const cleaned = messages.map((msg) => {
|
||||
if (msg.role === "assistant" && typeof msg.content === "string") {
|
||||
return { ...msg, content: msg.content.replace(CACHE_TAG_PATTERN, "").trimEnd() };
|
||||
}
|
||||
return msg;
|
||||
});
|
||||
|
||||
// Find last assistant message with string content
|
||||
const lastAssistantIdx = cleaned.map((m) => m.role).lastIndexOf("assistant");
|
||||
if (lastAssistantIdx === -1) return cleaned;
|
||||
|
||||
const msg = cleaned[lastAssistantIdx];
|
||||
if (typeof msg.content !== "string") return cleaned;
|
||||
|
||||
const tagged = [...cleaned];
|
||||
tagged[lastAssistantIdx] = {
|
||||
...msg,
|
||||
content: `${msg.content}\n<omniModel>${providerModel}</omniModel>`,
|
||||
};
|
||||
return tagged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan message history for the model tag injected by a previous response.
|
||||
* Returns the pinned "provider/model" string, or null if not found.
|
||||
*/
|
||||
export function extractPinnedModel(messages: Message[]): string | null {
|
||||
// Scan from newest to oldest for efficiency
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i];
|
||||
if (msg.role === "assistant" && typeof msg.content === "string") {
|
||||
const match = CACHE_TAG_PATTERN.exec(msg.content);
|
||||
if (match) return match[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── System Message Override ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Replace or inject a system message at the beginning of the messages array.
|
||||
* Existing system messages are removed if a combo override is set.
|
||||
*/
|
||||
export function applySystemMessageOverride(messages: Message[], systemMessage: string): Message[] {
|
||||
// Remove all existing system messages
|
||||
const filtered = messages.filter((m) => m.role !== "system");
|
||||
// Inject combo system message at start
|
||||
return [{ role: "system", content: systemMessage }, ...filtered];
|
||||
}
|
||||
|
||||
// ── Tool Filter Regex ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Filter the tools array, keeping only tools whose name matches the regex.
|
||||
* Returns the original array unchanged if pattern is null/empty.
|
||||
*/
|
||||
export function applyToolFilter(
|
||||
tools: unknown[] | undefined,
|
||||
pattern: string | null | undefined
|
||||
): unknown[] | undefined {
|
||||
if (!tools || !pattern) return tools;
|
||||
|
||||
let regex: RegExp;
|
||||
try {
|
||||
regex = new RegExp(pattern);
|
||||
} catch {
|
||||
// Invalid regex — return tools unchanged rather than crashing
|
||||
console.warn(`[ComboAgent] Invalid tool_filter_regex: "${pattern}"`);
|
||||
return tools;
|
||||
}
|
||||
|
||||
return tools.filter((tool) => {
|
||||
const t = tool as Record<string, unknown>;
|
||||
// Support both OpenAI format ({ function: { name } }) and Anthropic ({ name })
|
||||
const name = (t.function as Record<string, unknown> | undefined)?.name ?? t.name ?? "";
|
||||
return regex.test(String(name));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Main Middleware ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Apply all combo agent features to the request body.
|
||||
* Safe to call with null/undefined comboConfig — returns body unchanged.
|
||||
*/
|
||||
export function applyComboAgentMiddleware(
|
||||
body: Record<string, unknown>,
|
||||
comboConfig: ComboConfig | null | undefined,
|
||||
providerModel: string // "provider/model" string for context caching
|
||||
): { body: Record<string, unknown>; pinnedModel: string | null } {
|
||||
if (!comboConfig) return { body, pinnedModel: null };
|
||||
|
||||
let messages: Message[] = Array.isArray(body.messages) ? [...body.messages] : [];
|
||||
let pinnedModel: string | null = null;
|
||||
|
||||
// 1. Context caching: check for pinned model in history
|
||||
if (comboConfig.context_cache_protection) {
|
||||
pinnedModel = extractPinnedModel(messages);
|
||||
if (pinnedModel) {
|
||||
// Model is pinned — caller should override model selection
|
||||
}
|
||||
}
|
||||
|
||||
// 2. System message override
|
||||
if (comboConfig.system_message && comboConfig.system_message.trim()) {
|
||||
messages = applySystemMessageOverride(messages, comboConfig.system_message);
|
||||
}
|
||||
|
||||
// 3. Tool filter
|
||||
const filteredTools = applyToolFilter(
|
||||
body.tools as unknown[] | undefined,
|
||||
comboConfig.tool_filter_regex
|
||||
);
|
||||
|
||||
return {
|
||||
body: {
|
||||
...body,
|
||||
messages,
|
||||
...(filteredTools !== body.tools && { tools: filteredTools }),
|
||||
},
|
||||
pinnedModel,
|
||||
};
|
||||
}
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "2.6.7",
|
||||
"version": "2.6.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "omniroute",
|
||||
"version": "2.6.7",
|
||||
"version": "2.6.8",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "2.6.7",
|
||||
"version": "2.6.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": {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* GET /api/logs/detail — List detailed request logs
|
||||
* GET /api/logs/detail/:id — Get specific detailed log
|
||||
* POST /api/logs/detail/toggle — Enable/disable detailed logging
|
||||
*/
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import {
|
||||
getRequestDetailLogs,
|
||||
getRequestDetailLogCount,
|
||||
isDetailedLoggingEnabled,
|
||||
} from "@/lib/db/detailedLogs";
|
||||
import { updateSettings } from "@/lib/db/settings";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
if (!isAuthenticated(req)) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const url = new URL(req.url);
|
||||
const limit = Math.min(Number(url.searchParams.get("limit") ?? 50), 200);
|
||||
const offset = Number(url.searchParams.get("offset") ?? 0);
|
||||
|
||||
const logs = getRequestDetailLogs(limit, offset);
|
||||
const total = getRequestDetailLogCount();
|
||||
const enabled = await isDetailedLoggingEnabled();
|
||||
|
||||
return NextResponse.json({ enabled, total, logs });
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
if (!isAuthenticated(req)) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const enabled = body.enabled === true || body.enabled === "1";
|
||||
|
||||
await updateSettings({ detailed_logs_enabled: enabled });
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
enabled,
|
||||
message: enabled
|
||||
? "Detailed logging enabled. Pipeline bodies will be captured for new requests."
|
||||
: "Detailed logging disabled.",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* GET /api/system/version — Returns current version and latest available on npm
|
||||
* POST /api/system/update — Triggers npm install -g omniroute@latest + pm2 restart
|
||||
*
|
||||
* Security: Requires admin authentication (same as other management routes).
|
||||
* Safety: Update only runs if a newer version is available on npm.
|
||||
*/
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { execFile } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/** Fetch latest version from npm registry (no install, just metadata) */
|
||||
async function getLatestNpmVersion(): Promise<string | null> {
|
||||
try {
|
||||
const { stdout } = await execFileAsync("npm", ["info", "omniroute", "version", "--json"], {
|
||||
timeout: 10000,
|
||||
});
|
||||
const parsed = JSON.parse(stdout.trim());
|
||||
return typeof parsed === "string" ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Current installed version from package.json */
|
||||
function getCurrentVersion(): string {
|
||||
try {
|
||||
|
||||
return require("../../../../../package.json").version as string;
|
||||
} catch {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/** Compare semver strings — returns true if a > b */
|
||||
function isNewer(a: string | null, b: string): boolean {
|
||||
if (!a) return false;
|
||||
const parse = (v: string) => v.split(".").map(Number);
|
||||
const [aMaj, aMin, aPat] = parse(a);
|
||||
const [bMaj, bMin, bPat] = parse(b);
|
||||
if (aMaj !== bMaj) return aMaj > bMaj;
|
||||
if (aMin !== bMin) return aMin > bMin;
|
||||
return aPat > bPat;
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
if (!isAuthenticated(req)) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const current = getCurrentVersion();
|
||||
const latest = await getLatestNpmVersion();
|
||||
const updateAvailable = isNewer(latest, current);
|
||||
|
||||
return NextResponse.json({
|
||||
current,
|
||||
latest: latest ?? "unavailable",
|
||||
updateAvailable,
|
||||
channel: "npm",
|
||||
});
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
if (!isAuthenticated(req)) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const current = getCurrentVersion();
|
||||
const latest = await getLatestNpmVersion();
|
||||
|
||||
if (!latest) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Could not reach npm registry" },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!isNewer(latest, current)) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: `Already on latest version (${current})`,
|
||||
current,
|
||||
latest,
|
||||
});
|
||||
}
|
||||
|
||||
// Run update in background — client gets immediate acknowledgment
|
||||
const install = async () => {
|
||||
try {
|
||||
await execFileAsync("npm", ["install", "-g", `omniroute@${latest}`, "--ignore-scripts"], {
|
||||
timeout: 300000, // 5 minutes
|
||||
});
|
||||
// Restart PM2 — non-fatal if pm2 not available (Docker/manual setups)
|
||||
await execFileAsync("pm2", ["restart", "omniroute"]).catch(() => null);
|
||||
console.log(`[AutoUpdate] Successfully updated to v${latest}`);
|
||||
} catch (err) {
|
||||
console.error(`[AutoUpdate] Update failed:`, err);
|
||||
}
|
||||
};
|
||||
|
||||
// Fire-and-forget
|
||||
install();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Update to v${latest} started. Restarting in ~30 seconds.`,
|
||||
from: current,
|
||||
to: latest,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Detailed Request Logs DB Layer (#378)
|
||||
*
|
||||
* Saves full request/response bodies at each pipeline stage.
|
||||
* Ring-buffer of 500 entries enforced by SQL trigger in migration 006.
|
||||
* Only active when settings.detailed_logs_enabled = "1".
|
||||
*/
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { getDbInstance } from "./core";
|
||||
import { getSettings } from "./settings";
|
||||
|
||||
export interface RequestDetailLog {
|
||||
id?: string;
|
||||
call_log_id?: string | null;
|
||||
timestamp?: string;
|
||||
client_request?: string | null;
|
||||
translated_request?: string | null;
|
||||
provider_response?: string | null;
|
||||
client_response?: string | null;
|
||||
provider?: string | null;
|
||||
model?: string | null;
|
||||
source_format?: string | null;
|
||||
target_format?: string | null;
|
||||
duration_ms?: number;
|
||||
}
|
||||
|
||||
/** Returns true if detailed logging is enabled in settings */
|
||||
export async function isDetailedLoggingEnabled(): Promise<boolean> {
|
||||
try {
|
||||
const settings = await getSettings();
|
||||
const val = settings.detailed_logs_enabled;
|
||||
return val === true || val === "1" || val === "true";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Save a detailed log entry — caller must verify isDetailedLoggingEnabled() first */
|
||||
export function saveRequestDetailLog(entry: RequestDetailLog): void {
|
||||
const db = getDbInstance();
|
||||
const id = entry.id ?? uuidv4();
|
||||
const timestamp = entry.timestamp ?? new Date().toISOString();
|
||||
|
||||
// Trim large bodies to avoid excessive disk usage (max 64KB each)
|
||||
const trim = (s: string | null | undefined, max = 65536): string | null => {
|
||||
if (!s) return null;
|
||||
return s.length > max ? s.slice(0, max) + "…[truncated]" : s;
|
||||
};
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO request_detail_logs
|
||||
(id, call_log_id, timestamp, client_request, translated_request,
|
||||
provider_response, client_response, provider, model, source_format, target_format, duration_ms)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
).run(
|
||||
id,
|
||||
entry.call_log_id ?? null,
|
||||
timestamp,
|
||||
trim(entry.client_request),
|
||||
trim(entry.translated_request),
|
||||
trim(entry.provider_response),
|
||||
trim(entry.client_response),
|
||||
entry.provider ?? null,
|
||||
entry.model ?? null,
|
||||
entry.source_format ?? null,
|
||||
entry.target_format ?? null,
|
||||
entry.duration_ms ?? 0
|
||||
);
|
||||
}
|
||||
|
||||
/** Fetch detailed logs (latest first) */
|
||||
export function getRequestDetailLogs(limit = 50, offset = 0): RequestDetailLog[] {
|
||||
const db = getDbInstance();
|
||||
return db
|
||||
.prepare(
|
||||
`
|
||||
SELECT * FROM request_detail_logs
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`
|
||||
)
|
||||
.all(limit, offset) as RequestDetailLog[];
|
||||
}
|
||||
|
||||
/** Get a single detailed log by ID */
|
||||
export function getRequestDetailLogById(id: string): RequestDetailLog | null {
|
||||
const db = getDbInstance();
|
||||
return (db.prepare("SELECT * FROM request_detail_logs WHERE id = ?").get(id) ??
|
||||
null) as RequestDetailLog | null;
|
||||
}
|
||||
|
||||
/** Get total count of detailed logs */
|
||||
export function getRequestDetailLogCount(): number {
|
||||
const db = getDbInstance();
|
||||
const row = db.prepare("SELECT COUNT(*) as cnt FROM request_detail_logs").get() as {
|
||||
cnt: number;
|
||||
};
|
||||
return row?.cnt ?? 0;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
-- 005_combo_agent_fields.sql
|
||||
-- Safe migration for existing users: adds optional agent fields to combos.
|
||||
-- Uses ADD COLUMN with DEFAULT NULL (SQLite compatible) — existing rows are untouched.
|
||||
-- New fields are read as NULL by old code versions (backward compatible).
|
||||
|
||||
-- System prompt override: when set, injected as the first system message before
|
||||
-- forwarding to the provider. Overrides any system message from the client.
|
||||
ALTER TABLE combos ADD COLUMN system_message TEXT DEFAULT NULL;
|
||||
|
||||
-- Regex-based tool filter: when set, only tool calls whose "name" matches this
|
||||
-- regex pattern are forwarded to the provider. Others are stripped silently.
|
||||
-- Example: "^(gh_|create_file|web_fetch)" — allows only GitHub and web tools.
|
||||
ALTER TABLE combos ADD COLUMN tool_filter_regex TEXT DEFAULT NULL;
|
||||
|
||||
-- Context caching protection: when 1, the proxy tags assistant responses with
|
||||
-- <omniModel>provider/model</omniModel> and pins the model for the session.
|
||||
ALTER TABLE combos ADD COLUMN context_cache_protection INTEGER DEFAULT 0;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_combos_cache_protection ON combos(context_cache_protection);
|
||||
@@ -0,0 +1,42 @@
|
||||
-- 006_detailed_request_logs.sql
|
||||
-- Stores full request/response bodies at each pipeline stage for debugging.
|
||||
-- Only populated when detailed_logs_enabled = 1 in settings (off by default).
|
||||
-- Ring-buffer enforced via trigger: keeps only the last 500 entries.
|
||||
-- Existing users are not impacted (table is new, feature is opt-in).
|
||||
|
||||
CREATE TABLE IF NOT EXISTS request_detail_logs (
|
||||
id TEXT PRIMARY KEY,
|
||||
call_log_id TEXT, -- FK to call_logs.id (optional, nullable)
|
||||
timestamp TEXT NOT NULL,
|
||||
-- The 4 pipeline stages (all nullable — only populated when available)
|
||||
client_request TEXT, -- Raw body received from the client (JSON)
|
||||
translated_request TEXT, -- Body after format translation (JSON)
|
||||
provider_response TEXT, -- Raw body from the provider (JSON)
|
||||
client_response TEXT, -- Final body sent to the client (JSON)
|
||||
-- Metadata
|
||||
provider TEXT,
|
||||
model TEXT,
|
||||
source_format TEXT,
|
||||
target_format TEXT,
|
||||
duration_ms INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_rdl_timestamp ON request_detail_logs(timestamp);
|
||||
CREATE INDEX IF NOT EXISTS idx_rdl_call_log_id ON request_detail_logs(call_log_id);
|
||||
|
||||
-- Ring-buffer trigger: auto-delete oldest records beyond 500
|
||||
CREATE TRIGGER IF NOT EXISTS trg_rdl_ring_buffer
|
||||
AFTER INSERT ON request_detail_logs
|
||||
BEGIN
|
||||
DELETE FROM request_detail_logs
|
||||
WHERE id IN (
|
||||
SELECT id FROM request_detail_logs
|
||||
ORDER BY timestamp ASC
|
||||
LIMIT MAX(0, (SELECT COUNT(*) FROM request_detail_logs) - 500)
|
||||
);
|
||||
END;
|
||||
|
||||
-- Settings key for enabling/disabling detailed logs (default: disabled)
|
||||
-- Inserted only if not already present (safe for existing installs)
|
||||
INSERT OR IGNORE INTO key_value (namespace, key, value)
|
||||
VALUES ('settings', 'detailed_logs_enabled', '0');
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Kiro IDE MITM Configuration (#336)
|
||||
*
|
||||
* Kiro IDE removed the Base URL / API Key configuration UI.
|
||||
* To route Kiro's traffic through OmniRoute, we intercept it using MITM,
|
||||
* similar to the existing Antigravity/Claude Code implementation.
|
||||
*
|
||||
* Kiro IDE uses the Anthropic API at https://api.anthropic.com:
|
||||
* - Main endpoint: POST /v1/messages
|
||||
* - Auth header: x-api-key: <key>
|
||||
* - User-Agent contains: "kiro" or "Kiro"
|
||||
*
|
||||
* To use: Install OmniRoute's MITM certificate, then run:
|
||||
* omniroute mitm start --targets kiro
|
||||
*
|
||||
* The MITM server intercepts requests to api.anthropic.com and forwards
|
||||
* them to the OmniRoute proxy (localhost:20128) instead.
|
||||
*/
|
||||
|
||||
export interface MitmTarget {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
targetHost: string;
|
||||
targetPort: number;
|
||||
localPort: number;
|
||||
userAgentPattern: string | null;
|
||||
apiEndpoints: string[];
|
||||
authHeader: string;
|
||||
instructions: string[];
|
||||
referenceIde?: string;
|
||||
}
|
||||
|
||||
/** Kiro IDE MITM profile */
|
||||
export const KIRO_MITM_PROFILE: MitmTarget = {
|
||||
id: "kiro",
|
||||
name: "Kiro IDE",
|
||||
description:
|
||||
"Intercepts Kiro IDE requests to api.anthropic.com and routes them through OmniRoute.",
|
||||
targetHost: "api.anthropic.com",
|
||||
targetPort: 443,
|
||||
localPort: 20130,
|
||||
userAgentPattern: null, // Kiro does not expose a stable User-Agent
|
||||
apiEndpoints: ["/v1/messages"],
|
||||
authHeader: "x-api-key",
|
||||
instructions: [
|
||||
"1. Install OmniRoute's root certificate: run `omniroute cert install` or go to Settings → MITM Certificates",
|
||||
"2. Start the MITM proxy: `omniroute mitm start --target kiro`",
|
||||
"3. Set your system HTTP proxy to 127.0.0.1:20130 (or use transparent MITM via DNS override)",
|
||||
"4. Open Kiro IDE — API calls will be automatically routed through OmniRoute.",
|
||||
"5. Verify: check the Proxy Logs in OmniRoute dashboard and look for provider=anthropic source=mitm",
|
||||
],
|
||||
referenceIde: "antigravity", // Same MITM infrastructure as Antigravity
|
||||
};
|
||||
Reference in New Issue
Block a user