mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 15:22:12 +03:00
Merge pull request #2474 from Tentoxa/fix/memory-system-role-semantic-passthrough
fix: extract system role messages in semantic passthrough path to prevent 400 on memory injection
This commit is contained in:
@@ -600,7 +600,7 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98
|
||||
# Used by: open-sse/executors/base.ts — buildHeaders() dynamic lookup.
|
||||
# Update these when providers release new CLI versions to avoid blocks.
|
||||
|
||||
CLAUDE_USER_AGENT="claude-cli/2.1.145 (external, cli)"
|
||||
CLAUDE_USER_AGENT="claude-cli/2.1.146 (external, cli)"
|
||||
CODEX_USER_AGENT="codex-cli/0.132.0 (Windows 10.0.26200; x64)"
|
||||
GITHUB_USER_AGENT="GitHubCopilotChat/0.45.1"
|
||||
ANTIGRAVITY_USER_AGENT="antigravity/2.0.1 linux/arm64 google-api-nodejs-client/10.3.0"
|
||||
|
||||
@@ -12,6 +12,9 @@ const ANTHROPIC_BETA_BASE = Object.freeze([
|
||||
"fast-mode-2026-02-01",
|
||||
"redact-thinking-2026-02-12",
|
||||
"token-efficient-tools-2026-03-28",
|
||||
"advisor-tool-2026-03-01",
|
||||
"extended-cache-ttl-2025-04-11",
|
||||
"cache-diagnosis-2026-04-07",
|
||||
]);
|
||||
|
||||
const CLAUDE_OAUTH_EXTRA_BETAS = Object.freeze(["fine-grained-tool-streaming-2025-05-14"]);
|
||||
@@ -26,7 +29,7 @@ export const ANTHROPIC_BETA_CLAUDE_OAUTH = [
|
||||
...ANTHROPIC_BETA_BASE.slice(3),
|
||||
].join(",");
|
||||
|
||||
export const CLAUDE_CLI_VERSION = "2.1.137";
|
||||
export const CLAUDE_CLI_VERSION = "2.1.146";
|
||||
export const CLAUDE_CLI_USER_AGENT = `claude-cli/${CLAUDE_CLI_VERSION} (external, cli)`;
|
||||
export const CLAUDE_CLI_STAINLESS_PACKAGE_VERSION = "0.81.0";
|
||||
export const CLAUDE_CLI_STAINLESS_PACKAGE_VERSION = "0.94.0";
|
||||
export const CLAUDE_CLI_STAINLESS_RUNTIME_VERSION = "v24.3.0";
|
||||
|
||||
@@ -12,9 +12,9 @@ import { createHash, randomBytes, randomUUID } from "node:crypto";
|
||||
|
||||
// ---------- Versions ------------------------------------------------------
|
||||
|
||||
export const CLAUDE_CODE_VERSION = "2.1.131";
|
||||
export const CLAUDE_CODE_VERSION = "2.1.146";
|
||||
/** Bundled @anthropic-ai/sdk version for the pinned CLI release. */
|
||||
export const CLAUDE_CODE_STAINLESS_VERSION = "0.81.0";
|
||||
export const CLAUDE_CODE_STAINLESS_VERSION = "0.94.0";
|
||||
|
||||
// ---------- Stainless OS / Arch / Runtime --------------------------------
|
||||
|
||||
@@ -340,7 +340,7 @@ export function selectBetaFlags(
|
||||
if (hasStructuredOutput && !isFullAgent) flags.push("structured-outputs-2025-12-15");
|
||||
// extended-cache-ttl is sent for all full-agent shapes (incl. Haiku); the
|
||||
// heavier advanced-tool-use / effort flags are Opus/Sonnet-only.
|
||||
if (isFullAgent) flags.push("extended-cache-ttl-2025-04-11");
|
||||
if (isFullAgent) flags.push("extended-cache-ttl-2025-04-11", "cache-diagnosis-2026-04-07");
|
||||
if (isHeavyAgent) {
|
||||
flags.push("advanced-tool-use-2025-11-20", "effort-2025-11-24");
|
||||
}
|
||||
|
||||
@@ -1273,6 +1273,41 @@ function isCopilotClient(
|
||||
return false;
|
||||
}
|
||||
|
||||
export function extractSystemRoleMessages(payload: Record<string, unknown>): void {
|
||||
if (!Array.isArray(payload.messages)) return;
|
||||
const messages = payload.messages as Array<{ role?: unknown; content?: unknown }>;
|
||||
const systemMessages = messages.filter(
|
||||
(m) => typeof m.role === "string" && m.role.toLowerCase() === "system"
|
||||
);
|
||||
if (systemMessages.length === 0) return;
|
||||
|
||||
const extraBlocks: Array<Record<string, unknown>> = [];
|
||||
for (const sm of systemMessages) {
|
||||
if (typeof sm.content === "string" && sm.content.length > 0) {
|
||||
extraBlocks.push({ type: "text", text: sm.content });
|
||||
} else if (Array.isArray(sm.content)) {
|
||||
for (const block of sm.content as Array<Record<string, unknown>>) {
|
||||
if (block?.type === "text" && typeof block.text === "string" && block.text.length > 0) {
|
||||
extraBlocks.push({ ...block });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (extraBlocks.length > 0) {
|
||||
const existingSystem = payload.system;
|
||||
if (typeof existingSystem === "string" && existingSystem.length > 0) {
|
||||
payload.system = [{ type: "text", text: existingSystem }, ...extraBlocks];
|
||||
} else if (Array.isArray(existingSystem)) {
|
||||
payload.system = [...(existingSystem as Array<Record<string, unknown>>), ...extraBlocks];
|
||||
} else {
|
||||
payload.system = extraBlocks;
|
||||
}
|
||||
}
|
||||
payload.messages = messages.filter(
|
||||
(m) => typeof m.role !== "string" || m.role.toLowerCase() !== "system"
|
||||
);
|
||||
}
|
||||
|
||||
export async function handleChatCore({
|
||||
body,
|
||||
modelInfo,
|
||||
|
||||
@@ -114,7 +114,7 @@ export const CCH_SALT = "59cf53e54c78";
|
||||
/** Character positions sampled from the first user message text. */
|
||||
export const CCH_POSITIONS = [4, 7, 20] as const;
|
||||
/** Default `cc_version=` value embedded in the billing header. */
|
||||
export const DEFAULT_CLAUDE_CODE_VERSION = "2.1.137";
|
||||
export const DEFAULT_CLAUDE_CODE_VERSION = "2.1.146";
|
||||
/** Identity sentinel prepended for Claude Agent SDK callers. */
|
||||
export const CLAUDE_AGENT_SDK_IDENTITY =
|
||||
"You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
||||
|
||||
@@ -39,9 +39,9 @@ export const CLAUDE_CODE_COMPATIBLE_ANTHROPIC_BETA = [
|
||||
"interleaved-thinking-2025-05-14",
|
||||
"effort-2025-11-24",
|
||||
].join(",");
|
||||
export const CLAUDE_CODE_COMPATIBLE_VERSION = "2.1.137";
|
||||
export const CLAUDE_CODE_COMPATIBLE_USER_AGENT = "claude-cli/2.1.137 (external, sdk-cli)";
|
||||
export const CLAUDE_CODE_COMPATIBLE_STAINLESS_PACKAGE_VERSION = "0.81.0";
|
||||
export const CLAUDE_CODE_COMPATIBLE_VERSION = "2.1.146";
|
||||
export const CLAUDE_CODE_COMPATIBLE_USER_AGENT = "claude-cli/2.1.146 (external, sdk-cli)";
|
||||
export const CLAUDE_CODE_COMPATIBLE_STAINLESS_PACKAGE_VERSION = "0.94.0";
|
||||
export const CLAUDE_CODE_COMPATIBLE_STAINLESS_RUNTIME_VERSION = "v24.3.0";
|
||||
export const CONTEXT_1M_BETA_HEADER = "context-1m-2025-08-07";
|
||||
const CLAUDE_CODE_COMPATIBLE_DEFAULT_SYSTEM_BLOCKS = [
|
||||
|
||||
@@ -34,7 +34,6 @@ for (const [id, alias] of Object.entries(PROVIDER_ID_TO_ALIAS)) {
|
||||
// opencode/ → opencode-zen (the main free/open tier; opencode-go is a separate paid tier)
|
||||
ALIAS_TO_PROVIDER_ID["opencode"] = "opencode-zen";
|
||||
|
||||
|
||||
// Manual aliases for external compatibility not covered by PROVIDER_ID_TO_ALIAS.
|
||||
// OpenCode's Zen provider now uses the "opencode" slug, but OmniRoute registers
|
||||
// it as "opencode-zen". This alias ensures `opencode/<model>` resolves correctly.
|
||||
|
||||
@@ -11,7 +11,11 @@ interface Role {
|
||||
|
||||
const HERMES_ROLES: Role[] = [
|
||||
{ id: "default", label: "Default (main)", description: "Primary conversation model" },
|
||||
{ id: "delegation", label: "Delegation (subagents)", description: "Orchestrator and sub-agent model" },
|
||||
{
|
||||
id: "delegation",
|
||||
label: "Delegation (subagents)",
|
||||
description: "Orchestrator and sub-agent model",
|
||||
},
|
||||
{ id: "vision", label: "Vision", description: "Image and screenshot understanding" },
|
||||
{ id: "compression", label: "Compression", description: "Prompt compression & summarization" },
|
||||
{ id: "web_extract", label: "Web Extract", description: "Web page content extraction" },
|
||||
@@ -60,9 +64,11 @@ export default function HermesAgentToolCard({
|
||||
useEffect(() => {
|
||||
if (isExpanded) {
|
||||
// Phase 3: Seed from detector snapshot (batchStatus) for instant UI
|
||||
if (!seededFromBatchRef.current &&
|
||||
Object.keys(currentRoles).length === 0 &&
|
||||
batchStatus?.hermesAgentRoles) {
|
||||
if (
|
||||
!seededFromBatchRef.current &&
|
||||
Object.keys(currentRoles).length === 0 &&
|
||||
batchStatus?.hermesAgentRoles
|
||||
) {
|
||||
const seeded: Record<string, any> = {};
|
||||
Object.entries(batchStatus.hermesAgentRoles).forEach(([role, info]: [string, any]) => {
|
||||
seeded[role] = {
|
||||
@@ -252,10 +258,7 @@ export default function HermesAgentToolCard({
|
||||
return (
|
||||
<Card padding="sm" className="overflow-hidden">
|
||||
{/* Collapsed header — exact match to OpenClaw / Kilo / other Auto-Configured entries */}
|
||||
<div
|
||||
className="flex items-center justify-between hover:cursor-pointer"
|
||||
onClick={onToggle}
|
||||
>
|
||||
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-8 flex items-center justify-center shrink-0">
|
||||
<span className="material-symbols-outlined text-[22px] text-text-muted">terminal</span>
|
||||
@@ -274,8 +277,8 @@ export default function HermesAgentToolCard({
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
{(Object.keys(currentRoles).length > 0 ||
|
||||
Object.keys(selections).length > 0 ||
|
||||
{(Object.keys(currentRoles).length > 0 ||
|
||||
Object.keys(selections).length > 0 ||
|
||||
Object.keys(batchStatus?.hermesAgentRoles || {}).length > 0) && (
|
||||
<span className="text-[10px] px-1.5 py-px rounded bg-emerald-500/10 text-emerald-600">
|
||||
{configuredRolesCount}/{HERMES_ROLES.length} roles
|
||||
@@ -368,7 +371,9 @@ export default function HermesAgentToolCard({
|
||||
{/* Left: role label + subtitle (now has room so long descriptions stay on one line) */}
|
||||
<div className="min-w-0 pr-3">
|
||||
<div className="font-medium text-sm text-text-main">{role.label}</div>
|
||||
<div className="text-[10px] leading-tight text-text-muted">{role.description}</div>
|
||||
<div className="text-[10px] leading-tight text-text-muted">
|
||||
{role.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right cluster: model name + status badge + actions (pushed to the right) */}
|
||||
@@ -392,7 +397,8 @@ export default function HermesAgentToolCard({
|
||||
: "bg-emerald-500/10 text-emerald-600"
|
||||
}`}
|
||||
>
|
||||
{badge.label}{badge.pending ? " *" : ""}
|
||||
{badge.label}
|
||||
{badge.pending ? " *" : ""}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -453,7 +459,11 @@ export default function HermesAgentToolCard({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleTogglePreview}
|
||||
disabled={isSaving || isLoading || (Object.keys(selections).length === 0 && Object.keys(currentRoles).length === 0)}
|
||||
disabled={
|
||||
isSaving ||
|
||||
isLoading ||
|
||||
(Object.keys(selections).length === 0 && Object.keys(currentRoles).length === 0)
|
||||
}
|
||||
loading={isPreviewLoading}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">visibility</span>
|
||||
@@ -462,7 +472,8 @@ export default function HermesAgentToolCard({
|
||||
|
||||
{Object.keys(selections).length > 0 && (
|
||||
<span className="text-xs text-text-muted ml-1">
|
||||
{Object.keys(selections).length} role{Object.keys(selections).length === 1 ? "" : "s"} will be updated
|
||||
{Object.keys(selections).length} role
|
||||
{Object.keys(selections).length === 1 ? "" : "s"} will be updated
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -510,4 +521,3 @@ export default function HermesAgentToolCard({
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1740,12 +1740,24 @@ function ComboCard({
|
||||
className="text-xs py-1 px-2 rounded border border-black/10 dark:border-white/10 bg-surface text-text-main focus:border-primary focus:outline-none transition-colors disabled:opacity-50 max-w-[130px] md:max-w-none"
|
||||
title={t("compressionOverride")}
|
||||
>
|
||||
<option value="" className="bg-surface text-text-main">Default</option>
|
||||
<option value="off" className="bg-surface text-text-main">Off</option>
|
||||
<option value="lite" className="bg-surface text-text-main">Lite</option>
|
||||
<option value="standard" className="bg-surface text-text-main">Standard</option>
|
||||
<option value="aggressive" className="bg-surface text-text-main">Aggressive</option>
|
||||
<option value="ultra" className="bg-surface text-text-main">Ultra</option>
|
||||
<option value="" className="bg-surface text-text-main">
|
||||
Default
|
||||
</option>
|
||||
<option value="off" className="bg-surface text-text-main">
|
||||
Off
|
||||
</option>
|
||||
<option value="lite" className="bg-surface text-text-main">
|
||||
Lite
|
||||
</option>
|
||||
<option value="standard" className="bg-surface text-text-main">
|
||||
Standard
|
||||
</option>
|
||||
<option value="aggressive" className="bg-surface text-text-main">
|
||||
Aggressive
|
||||
</option>
|
||||
<option value="ultra" className="bg-surface text-text-main">
|
||||
Ultra
|
||||
</option>
|
||||
</select>
|
||||
)}
|
||||
<button
|
||||
|
||||
@@ -112,7 +112,9 @@ export default function ProviderQuotaWidget() {
|
||||
{/* Header with title + Refresh All in upper right */}
|
||||
<div className="flex items-center justify-between border-b border-border px-4 py-3 bg-surface/60">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-primary text-[20px]">account_balance</span>
|
||||
<span className="material-symbols-outlined text-primary text-[20px]">
|
||||
account_balance
|
||||
</span>
|
||||
<div>
|
||||
<h3 className="font-semibold text-base">{t("providerQuota") || "Provider Quota"}</h3>
|
||||
<p className="text-[11px] text-text-muted -mt-0.5">
|
||||
@@ -127,7 +129,9 @@ export default function ProviderQuotaWidget() {
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-border bg-bg-subtle text-xs font-medium text-text-main disabled:opacity-50 disabled:cursor-not-allowed hover:bg-surface transition-colors"
|
||||
title={t("refreshAll") || "Refresh All"}
|
||||
>
|
||||
<span className={`material-symbols-outlined text-[16px] ${refreshingAll ? "animate-spin" : ""}`}>
|
||||
<span
|
||||
className={`material-symbols-outlined text-[16px] ${refreshingAll ? "animate-spin" : ""}`}
|
||||
>
|
||||
refresh
|
||||
</span>
|
||||
<span>{t("refreshAll") || "Refresh All"}</span>
|
||||
@@ -144,7 +148,9 @@ export default function ProviderQuotaWidget() {
|
||||
) : providerEntries.length === 0 ? (
|
||||
<div className="text-center py-6 text-sm text-text-muted">
|
||||
No quota-supported providers connected yet.
|
||||
<div className="mt-1 text-xs">Add accounts on the Providers page to see quota status here.</div>
|
||||
<div className="mt-1 text-xs">
|
||||
Add accounts on the Providers page to see quota status here.
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
||||
|
||||
@@ -257,7 +257,8 @@ async function saveQwenConfig({ baseUrl, apiKey, model }) {
|
||||
* keys, but make sure the OmniRoute provider entry is present and selected.
|
||||
*/
|
||||
async function saveHermesConfig({ baseUrl, apiKey, model }) {
|
||||
const configPath = getCliPrimaryConfigPath("hermes") || path.join(os.homedir(), ".hermes", "config.yaml");
|
||||
const configPath =
|
||||
getCliPrimaryConfigPath("hermes") || path.join(os.homedir(), ".hermes", "config.yaml");
|
||||
const configDir = path.dirname(configPath);
|
||||
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
@@ -265,7 +266,9 @@ async function saveHermesConfig({ baseUrl, apiKey, model }) {
|
||||
const normalizedBaseUrl = String(baseUrl || "")
|
||||
.trim()
|
||||
.replace(/\/+$/, "");
|
||||
const providerBaseUrl = normalizedBaseUrl.endsWith("/v1") ? normalizedBaseUrl : `${normalizedBaseUrl}/v1`;
|
||||
const providerBaseUrl = normalizedBaseUrl.endsWith("/v1")
|
||||
? normalizedBaseUrl
|
||||
: `${normalizedBaseUrl}/v1`;
|
||||
|
||||
if (!model) {
|
||||
return NextResponse.json({ error: "model is required for Hermes" }, { status: 400 });
|
||||
@@ -296,7 +299,12 @@ async function saveHermesConfig({ baseUrl, apiKey, model }) {
|
||||
omniroute: {
|
||||
...((existingConfig.providers && existingConfig.providers.omniroute) || {}),
|
||||
base_url: providerBaseUrl,
|
||||
api_key: apiKey || ((existingConfig.providers && existingConfig.providers.omniroute && existingConfig.providers.omniroute.api_key) || ""),
|
||||
api_key:
|
||||
apiKey ||
|
||||
(existingConfig.providers &&
|
||||
existingConfig.providers.omniroute &&
|
||||
existingConfig.providers.omniroute.api_key) ||
|
||||
"",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -309,5 +317,3 @@ async function saveHermesConfig({ baseUrl, apiKey, model }) {
|
||||
configPath,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,10 @@ import path from "path";
|
||||
import os from "os";
|
||||
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
|
||||
import { getCliPrimaryConfigPath } from "@/shared/services/cliRuntime";
|
||||
import { generateHermesAgentConfig, getCurrentHermesAgentRoles } from "@/lib/cli-helper/config-generator/hermes-agent";
|
||||
import {
|
||||
generateHermesAgentConfig,
|
||||
getCurrentHermesAgentRoles,
|
||||
} from "@/lib/cli-helper/config-generator/hermes-agent";
|
||||
|
||||
/**
|
||||
* Dedicated endpoint for Hermes Agent (the advanced Nous Research terminal agent).
|
||||
|
||||
@@ -151,7 +151,16 @@ export async function GET(request: Request) {
|
||||
);
|
||||
|
||||
// Check config status for installed+runnable tools via direct file reads
|
||||
const settingsTools = ["claude", "codex", "droid", "openclaw", "cline", "kilo", "qwen", "hermes"];
|
||||
const settingsTools = [
|
||||
"claude",
|
||||
"codex",
|
||||
"droid",
|
||||
"openclaw",
|
||||
"cline",
|
||||
"kilo",
|
||||
"qwen",
|
||||
"hermes",
|
||||
];
|
||||
|
||||
await Promise.all(
|
||||
settingsTools.map(async (toolId) => {
|
||||
|
||||
@@ -31,16 +31,20 @@ import os from "node:os";
|
||||
import * as yaml from "js-yaml";
|
||||
|
||||
export const HERMES_AGENT_ROLES = [
|
||||
{ id: "default", label: "Default (main)", description: "Primary conversation model" },
|
||||
{ id: "delegation", label: "Delegation (subagents)", description: "Orchestrator and sub-agent spawning model" },
|
||||
{ id: "vision", label: "Vision", description: "Image and screenshot understanding" },
|
||||
{ id: "compression", label: "Compression", description: "Prompt compression and summarization" },
|
||||
{ id: "web_extract", label: "Web Extract", description: "Web page / content extraction" },
|
||||
{ id: "skills_hub", label: "Skills Hub", description: "Skills and tool-use reasoning" },
|
||||
{ id: "approval", label: "Approval", description: "Safety and approval decisions" },
|
||||
{ id: "default", label: "Default (main)", description: "Primary conversation model" },
|
||||
{
|
||||
id: "delegation",
|
||||
label: "Delegation (subagents)",
|
||||
description: "Orchestrator and sub-agent spawning model",
|
||||
},
|
||||
{ id: "vision", label: "Vision", description: "Image and screenshot understanding" },
|
||||
{ id: "compression", label: "Compression", description: "Prompt compression and summarization" },
|
||||
{ id: "web_extract", label: "Web Extract", description: "Web page / content extraction" },
|
||||
{ id: "skills_hub", label: "Skills Hub", description: "Skills and tool-use reasoning" },
|
||||
{ id: "approval", label: "Approval", description: "Safety and approval decisions" },
|
||||
] as const;
|
||||
|
||||
export type HermesAgentRole = typeof HERMES_AGENT_ROLES[number]["id"];
|
||||
export type HermesAgentRole = (typeof HERMES_AGENT_ROLES)[number]["id"];
|
||||
|
||||
export interface HermesAgentRoleSelection {
|
||||
role: HermesAgentRole;
|
||||
|
||||
@@ -48,8 +48,6 @@ const TOOL_CONFIG_PATHS: Record<string, string> = {
|
||||
"hermes-agent": path.join(os.homedir(), ".hermes", "config.yaml"),
|
||||
};
|
||||
|
||||
|
||||
|
||||
type ConfigGenerator = (options: GenerateOptions) => string | Promise<string>;
|
||||
|
||||
const GENERATORS: Record<string, ConfigGenerator> = {
|
||||
@@ -94,7 +92,15 @@ export async function generateConfig(
|
||||
}
|
||||
|
||||
export async function generateAllConfigs(options: GenerateOptions): Promise<GenerateResult[]> {
|
||||
const toolIds = ["claude", "codex", "opencode", "cline", "kilocode", "continue", "hermes"] as const;
|
||||
const toolIds = [
|
||||
"claude",
|
||||
"codex",
|
||||
"opencode",
|
||||
"cline",
|
||||
"kilocode",
|
||||
"continue",
|
||||
"hermes",
|
||||
] as const;
|
||||
const results = await Promise.allSettled(toolIds.map((id) => generateConfig(id, options)));
|
||||
|
||||
return results.map((r) =>
|
||||
|
||||
@@ -21,11 +21,14 @@ export interface DetectedTool {
|
||||
configContents?: string;
|
||||
|
||||
// Rich per-role status for Hermes Agent
|
||||
hermesAgentRoles?: Record<string, {
|
||||
model: string;
|
||||
provider?: string;
|
||||
usingOmniRoute: boolean;
|
||||
}>;
|
||||
hermesAgentRoles?: Record<
|
||||
string,
|
||||
{
|
||||
model: string;
|
||||
provider?: string;
|
||||
usingOmniRoute: boolean;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
const TOOLS = [
|
||||
@@ -119,7 +122,8 @@ export async function detectTool(id: string): Promise<DetectedTool | null> {
|
||||
const richRoles: Record<string, any> = {};
|
||||
|
||||
Object.entries(roles).forEach(([role, info]) => {
|
||||
const usingOmni = info?.provider === "omniroute" ||
|
||||
const usingOmni =
|
||||
info?.provider === "omniroute" ||
|
||||
(info?.base_url || "").includes("20128") ||
|
||||
(info?.base_url || "").includes("localhost:20128");
|
||||
|
||||
|
||||
@@ -85,7 +85,8 @@ describe("config-generator", () => {
|
||||
|
||||
describe("hermes-agent (rich multi-role)", () => {
|
||||
it("exports HERMES_AGENT_ROLES with expected roles", async () => {
|
||||
const hermesAgent = await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts");
|
||||
const hermesAgent =
|
||||
await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts");
|
||||
assert.ok(Array.isArray(hermesAgent.HERMES_AGENT_ROLES));
|
||||
const ids = hermesAgent.HERMES_AGENT_ROLES.map((r: any) => r.id);
|
||||
assert.ok(ids.includes("default"));
|
||||
@@ -95,13 +96,15 @@ describe("config-generator", () => {
|
||||
});
|
||||
|
||||
it("getCurrentHermesAgentRoles returns an object", async () => {
|
||||
const hermesAgent = await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts");
|
||||
const hermesAgent =
|
||||
await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts");
|
||||
const roles = await hermesAgent.getCurrentHermesAgentRoles();
|
||||
assert.ok(typeof roles === "object" && roles !== null);
|
||||
});
|
||||
|
||||
it("generateHermesAgentConfig returns yaml string for valid payload", async () => {
|
||||
const hermesAgent = await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts");
|
||||
const hermesAgent =
|
||||
await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts");
|
||||
const result = await hermesAgent.generateHermesAgentConfig({
|
||||
baseUrl: "http://localhost:20128",
|
||||
apiKey: "sk-test-omniroute",
|
||||
@@ -119,7 +122,8 @@ describe("config-generator", () => {
|
||||
});
|
||||
|
||||
it("generateHermesAgentConfig includes auxiliary section for non-default roles", async () => {
|
||||
const hermesAgent = await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts");
|
||||
const hermesAgent =
|
||||
await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts");
|
||||
const result = await hermesAgent.generateHermesAgentConfig({
|
||||
baseUrl: "http://localhost:20128",
|
||||
apiKey: "sk-test",
|
||||
@@ -134,7 +138,8 @@ describe("config-generator", () => {
|
||||
});
|
||||
|
||||
it("generateHermesAgentConfig returns error when baseUrl is missing", async () => {
|
||||
const hermesAgent = await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts");
|
||||
const hermesAgent =
|
||||
await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts");
|
||||
const result = await hermesAgent.generateHermesAgentConfig({
|
||||
baseUrl: "",
|
||||
selections: [{ role: "default", model: "x" }],
|
||||
@@ -145,7 +150,8 @@ describe("config-generator", () => {
|
||||
});
|
||||
|
||||
it("generateHermesAgentConfig correctly structures delegation and auxiliary roles", async () => {
|
||||
const hermesAgent = await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts");
|
||||
const hermesAgent =
|
||||
await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts");
|
||||
const result = await hermesAgent.generateHermesAgentConfig({
|
||||
baseUrl: "http://localhost:20128",
|
||||
apiKey: "sk-test",
|
||||
@@ -166,7 +172,8 @@ describe("config-generator", () => {
|
||||
|
||||
it("generateHermesAgentConfig performs non-destructive merge (preserves other keys)", async () => {
|
||||
// This test mainly verifies the function doesn't blow away unrelated config
|
||||
const hermesAgent = await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts");
|
||||
const hermesAgent =
|
||||
await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts");
|
||||
const result = await hermesAgent.generateHermesAgentConfig({
|
||||
baseUrl: "http://localhost:20128",
|
||||
apiKey: "sk-test",
|
||||
|
||||
@@ -47,7 +47,6 @@ describe("tool-detector", () => {
|
||||
assert.ok(result!.configPath.includes(".hermes/config.yaml"));
|
||||
assert.strictEqual(typeof result!.configured, "boolean");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("detectAllTools", () => {
|
||||
|
||||
@@ -95,7 +95,9 @@ test("guide-settings POST creates new hermes config.yaml if it doesn't exist", a
|
||||
apiKey: "sk-hermes",
|
||||
model: "gpt-5.4-mini",
|
||||
});
|
||||
const response = (await guideSettingsRoute.POST(req, { params: { toolId: "hermes" } })) as Response;
|
||||
const response = (await guideSettingsRoute.POST(req, {
|
||||
params: { toolId: "hermes" },
|
||||
})) as Response;
|
||||
const data = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 200, "Response should be OK");
|
||||
@@ -109,7 +111,6 @@ test("guide-settings POST creates new hermes config.yaml if it doesn't exist", a
|
||||
assert.ok(String(content.providers?.omniroute?.api_key || "").startsWith("sk-"));
|
||||
});
|
||||
|
||||
|
||||
test("guide-settings POST merges into existing qwen settings.json", async () => {
|
||||
await fs.mkdir(path.dirname(QWEN_CONFIG_PATH), { recursive: true });
|
||||
await fs.writeFile(
|
||||
|
||||
119
tests/unit/system-role-extraction.test.ts
Normal file
119
tests/unit/system-role-extraction.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { extractSystemRoleMessages } from "../../open-sse/handlers/chatCore.ts";
|
||||
|
||||
test("extractSystemRoleMessages moves role=system to top-level system", () => {
|
||||
const payload = {
|
||||
messages: [
|
||||
{ role: "system", content: "Memory context: foo" },
|
||||
{ role: "user", content: "hello" },
|
||||
{ role: "assistant", content: "hi" },
|
||||
],
|
||||
};
|
||||
extractSystemRoleMessages(payload);
|
||||
assert.equal(payload.messages.length, 2);
|
||||
assert.equal(payload.messages[0].role, "user");
|
||||
assert.deepEqual(payload.system, [{ type: "text", text: "Memory context: foo" }]);
|
||||
});
|
||||
|
||||
test("extractSystemRoleMessages merges with existing top-level system string", () => {
|
||||
const payload = {
|
||||
system: "You are Claude.",
|
||||
messages: [
|
||||
{ role: "system", content: "Memory context: bar" },
|
||||
{ role: "user", content: "hello" },
|
||||
],
|
||||
};
|
||||
extractSystemRoleMessages(payload);
|
||||
assert.equal(payload.messages.length, 1);
|
||||
assert.deepEqual(payload.system, [
|
||||
{ type: "text", text: "You are Claude." },
|
||||
{ type: "text", text: "Memory context: bar" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("extractSystemRoleMessages merges with existing top-level system array", () => {
|
||||
const payload = {
|
||||
system: [{ type: "text", text: "Existing system" }],
|
||||
messages: [
|
||||
{ role: "system", content: "Memory context: baz" },
|
||||
{ role: "user", content: "hello" },
|
||||
],
|
||||
};
|
||||
extractSystemRoleMessages(payload);
|
||||
assert.equal(payload.messages.length, 1);
|
||||
assert.deepEqual(payload.system, [
|
||||
{ type: "text", text: "Existing system" },
|
||||
{ type: "text", text: "Memory context: baz" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("extractSystemRoleMessages does nothing when no system role messages", () => {
|
||||
const payload = {
|
||||
messages: [
|
||||
{ role: "user", content: "hello" },
|
||||
{ role: "assistant", content: "hi" },
|
||||
],
|
||||
};
|
||||
extractSystemRoleMessages(payload);
|
||||
assert.equal(payload.messages.length, 2);
|
||||
assert.equal(payload.system, undefined);
|
||||
});
|
||||
|
||||
test("extractSystemRoleMessages handles non-array messages gracefully", () => {
|
||||
const payload = { messages: "not-an-array" };
|
||||
extractSystemRoleMessages(payload);
|
||||
assert.equal(payload.messages, "not-an-array");
|
||||
});
|
||||
|
||||
test("extractSystemRoleMessages handles empty messages array", () => {
|
||||
const payload = { messages: [] };
|
||||
extractSystemRoleMessages(payload);
|
||||
assert.equal(payload.messages.length, 0);
|
||||
});
|
||||
|
||||
test("extractSystemRoleMessages handles case-insensitive role System", () => {
|
||||
const payload = {
|
||||
messages: [
|
||||
{ role: "System", content: "Memory context: caps" },
|
||||
{ role: "user", content: "hello" },
|
||||
],
|
||||
};
|
||||
extractSystemRoleMessages(payload);
|
||||
assert.equal(payload.messages.length, 1);
|
||||
assert.deepEqual(payload.system, [{ type: "text", text: "Memory context: caps" }]);
|
||||
});
|
||||
|
||||
test("extractSystemRoleMessages drops empty text content from system messages", () => {
|
||||
const payload = {
|
||||
messages: [
|
||||
{ role: "system", content: "" },
|
||||
{ role: "system", content: "valid" },
|
||||
{ role: "user", content: "hello" },
|
||||
],
|
||||
};
|
||||
extractSystemRoleMessages(payload);
|
||||
assert.equal(payload.messages.length, 1);
|
||||
assert.deepEqual(payload.system, [{ type: "text", text: "valid" }]);
|
||||
});
|
||||
|
||||
test("extractSystemRoleMessages handles system messages with array content", () => {
|
||||
const payload = {
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: [
|
||||
{ type: "text", text: "Block 1" },
|
||||
{ type: "text", text: "Block 2" },
|
||||
],
|
||||
},
|
||||
{ role: "user", content: "hello" },
|
||||
],
|
||||
};
|
||||
extractSystemRoleMessages(payload);
|
||||
assert.equal(payload.messages.length, 1);
|
||||
assert.deepEqual(payload.system, [
|
||||
{ type: "text", text: "Block 1" },
|
||||
{ type: "text", text: "Block 2" },
|
||||
]);
|
||||
});
|
||||
@@ -84,45 +84,27 @@ test("#2232 — whitespace-only VISION_BRIDGE_BASE_URL falls through to OPENAI_A
|
||||
test("#2232 — OmniRoute-internal providers default to self-loop when no env vars set", () => {
|
||||
clearEnv();
|
||||
// Non-standard prefixes (kr/, if/, pol/, groq/) should use OmniRoute self-loop
|
||||
assert.equal(
|
||||
resolveVisionBridgeBaseUrl("kr/claude-sonnet-4-5"),
|
||||
"http://localhost:20128/v1"
|
||||
);
|
||||
assert.equal(
|
||||
resolveVisionBridgeBaseUrl("if/kimi-k2-thinking"),
|
||||
"http://localhost:20128/v1"
|
||||
);
|
||||
assert.equal(
|
||||
resolveVisionBridgeBaseUrl("pol/gpt-5"),
|
||||
"http://localhost:20128/v1"
|
||||
);
|
||||
assert.equal(resolveVisionBridgeBaseUrl("kr/claude-sonnet-4-5"), "http://localhost:20128/v1");
|
||||
assert.equal(resolveVisionBridgeBaseUrl("if/kimi-k2-thinking"), "http://localhost:20128/v1");
|
||||
assert.equal(resolveVisionBridgeBaseUrl("pol/gpt-5"), "http://localhost:20128/v1");
|
||||
});
|
||||
|
||||
test("#2232 — OpenAI and Anthropic models still default to api.openai.com", () => {
|
||||
clearEnv();
|
||||
// Standard prefixes should keep default behavior
|
||||
assert.equal(
|
||||
resolveVisionBridgeBaseUrl("openai/gpt-4o"),
|
||||
"https://api.openai.com/v1"
|
||||
);
|
||||
assert.equal(resolveVisionBridgeBaseUrl("openai/gpt-4o"), "https://api.openai.com/v1");
|
||||
assert.equal(
|
||||
resolveVisionBridgeBaseUrl("anthropic/claude-sonnet-4-5"),
|
||||
"https://api.openai.com/v1" // anthropic goes through a different code path
|
||||
// but if passed here, should not self-loop
|
||||
"https://api.openai.com/v1" // anthropic goes through a different code path
|
||||
// but if passed here, should not self-loop
|
||||
);
|
||||
});
|
||||
|
||||
test("#2232 — unprefixed model names default to api.openai.com", () => {
|
||||
clearEnv();
|
||||
// Models without provider prefix should keep default behavior
|
||||
assert.equal(
|
||||
resolveVisionBridgeBaseUrl("gpt-4o-mini"),
|
||||
"https://api.openai.com/v1"
|
||||
);
|
||||
assert.equal(
|
||||
resolveVisionBridgeBaseUrl("deepseek-v4-flash"),
|
||||
"https://api.openai.com/v1"
|
||||
);
|
||||
assert.equal(resolveVisionBridgeBaseUrl("gpt-4o-mini"), "https://api.openai.com/v1");
|
||||
assert.equal(resolveVisionBridgeBaseUrl("deepseek-v4-flash"), "https://api.openai.com/v1");
|
||||
});
|
||||
|
||||
test("#2232 — VISION_BRIDGE_BASE_URL env var takes precedence over self-loop auto-detection", () => {
|
||||
|
||||
Reference in New Issue
Block a user