Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b68deb0f6 | |||
| d1497c9ac8 | |||
| 03d4cbf6d5 |
@@ -4,6 +4,21 @@
|
||||
|
||||
---
|
||||
|
||||
## [2.7.8] — 2026-03-18
|
||||
|
||||
> Sprint: Budget save bug + combo agent features UI + omniModel tag security fix.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **fix(budget)**: "Save Limits" no longer returns 422 — `warningThreshold` is now correctly sent as fraction (0–1) instead of percentage (0–100) (#451)
|
||||
- **fix(combos)**: `<omniModel>` internal cache tag is now stripped before forwarding requests to providers, preventing cache session breaks (#454)
|
||||
|
||||
### ✨ Features
|
||||
|
||||
- **feat(combos)**: Agent Features section added to combo create/edit modal — expose `system_message` override, `tool_filter_regex`, and `context_cache_protection` directly from the dashboard (#454)
|
||||
|
||||
---
|
||||
|
||||
## [2.7.7] — 2026-03-18
|
||||
|
||||
> Sprint: Docker pino crash, Codex CLI responses worker fix, package-lock sync.
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: OmniRoute API
|
||||
version: 2.7.7
|
||||
version: 2.7.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,
|
||||
|
||||
@@ -123,6 +123,20 @@ export function applyToolFilter(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip all <omniModel> tags from message content before forwarding to the provider.
|
||||
* The tag is an internal OmniRoute marker; providers must never see it or their
|
||||
* cache will treat every tagged request as a new session (#454).
|
||||
*/
|
||||
export function stripModelTags(messages: Message[]): Message[] {
|
||||
return messages.map((msg) => {
|
||||
if (typeof msg.content === "string" && CACHE_TAG_PATTERN.test(msg.content)) {
|
||||
return { ...msg, content: msg.content.replace(CACHE_TAG_PATTERN, "").trimEnd() };
|
||||
}
|
||||
return msg;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Main Middleware ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -158,6 +172,11 @@ export function applyComboAgentMiddleware(
|
||||
comboConfig.tool_filter_regex
|
||||
);
|
||||
|
||||
// 4. Strip internal <omniModel> tags before forwarding to provider (#454)
|
||||
// These tags are OmniRoute-internal markers and must never reach the provider
|
||||
// since providers would treat each tagged request as a new cache session.
|
||||
messages = stripModelTags(messages);
|
||||
|
||||
return {
|
||||
body: {
|
||||
...body,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "2.7.7",
|
||||
"version": "2.7.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": {
|
||||
|
||||
@@ -1181,6 +1181,12 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
|
||||
const [config, setConfig] = useState(combo?.config || {});
|
||||
const [showStrategyNudge, setShowStrategyNudge] = useState(false);
|
||||
const strategyChangeMountedRef = useRef(false);
|
||||
// Agent features (#399 / #401 / #454)
|
||||
const [agentSystemMessage, setAgentSystemMessage] = useState<string>(combo?.system_message || "");
|
||||
const [agentToolFilter, setAgentToolFilter] = useState<string>(combo?.tool_filter_regex || "");
|
||||
const [agentContextCache, setAgentContextCache] = useState<boolean>(
|
||||
!!combo?.context_cache_protection
|
||||
);
|
||||
|
||||
// DnD state
|
||||
const hasPricingForModel = useCallback(
|
||||
@@ -1532,6 +1538,14 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
|
||||
saveData.config = configToSave;
|
||||
}
|
||||
|
||||
// Agent features (#399 / #401 / #454)
|
||||
if (agentSystemMessage.trim()) saveData.system_message = agentSystemMessage.trim();
|
||||
else delete saveData.system_message;
|
||||
if (agentToolFilter.trim()) saveData.tool_filter_regex = agentToolFilter.trim();
|
||||
else delete saveData.tool_filter_regex;
|
||||
if (agentContextCache) saveData.context_cache_protection = true;
|
||||
else delete saveData.context_cache_protection;
|
||||
|
||||
await onSave(saveData);
|
||||
setSaving(false);
|
||||
};
|
||||
@@ -2052,6 +2066,72 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Agent Features (#399 / #401 / #454) */}
|
||||
<div className="flex flex-col gap-2 p-3 bg-black/[0.02] dark:bg-white/[0.02] rounded-lg border border-black/5 dark:border-white/5">
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<span className="material-symbols-outlined text-[14px] text-primary">smart_toy</span>
|
||||
<p className="text-xs font-medium">Agent Features</p>
|
||||
<span className="text-[10px] text-text-muted">
|
||||
— optional, for agent/tool workflows
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* System Message Override */}
|
||||
<div>
|
||||
<label className="text-[11px] font-medium text-text-muted block mb-0.5">
|
||||
System Message Override
|
||||
</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
value={agentSystemMessage}
|
||||
onChange={(e) => setAgentSystemMessage(e.target.value)}
|
||||
placeholder="Override the system prompt for all requests routed through this combo…"
|
||||
className="w-full text-xs py-1.5 px-2 rounded border border-black/10 dark:border-white/10 bg-transparent focus:border-primary focus:outline-none resize-none"
|
||||
/>
|
||||
<p className="text-[10px] text-text-muted mt-0.5">
|
||||
Replaces any system message sent by the client. Leave empty to pass through client
|
||||
system messages.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tool Filter Regex */}
|
||||
<div>
|
||||
<label className="text-[11px] font-medium text-text-muted block mb-0.5">
|
||||
Tool Filter Regex
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={agentToolFilter}
|
||||
onChange={(e) => setAgentToolFilter(e.target.value)}
|
||||
placeholder="e.g. ^(bash|computer)$"
|
||||
className="w-full text-xs py-1.5 px-2 rounded border border-black/10 dark:border-white/10 bg-transparent focus:border-primary focus:outline-none font-mono"
|
||||
/>
|
||||
<p className="text-[10px] text-text-muted mt-0.5">
|
||||
Only tools whose name matches this regex are forwarded to the provider. Leave empty
|
||||
to forward all tools.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Context Cache Protection */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div>
|
||||
<label className="text-[11px] font-medium text-text-muted block">
|
||||
Context Cache Protection
|
||||
</label>
|
||||
<p className="text-[10px] text-text-muted">
|
||||
Pins the provider/model across turns to preserve cache sessions. Internal tags are
|
||||
stripped before forwarding to the provider.
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={agentContextCache}
|
||||
onChange={(e) => setAgentContextCache(e.target.checked)}
|
||||
className="accent-primary shrink-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2 pt-1">
|
||||
<Button onClick={onClose} variant="ghost" fullWidth size="sm">
|
||||
|
||||
@@ -83,7 +83,11 @@ export default function BudgetTab() {
|
||||
if (data.monthlyLimitUsd)
|
||||
setForm((f) => ({ ...f, monthlyLimitUsd: String(data.monthlyLimitUsd) }));
|
||||
if (data.warningThreshold)
|
||||
setForm((f) => ({ ...f, warningThreshold: String(data.warningThreshold) }));
|
||||
// stored as fraction (0–1), display as percentage (0–100)
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
warningThreshold: String(Math.round(data.warningThreshold * 100)),
|
||||
}));
|
||||
}
|
||||
} catch {
|
||||
// silent
|
||||
@@ -104,7 +108,8 @@ export default function BudgetTab() {
|
||||
apiKeyId: selectedKey,
|
||||
dailyLimitUsd: form.dailyLimitUsd ? parseFloat(form.dailyLimitUsd) : null,
|
||||
monthlyLimitUsd: form.monthlyLimitUsd ? parseFloat(form.monthlyLimitUsd) : null,
|
||||
warningThreshold: parseInt(form.warningThreshold) || 80,
|
||||
// schema expects a fraction (0–1); UI shows percentage (0–100)
|
||||
warningThreshold: (parseInt(form.warningThreshold) || 80) / 100,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
|
||||
Reference in New Issue
Block a user