mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-09 00:32:13 +03:00
Compare commits
2 Commits
feat/9544-
...
fix/escala
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69647b3b94 | ||
|
|
36abd86929 |
@@ -1 +0,0 @@
|
||||
- feat(providers): add Muse Code CLI provider preset (#9544)
|
||||
1
changelog.d/maintenance/9738-deadcode-radar-referrals.md
Normal file
1
changelog.d/maintenance/9738-deadcode-radar-referrals.md
Normal file
@@ -0,0 +1 @@
|
||||
- Removed the unused `RadarReferrals` type export left by the radar referral-links feature (#9697), returning the dead-code ratchet to its 227 baseline.
|
||||
@@ -96,6 +96,7 @@
|
||||
"node-machine-id",
|
||||
"omniglyph",
|
||||
"open",
|
||||
"opencode-ai",
|
||||
"ora",
|
||||
"parse5",
|
||||
"pino",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"_rebaseline_2026_08_08_v3850_base_drift_batch_9757": "Base drift on release/v3.8.50, not own growth: the 08-06..08-08 merge batches grew 12 already-frozen (or newly-landed) files without carrying their rebaselines — the dedicated rebaseline PR #9616 was closed as 'superseded' but its file-size entries never actually reached the base, and later merges (#8894 combos page, #9539 EditConnectionModal, #8895 models route, #9294/#9293 catalog, #9541 db/core, #8970 tokenHealthCheck, #8925 mcp schemas+server, #8890 accountFallback, #9467 chat.ts, #8931 openai-to-kiro, ProxyRegistryManager) kept growing them. All 12 values re-measured on THIS branch's tree (= pure tip + this PR's 1-line chat.ts fix, which adds zero lines). This PR's own source changes (chat.ts identifier restore, stream.ts format carve-out) do not grow any frozen file past these values.",
|
||||
"_rebaseline_2026_08_08_migration_135_collision": "fix(db): resolve migration version 135 numbering collision — #9449's 135_connection_runtime_state.sql and #8908's 135_migrate_model_capability_max_token.sql both claimed version 135 (#9449 branched before #8908 merged and never got renumbered before landing on release/v3.8.50), which threw 'Migration version collision detected' the moment ANY code touched the database — a fresh install/deploy from this tip cannot even boot. Renumbered the later-landing file to 140 (next free slot) and added the matching isSchemaAlreadyApplied('140') retroactive guard, matching the established pattern already used for the prior 135/136 -> 137/138 renumber in the same file. Own growth: src/lib/db/migrationRunner.ts 1084->1094 (+10, the new case block) — irreducible, matches the existing per-case guard pattern exactly. Covered by tests/unit/migration-135-numbering-collision.test.ts (2/2), confirmed failing (reproducing the exact live crash) against the pre-fix colliding filenames, passing after.",
|
||||
"_rebaseline_2026_08_02_9259_rolling_rpm": "PR #9259 (issue #8733) own growth: open-sse/services/rateLimitManager.ts baseline 1060->1167 (+107; final source 1153). The existing withRateLimit chokepoint now composes process-local rolling RPM leases with Bottleneck admission, releases pre-dispatch leases on queue timeout/abort/connection disable, preserves caller abort reasons, and wires 429/header state into the extracted rollingRpmGate.ts. The remaining growth is irreducible lifecycle wiring at the dispatch boundary plus the real watchdog test hooks needed to verify queued-wedge recovery; moving it further would obscure lease ownership and Bottleneck cleanup. Covered by the focused rate-limit manager/sliding-window suite (33/33); distributed multi-instance coordination remains explicitly out of scope.",
|
||||
"_rebaseline_2026_07_24_8470_hyperagent_sticky_thread": "PR #8470 (artickc, fix/hyperagent-tool-loop-thread-sticky) own growth: open-sse/executors/hyperagent.ts 936->1025 (wc -l; check-file-size.mjs counts via split(\"\\n\").length so the gate sees 937->1026, +89, crosses the 1000 cap). Fixes a real bug where a reverse-conversion proxy (text-Intent/JSON to Claude Code native tool_calls) rewrites assistant messages between agentic tool-loop turns, breaking HyperAgent’s conversation-prefix fingerprint and cold-starting the thread mid tool-loop. Adds Anthropic tool_use/tool_result flattening to extractMessageText() plus a new rootUserFingerprint()/root-key lookup tier in resolveHyperAgentThreadBinding()/storeHyperAgentThreadAfterTurn() so the thread stays sticky across the tool loop. Cohesive additions inside the existing single-file executor; not extractable without splitting the executor mid-request-flow. Covered by tests/unit/executor-hyperagent.test.ts (19/19, +5 new cases for tool_result/tool_use flattening + root-key stickiness). Pre-merge review flagged a cross-conversation root-key collision risk (tracked in the PR’s own mandatory pre-merge checklist, not yet addressed) — unrelated to this file-size ratchet, tracked separately by /fix-prs.",
|
||||
@@ -232,10 +233,10 @@
|
||||
"open-sse/handlers/responseSanitizer.ts": 1128,
|
||||
"open-sse/handlers/search.ts": 1536,
|
||||
"open-sse/handlers/videoGeneration.ts": 1063,
|
||||
"open-sse/mcp-server/schemas/tools.ts": 1505,
|
||||
"open-sse/mcp-server/server.ts": 1411,
|
||||
"open-sse/mcp-server/schemas/tools.ts": 1553,
|
||||
"open-sse/mcp-server/server.ts": 1448,
|
||||
"open-sse/mcp-server/tools/advancedTools.ts": 1120,
|
||||
"open-sse/services/accountFallback.ts": 1972,
|
||||
"open-sse/services/accountFallback.ts": 1978,
|
||||
"open-sse/services/adobeFireflyClient.ts": 2385,
|
||||
"open-sse/services/claudeCodeCompatible.ts": 1202,
|
||||
"open-sse/services/combo.ts": 3648,
|
||||
@@ -248,27 +249,27 @@
|
||||
"src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx": 1031,
|
||||
"src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx": 3117,
|
||||
"src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx": 1067,
|
||||
"src/app/(dashboard)/dashboard/combos/page.tsx": 4647,
|
||||
"src/app/(dashboard)/dashboard/combos/page.tsx": 4703,
|
||||
"src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx": 1283,
|
||||
"src/app/(dashboard)/dashboard/costs/quota-share/components/PoolWizard.tsx": 1022,
|
||||
"src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": 2615,
|
||||
"src/app/(dashboard)/dashboard/health/page.tsx": 1165,
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": 1316,
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": 1324,
|
||||
"src/app/(dashboard)/dashboard/providers/page.tsx": 1944,
|
||||
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1201,
|
||||
"src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": 1019,
|
||||
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1464,
|
||||
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1470,
|
||||
"src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": 1123,
|
||||
"src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1629,
|
||||
"src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1573,
|
||||
"src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx": 1028,
|
||||
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2148,
|
||||
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1119,
|
||||
"src/app/api/providers/[id]/models/route.ts": 2250,
|
||||
"src/app/api/v1/models/catalog.ts": 1549,
|
||||
"src/lib/tokenHealthCheck.ts": 1021,
|
||||
"src/app/api/providers/[id]/models/route.ts": 2361,
|
||||
"src/app/api/v1/models/catalog.ts": 1590,
|
||||
"src/lib/tokenHealthCheck.ts": 1053,
|
||||
"src/lib/db/apiKeys.ts": 1529,
|
||||
"src/lib/db/core.ts": 1637,
|
||||
"src/lib/db/core.ts": 1639,
|
||||
"src/lib/db/migrationRunner.ts": 1094,
|
||||
"src/lib/db/models.ts": 1097,
|
||||
"src/lib/db/providers.ts": 1034,
|
||||
@@ -279,13 +280,14 @@
|
||||
"src/shared/components/RequestLoggerV2.tsx": 1629,
|
||||
"src/shared/components/analytics/charts.tsx": 1035,
|
||||
"src/shared/services/cliRuntime.ts": 1122,
|
||||
"src/sse/handlers/chat.ts": 1877,
|
||||
"src/sse/handlers/chat.ts": 1904,
|
||||
"src/sse/services/auth.ts": 2508,
|
||||
"tests/unit/account-fallback-service.test.ts": 1572,
|
||||
"tests/unit/provider-validation-specialty.test.ts": 2985,
|
||||
"open-sse/executors/hyperagent.ts": 1026,
|
||||
"open-sse/executors/default.ts": 1042,
|
||||
"open-sse/executors/kiro.ts": 1069
|
||||
"open-sse/executors/kiro.ts": 1069,
|
||||
"open-sse/translator/request/openai-to-kiro.ts": 1057
|
||||
},
|
||||
"testCap": 1000,
|
||||
"testFrozen": {
|
||||
|
||||
@@ -158,7 +158,7 @@ error lines produced **95.93% token savings / 96.26% character savings** — squ
|
||||
range. But the same pipeline run against normal, non-redundant tool output (a clean `grep` match list,
|
||||
a short file read, ordinary conversational text) correctly produces **near-zero savings**, because
|
||||
there is nothing repetitive to remove and `validateCompression()` (`validation.ts`) refuses to ship a
|
||||
rewrite that would drop or alter code blocks, URLs, headings, versions, or `CONST_CASE` identifiers.
|
||||
rewrite that would drop or alter code blocks, URLs, headings, versions, or ALL-CAPS constant identifiers.
|
||||
|
||||
This is expected, safe behavior, not a bug: a coding session that mostly reads/greps clean files will
|
||||
see modest total savings even with compression fully enabled, while a session that hits a failing
|
||||
|
||||
@@ -225,7 +225,6 @@ import { digitaloceanProvider } from "./registry/digitalocean/index.ts";
|
||||
import { hcnsecProvider } from "./registry/hcnsec/index.ts";
|
||||
import { promptqlProvider } from "./registry/promptql/index.ts";
|
||||
import { hyperagentProvider } from "./registry/hyperagent/index.ts";
|
||||
import { muse_codeProvider } from "./registry/muse-code/index.ts";
|
||||
|
||||
export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
aimlapi: aimlapiProvider,
|
||||
@@ -452,6 +451,5 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
hcnsec: hcnsecProvider,
|
||||
promptql: promptqlProvider,
|
||||
hyperagent: hyperagentProvider,
|
||||
"muse-code": muse_codeProvider,
|
||||
unorouter: unorouterProvider,
|
||||
};
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
/**
|
||||
* Muse Code CLI — Meta's agentic coding tool.
|
||||
*
|
||||
* Wire format: OpenAI Responses API (POST /responses).
|
||||
* Auth: Bearer token from META_API_KEY env var.
|
||||
* Reasoning efforts: xhigh/ultra -> high (handled generically).
|
||||
*
|
||||
* @see https://github.com/joymadhu49/muse-openrouter-shim
|
||||
*/
|
||||
export const muse_codeProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "muse-code",
|
||||
alias: "mc",
|
||||
passthroughModels: true,
|
||||
defaultContextLength: 200000,
|
||||
models: [
|
||||
{
|
||||
id: "llama-4-maverick",
|
||||
name: "Llama 4 Maverick",
|
||||
contextLength: 1048576,
|
||||
maxOutputTokens: 131072,
|
||||
supportsReasoning: true,
|
||||
supportsXHighEffort: true,
|
||||
toolCalling: true,
|
||||
supportsVision: true,
|
||||
targetFormat: "openai-responses",
|
||||
unsupportedParams: ["logprobs", "topLogprobs", "logitBias"],
|
||||
},
|
||||
{
|
||||
id: "llama-4-scout",
|
||||
name: "Llama 4 Scout",
|
||||
contextLength: 1048576,
|
||||
maxOutputTokens: 131072,
|
||||
supportsReasoning: true,
|
||||
supportsXHighEffort: true,
|
||||
toolCalling: true,
|
||||
supportsVision: true,
|
||||
targetFormat: "openai-responses",
|
||||
unsupportedParams: ["logprobs", "topLogprobs", "logitBias"],
|
||||
},
|
||||
{
|
||||
id: "llama-3.3-70b",
|
||||
name: "Llama 3.3 70B",
|
||||
contextLength: 131072,
|
||||
maxOutputTokens: 32768,
|
||||
supportsReasoning: false,
|
||||
toolCalling: true,
|
||||
targetFormat: "openai-responses",
|
||||
unsupportedParams: ["logprobs", "topLogprobs"],
|
||||
},
|
||||
{
|
||||
id: "llama-3.1-405b",
|
||||
name: "Llama 3.1 405B",
|
||||
contextLength: 131072,
|
||||
maxOutputTokens: 32768,
|
||||
supportsReasoning: false,
|
||||
toolCalling: true,
|
||||
targetFormat: "openai-responses",
|
||||
unsupportedParams: ["logprobs", "topLogprobs"],
|
||||
},
|
||||
{
|
||||
id: "llama-3.1-70b",
|
||||
name: "Llama 3.1 70B",
|
||||
contextLength: 131072,
|
||||
maxOutputTokens: 32768,
|
||||
supportsReasoning: false,
|
||||
toolCalling: true,
|
||||
targetFormat: "openai-responses",
|
||||
unsupportedParams: ["logprobs", "topLogprobs"],
|
||||
},
|
||||
{
|
||||
id: "llama-3.1-8b",
|
||||
name: "Llama 3.1 8B",
|
||||
contextLength: 131072,
|
||||
maxOutputTokens: 32768,
|
||||
supportsReasoning: false,
|
||||
toolCalling: true,
|
||||
targetFormat: "openai-responses",
|
||||
unsupportedParams: ["logprobs", "topLogprobs"],
|
||||
},
|
||||
{
|
||||
id: "llama-3.2-90b-vision",
|
||||
name: "Llama 3.2 90B Vision",
|
||||
contextLength: 131072,
|
||||
maxOutputTokens: 32768,
|
||||
supportsReasoning: false,
|
||||
toolCalling: true,
|
||||
supportsVision: true,
|
||||
targetFormat: "openai-responses",
|
||||
unsupportedParams: ["logprobs", "topLogprobs"],
|
||||
},
|
||||
{
|
||||
id: "llama-3.2-11b-vision",
|
||||
name: "Llama 3.2 11B Vision",
|
||||
contextLength: 131072,
|
||||
maxOutputTokens: 32768,
|
||||
supportsReasoning: false,
|
||||
toolCalling: true,
|
||||
supportsVision: true,
|
||||
targetFormat: "openai-responses",
|
||||
unsupportedParams: ["logprobs", "topLogprobs"],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -119,7 +119,8 @@ function serializeMessage(
|
||||
"unsupported_role"
|
||||
);
|
||||
}
|
||||
const label = role === "assistant" ? "Assistant" : role === "system" ? "System" : "User";
|
||||
// role was just narrowed to "user" | "assistant" by the guard above ("system" throws).
|
||||
const label = role === "assistant" ? "Assistant" : "User";
|
||||
const content = record.content;
|
||||
|
||||
if (typeof content === "string") return `[${label}]\n${content}`;
|
||||
|
||||
@@ -28,7 +28,14 @@ export class RaycastExecutor extends BaseExecutor {
|
||||
return RAYCAST_CHAT_URL;
|
||||
}
|
||||
|
||||
buildHeaders(credentials: ProviderCredentials, payload?: string): Record<string, string> {
|
||||
// Not a BaseExecutor.buildHeaders override: Raycast signs headers over the exact
|
||||
// request payload (2nd param is the body string, not the base's `stream` boolean),
|
||||
// and execute() below is fully custom — keep it as a distinct helper so a
|
||||
// polymorphic buildHeaders(credentials, true) call can never land here.
|
||||
private buildRaycastRequestHeaders(
|
||||
credentials: ProviderCredentials,
|
||||
payload?: string
|
||||
): Record<string, string> {
|
||||
const body = payload || "{}";
|
||||
return buildRaycastHeaders(body, credentials as JsonRecord);
|
||||
}
|
||||
@@ -44,7 +51,11 @@ export class RaycastExecutor extends BaseExecutor {
|
||||
return {
|
||||
response: new Response(
|
||||
JSON.stringify({
|
||||
error: { message: sanitizeErrorMessage(message), type: "invalid_request_error", code: "" },
|
||||
error: {
|
||||
message: sanitizeErrorMessage(message),
|
||||
type: "invalid_request_error",
|
||||
code: "",
|
||||
},
|
||||
}),
|
||||
{ status: 400, headers: { "Content-Type": "application/json" } }
|
||||
),
|
||||
@@ -54,7 +65,7 @@ export class RaycastExecutor extends BaseExecutor {
|
||||
};
|
||||
}
|
||||
|
||||
const headers = this.buildHeaders(credentials as ProviderCredentials, payload);
|
||||
const headers = this.buildRaycastRequestHeaders(credentials as ProviderCredentials, payload);
|
||||
mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders as Record<string, string> | null);
|
||||
|
||||
let raycastResponse: Response;
|
||||
|
||||
@@ -190,10 +190,26 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an `error` field carries a real failure signal. A key-presence check
|
||||
* (`!= null`) false-positives on benign values some backends emit on every
|
||||
* chunk (`{}`, `""`, `false`, `0`) — e.g. tool-call turns where a chunk with
|
||||
* real tool_calls content also carries `"error": {}`. Only substantive values
|
||||
* are treated as upstream failures.
|
||||
*/
|
||||
function isSubstantiveError(value: unknown): boolean {
|
||||
if (value === null || value === undefined) return false;
|
||||
if (typeof value === "string") return value.trim().length > 0;
|
||||
if (typeof value === "object" && !Array.isArray(value)) {
|
||||
return Object.keys(value as Record<string, unknown>).length > 0;
|
||||
}
|
||||
return value === true;
|
||||
}
|
||||
|
||||
function isStreamingUpstreamError(parsed: unknown, eventType: string): boolean {
|
||||
if (eventType === "response.failed" || eventType === "error") return true;
|
||||
if (!isRecord(parsed)) return false;
|
||||
if (parsed.error != null) return true;
|
||||
if (isSubstantiveError(parsed.error)) return true;
|
||||
|
||||
const nestedResponse = isRecord(parsed.response) ? parsed.response : null;
|
||||
return nestedResponse?.status === "failed" && nestedResponse.error != null;
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"replacement": " ",
|
||||
"context": "all",
|
||||
"category": "ultra",
|
||||
"minIntensity": "notes"
|
||||
"minIntensity": "ultra"
|
||||
},
|
||||
{
|
||||
"name": "ultra_lowercase",
|
||||
@@ -40,7 +40,7 @@
|
||||
"replacement": " $1",
|
||||
"context": "all",
|
||||
"category": "ultra",
|
||||
"minIntensity": "notes"
|
||||
"minIntensity": "ultra"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -166,6 +166,29 @@ function isReasoningOnlyReplayTarget(provider: unknown, model: unknown): boolean
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upstreams that reject an ABSENT reasoning_content on replay turns, so the
|
||||
* placeholder must survive the cache miss.
|
||||
*
|
||||
* #9573/#9610 removed the placeholder globally because the model echoed it as
|
||||
* its own reasoning and stopped (empty turns). That holds for DeepSeek, where
|
||||
* an absent field was verified to be accepted — but Xiaomi MiMo still 400s
|
||||
* ("Param Incorrect: The reasoning_content in the thinking mode must be passed
|
||||
* back to the API", 9router#1321/#1337), so omitting the field there trades one
|
||||
* live bug for another. Keep the placeholder only for those providers; the echo
|
||||
* that comes back is still stripped on the way in by
|
||||
* isInternalReasoningPlaceholder(), so it never re-poisons cache or history.
|
||||
*/
|
||||
function requiresReasoningContentPresence(provider: unknown, model: unknown): boolean {
|
||||
const normalizedProvider = String(provider ?? "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const normalizedModel = String(model ?? "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
return normalizedProvider === "xiaomi-mimo" || /(^|\/)mimo/i.test(normalizedModel);
|
||||
}
|
||||
|
||||
/** @param options.normalizeToolCallId - When true, use 9-char tool call ids (e.g. Mistral); when false, leave ids as-is */
|
||||
/** @param options.preserveDeveloperRole - undefined/true: keep developer for OpenAI format (default); false: map to system */
|
||||
/** @param options.preserveCacheControl - When true, preserve client-side cache_control markers (for Claude Code, etc.) */
|
||||
@@ -575,7 +598,11 @@ export function translateRequest(
|
||||
// the field instead; providers that genuinely enforce the contract
|
||||
// (kimi-coding, moonshot authentic-reasoning) have their own paths above.
|
||||
if ((hasToolCalls || shouldReplayReasoningOnly) && !msg.reasoning_content) {
|
||||
delete msg.reasoning_content;
|
||||
if (requiresReasoningContentPresence(normalizedProvider, normalizedModel)) {
|
||||
msg.reasoning_content = NON_ANTHROPIC_THINKING_PLACEHOLDER;
|
||||
} else {
|
||||
delete msg.reasoning_content;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
|
||||
@@ -2463,8 +2463,18 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
status: 200,
|
||||
usage,
|
||||
responseBody,
|
||||
// #9315 switched the summary to the accumulated responseBody to avoid
|
||||
// stale/truncated event data — but responseBody here is synthesized in
|
||||
// chat-completion shape, which loses the Responses API `response` object.
|
||||
// Keep the events-derived summary for OPENAI_RESPONSES only.
|
||||
providerPayload: providerPayloadCollector.build(
|
||||
responseBody,
|
||||
sourceFormat === FORMATS.OPENAI_RESPONSES
|
||||
? buildStreamSummaryFromEvents(
|
||||
providerPayloadCollector.getEvents(),
|
||||
sourceFormat,
|
||||
model
|
||||
)
|
||||
: responseBody,
|
||||
{ includeEvents: false }
|
||||
),
|
||||
clientPayload: clientPayloadCollector.build(responseBody, {
|
||||
@@ -2734,8 +2744,16 @@ export function createSSEStream(options: StreamOptions = {}) {
|
||||
status: 200,
|
||||
usage: state?.usage,
|
||||
responseBody,
|
||||
// Same OPENAI_RESPONSES carve-out as the passthrough branch above —
|
||||
// the synthesized chat-shaped responseBody drops the `response` object.
|
||||
providerPayload: providerPayloadCollector.build(
|
||||
responseBody,
|
||||
targetFormat === FORMATS.OPENAI_RESPONSES
|
||||
? buildStreamSummaryFromEvents(
|
||||
providerPayloadCollector.getEvents(),
|
||||
targetFormat,
|
||||
model
|
||||
)
|
||||
: responseBody,
|
||||
{ includeEvents: false }
|
||||
),
|
||||
clientPayload: clientPayloadCollector.build(responseBody, {
|
||||
|
||||
@@ -93,6 +93,10 @@ export const PACK_ARTIFACT_ROOT_ALLOWED_EXACT_PATHS: string[] = [
|
||||
// runtime; shipped via package.json "files", so it must be allowed here.
|
||||
"bin/aliasResolverHook.mjs",
|
||||
"bin/mcp-server.mjs",
|
||||
// #9281: stdout/stderr console guard preloaded via `node --import` by
|
||||
// bin/mcp-server.mjs before the MCP entry's module graph evaluates — without it
|
||||
// the published CLI's `omniroute --mcp` crashes on the pathToFileURL() import.
|
||||
"bin/mcpStdioConsoleGuard.mjs",
|
||||
"bin/nodeRuntimeSupport.mjs",
|
||||
"bin/omniroute.mjs",
|
||||
"bin/reset-password.mjs",
|
||||
@@ -183,6 +187,10 @@ export const PACK_ARTIFACT_REQUIRED_PATHS: string[] = [
|
||||
"bin/cli/utils/storageKeyProvision.mjs",
|
||||
"bin/cli/utils/versionFastPath.mjs",
|
||||
"bin/mcp-server.mjs",
|
||||
// #9281: stdout/stderr console guard preloaded via `node --import` by
|
||||
// bin/mcp-server.mjs before the MCP entry's module graph evaluates — without it
|
||||
// the published CLI's `omniroute --mcp` crashes on the pathToFileURL() import.
|
||||
"bin/mcpStdioConsoleGuard.mjs",
|
||||
"bin/nodeRuntimeSupport.mjs",
|
||||
"bin/omniroute.mjs",
|
||||
// #7808: aliasResolver + its hook file. bin/omniroute.mjs imports
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* Muse Code CLI proprietary model catalog endpoint.
|
||||
*
|
||||
* Muse CLI calls GET /muse-code/models (or --base-url/muse-code/models)
|
||||
* to discover available models. Returns the proprietary Muse format:
|
||||
*
|
||||
* { object: "list", data: [{ id, object, created, owned_by, metadata }] }
|
||||
*
|
||||
* Each model's metadata includes: name, family, reasoning, tool_call,
|
||||
* modalities, limit, cost.
|
||||
*/
|
||||
|
||||
import { muse_codeProvider } from "@omniroute/open-sse/config/providers/registry/muse-code/index.ts";
|
||||
|
||||
const MUSECODE_TIMESTAMP = Math.floor(Date.now() / 1000);
|
||||
|
||||
interface MuseCodeModel {
|
||||
id: string;
|
||||
object: "model";
|
||||
created: number;
|
||||
owned_by: string;
|
||||
metadata: {
|
||||
name: string;
|
||||
family: string;
|
||||
reasoning: boolean;
|
||||
tool_call: boolean;
|
||||
modalities: string[];
|
||||
limit: number;
|
||||
cost: number;
|
||||
};
|
||||
}
|
||||
|
||||
function buildModelCatalog(): MuseCodeModel[] {
|
||||
const data: MuseCodeModel[] = [];
|
||||
|
||||
for (const model of muse_codeProvider.models) {
|
||||
let family = "llama";
|
||||
if (model.id.includes("llama-4")) family = "llama-4";
|
||||
else if (model.id.includes("llama-3.3")) family = "llama-3.3";
|
||||
else if (model.id.includes("llama-3.2")) family = "llama-3.2";
|
||||
else if (model.id.includes("llama-3.1")) family = "llama-3.1";
|
||||
|
||||
const modalities: string[] = ["text"];
|
||||
if (model.supportsVision) modalities.push("image");
|
||||
|
||||
data.push({
|
||||
id: model.id,
|
||||
object: "model",
|
||||
created: MUSECODE_TIMESTAMP,
|
||||
owned_by: "meta",
|
||||
metadata: {
|
||||
name: model.name,
|
||||
family,
|
||||
reasoning: !!model.supportsReasoning,
|
||||
tool_call: !!model.toolCalling,
|
||||
modalities,
|
||||
limit: model.contextLength ?? 200_000,
|
||||
cost: model.id.includes("maverick") || model.id.includes("405b") ? 3 : 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Cache the catalog for the lifetime of the process — model list is static.
|
||||
const CATALOG = buildModelCatalog();
|
||||
const CATALOG_PAYLOAD = JSON.stringify({ object: "list", data: CATALOG }, null, 2);
|
||||
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "*",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
return new Response(CATALOG_PAYLOAD, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"cache-control": "public, max-age=3600",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -5071,6 +5071,8 @@
|
||||
"noNewModelsAddedExisting": "Không có mô hình mới nào được thêm (tất cả đã tồn tại).",
|
||||
"importDoneCount": "✓ Hoàn tất! {count, plural, one {Đã nhập # mô hình.} other {Đã nhập # mô hình.}}",
|
||||
"unexpectedErrorOccurred": "Đã xảy ra lỗi không mong muốn",
|
||||
"getApiKey": "Lấy khóa API",
|
||||
"getApiKeyDescription": "Đăng ký hoặc tạo tài khoản để nhận khóa API",
|
||||
"connectionCountLabel": "{count, plural, one {# kết nối} other {# kết nối}}",
|
||||
"messagesPath": "messages",
|
||||
"responsesPath": "responses",
|
||||
@@ -5201,6 +5203,18 @@
|
||||
"interceptFetchHint": "Ghi đè các lệnh gọi công cụ web_fetch gốc sang /v1/web/fetch của OmniRoute.",
|
||||
"interceptionLoadError": "Không thể tải cài đặt chặn: {error}",
|
||||
"interceptionSaveError": "Không thể lưu cài đặt chặn: {error}",
|
||||
"ccAliasSectionTitle": "Hiển thị trong Claude Code (claude/…)",
|
||||
"ccAliasSectionHint": "Công bố các mô hình của nhà cung cấp này dưới dạng id phản chiếu claude/<provider>/<model> để tính năng khám phá mô hình qua gateway của Claude Code có thể liệt kê chúng. Mặc định tắt — bật lên sẽ nhân đôi số mục trong danh mục với mọi client.",
|
||||
"ccAliasProviderLevelLabel": "Mặc định của nhà cung cấp",
|
||||
"ccAliasModelOverridesLabel": "Ghi đè theo từng mô hình",
|
||||
"ccAliasModelOverrideAriaLabel": "Ghi đè cho {modelId}",
|
||||
"ccAliasStateInherit": "Kế thừa",
|
||||
"ccAliasStateOn": "Bật",
|
||||
"ccAliasStateOff": "Tắt",
|
||||
"ccAliasAddModelPlaceholder": "Id mô hình (ví dụ: gpt-4o)",
|
||||
"ccAliasAddModelButton": "Thêm ghi đè",
|
||||
"ccAliasLoadError": "Không tải được cài đặt bí danh khám phá: {error}",
|
||||
"ccAliasSaveError": "Không lưu được cài đặt bí danh khám phá: {error}",
|
||||
"compatUpstreamHeadersLabel": "Các header upstream bổ sung",
|
||||
"compatUpstreamHeadersHint": "Cài đặt có đặc quyền cao — có cùng mức độ tin cậy như khi chỉnh sửa thông tin xác thực API của nhà cung cấp; chỉ quản trị viên đáng tin cậy mới nên sử dụng. Các header này được hợp nhất sau khi OmniRoute thêm thông tin xác thực từ khóa API của nhà cung cấp. Nếu một header tùy chỉnh có cùng tên với header hiện có (ví dụ: Authorization), giá trị của bạn sẽ thay thế hoàn toàn header được tạo tự động (bao gồm cả token Bearer) — máy chủ thượng nguồn chỉ nhận được nội dung bạn đã nhập, không phải khóa trong phần cài đặt. Cấu hình sai có thể gây ra lỗi 401 hoặc làm hỏng quá trình xác thực với máy chủ thượng nguồn. Mỗi hàng tương ứng với một header (ví dụ: header Authentication bổ sung cho một số cổng). Di chuột hoặc đặt tiêu điểm vào giá trị để xem trước. Tự động lưu khi mất tiêu điểm, nhấp ra ngoài hoặc đóng bảng điều khiển này.",
|
||||
"compatUpstreamHeaderName": "Tên header",
|
||||
@@ -5475,6 +5489,13 @@
|
||||
"newApiUserIdLabel": "ID người dùng New-API",
|
||||
"newApiUserIdPlaceholder": "vd. 12345",
|
||||
"newApiUserIdHint": "Giá trị tiêu đề New-Api-User của AgentRouter, dùng cùng với khóa API console để lấy số dư hạn mức.",
|
||||
"newApiAggregatorToggleLabel": "Cổng tổng hợp",
|
||||
"newApiAggregatorToggleHint": "Bật phát hiện số dư cho các node tổng hợp New-API / One-API / Sub2API. Bảng điều khiển sẽ hiển thị huy hiệu số dư và định tuyến quota-preflight sẽ bỏ qua các tài khoản đã cạn.",
|
||||
"newApiAggregatorConsoleApiKeyHint": "System Access Token cho endpoint /api/user/self của bộ tổng hợp. Không phải khóa API định tuyến.",
|
||||
"newApiAggregatorUserIdHint": "Giá trị header New-Api-User dùng để lấy số dư quota của người dùng bộ tổng hợp.",
|
||||
"newApiAggregatorQuotaPerUnitLabel": "Quota mỗi đơn vị",
|
||||
"newApiAggregatorQuotaPerUnitHint": "Số đơn vị tín dụng New-API cho mỗi 1 USD (mặc định: 500000). Ghi đè nếu bộ tổng hợp của bạn dùng tỷ lệ khác.",
|
||||
"featureFlagNewApiAggregatorBalanceDescription": "Bật phát hiện số dư cho các node tương thích New-API / One-API / Sub2API",
|
||||
"cpaModeDisabledTitle": "Chế độ tương thích CLIProxyAPI đã bị tắt",
|
||||
"cpaModeEnabledTitle": "Chế độ tương thích CLIProxyAPI đã được bật",
|
||||
"customUserAgentHint": "Gợi ý User Agent tùy chỉnh",
|
||||
@@ -5590,6 +5611,7 @@
|
||||
"tagGroupPlaceholder": "Nhập nhóm thẻ...",
|
||||
"testModel": "Kiểm tra mô hình",
|
||||
"testingModel": "Đang kiểm tra mô hình",
|
||||
"modelTestQuotaTooltip": "Đã hết quota — sẽ đặt lại vào ngày mai hoặc cần nạp thêm",
|
||||
"toggleOffShort": "Tắt",
|
||||
"toggleOnShort": "Bật",
|
||||
"tokenExpiredBadge": "Nhãn token đã hết hạn",
|
||||
@@ -6008,27 +6030,7 @@
|
||||
"kimiOfficialSupporterTooltip": "Kimi (Moonshot AI) là người bạn mã nguồn mở sáng lập của OmniRoute",
|
||||
"cheaperInferenceSupporterBadge": "Người bạn mã nguồn mở",
|
||||
"cheaperInferenceSupporterTooltip": "Cheaper Inference hỗ trợ OmniRoute với tư cách là người bạn mã nguồn mở",
|
||||
"kimiPartnerLinkNote": "Partner link — supports OmniRoute at no extra cost to you",
|
||||
"ccAliasSectionTitle": "Hiển thị trong Claude Code (claude/…)",
|
||||
"ccAliasSectionHint": "Công bố các mô hình của nhà cung cấp này dưới dạng id phản chiếu claude/<provider>/<model> để tính năng khám phá mô hình qua gateway của Claude Code có thể liệt kê chúng. Mặc định tắt — bật lên sẽ nhân đôi số mục trong danh mục với mọi client.",
|
||||
"ccAliasProviderLevelLabel": "Mặc định của nhà cung cấp",
|
||||
"ccAliasModelOverridesLabel": "Ghi đè theo từng mô hình",
|
||||
"ccAliasModelOverrideAriaLabel": "Ghi đè cho {modelId}",
|
||||
"ccAliasStateInherit": "Kế thừa",
|
||||
"ccAliasStateOn": "Bật",
|
||||
"ccAliasStateOff": "Tắt",
|
||||
"ccAliasAddModelPlaceholder": "Id mô hình (ví dụ: gpt-4o)",
|
||||
"ccAliasAddModelButton": "Thêm ghi đè",
|
||||
"ccAliasLoadError": "Không tải được cài đặt bí danh khám phá: {error}",
|
||||
"ccAliasSaveError": "Không lưu được cài đặt bí danh khám phá: {error}",
|
||||
"newApiAggregatorToggleLabel": "Cổng tổng hợp",
|
||||
"newApiAggregatorToggleHint": "Bật phát hiện số dư cho các node tổng hợp New-API / One-API / Sub2API. Bảng điều khiển sẽ hiển thị huy hiệu số dư và định tuyến quota-preflight sẽ bỏ qua các tài khoản đã cạn.",
|
||||
"newApiAggregatorConsoleApiKeyHint": "System Access Token cho endpoint /api/user/self của bộ tổng hợp. Không phải khóa API định tuyến.",
|
||||
"newApiAggregatorUserIdHint": "Giá trị header New-Api-User dùng để lấy số dư quota của người dùng bộ tổng hợp.",
|
||||
"newApiAggregatorQuotaPerUnitLabel": "Quota mỗi đơn vị",
|
||||
"newApiAggregatorQuotaPerUnitHint": "Số đơn vị tín dụng New-API cho mỗi 1 USD (mặc định: 500000). Ghi đè nếu bộ tổng hợp của bạn dùng tỷ lệ khác.",
|
||||
"featureFlagNewApiAggregatorBalanceDescription": "Bật phát hiện số dư cho các node tương thích New-API / One-API / Sub2API",
|
||||
"modelTestQuotaTooltip": "Đã hết quota — sẽ đặt lại vào ngày mai hoặc cần nạp thêm"
|
||||
"kimiPartnerLinkNote": "Partner link — supports OmniRoute at no extra cost to you"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Cài đặt",
|
||||
|
||||
@@ -345,7 +345,10 @@ function resolveCatalogPricing(
|
||||
// Consulted only when models.dev returned nothing, matching the order
|
||||
// already implemented in db/settings/pricing.ts::getPricing().
|
||||
try {
|
||||
const litellm = getSyncedPricing() as Record<string, Record<string, Record<string, number>>>;
|
||||
const litellm = getSyncedPricing() as unknown as Record<
|
||||
string,
|
||||
Record<string, Record<string, number>>
|
||||
>;
|
||||
const providerPricing =
|
||||
findInsensitive(litellm, provider) || findInsensitive(litellm, provider.replace(/-cn$/, ""));
|
||||
if (providerPricing) {
|
||||
|
||||
@@ -217,4 +217,3 @@ export type RadarProvider = z.infer<typeof ProviderSchema>;
|
||||
export type RadarQuirk = z.infer<typeof QuirkSchema>;
|
||||
export type RadarBudget = z.infer<typeof BudgetSchema>;
|
||||
export type RadarReferral = z.infer<typeof RadarReferralSchema>;
|
||||
export type RadarReferrals = z.infer<typeof RadarReferralsSchema>;
|
||||
|
||||
@@ -51,13 +51,6 @@ const GEMINI_CLI_PROFILE: ClientIdentityProfile = Object.freeze({
|
||||
"User-Agent": "GeminiCLI/0.1.0 (linux; x64)",
|
||||
}),
|
||||
});
|
||||
const MUSE_CLI_PROFILE: ClientIdentityProfile = Object.freeze({
|
||||
id: "muse-cli",
|
||||
label: "Muse Code CLI",
|
||||
headers: Object.freeze({
|
||||
"User-Agent": "MuseCodeCLI/0.1.0 (linux; x64)",
|
||||
}),
|
||||
});
|
||||
|
||||
/** Ordered so `CLIENT_IDENTITY_PROFILE_OPTIONS` renders "Default" first. */
|
||||
export const CLIENT_IDENTITY_PROFILES: Readonly<Record<string, ClientIdentityProfile>> =
|
||||
@@ -66,7 +59,6 @@ export const CLIENT_IDENTITY_PROFILES: Readonly<Record<string, ClientIdentityPro
|
||||
"claude-cli": CLAUDE_CLI_PROFILE,
|
||||
"codex-cli": CODEX_CLI_PROFILE,
|
||||
"gemini-cli": GEMINI_CLI_PROFILE,
|
||||
"muse-cli": MUSE_CLI_PROFILE,
|
||||
});
|
||||
|
||||
export const CLIENT_IDENTITY_PROFILE_IDS: readonly string[] = Object.keys(CLIENT_IDENTITY_PROFILES);
|
||||
|
||||
@@ -275,19 +275,4 @@ export const APIKEY_PROVIDERS_FRONTIER = {
|
||||
"Writer Palmyra is OpenAI-compatible at https://api.writer.com/v1. palmyra-x5 offers a 1M-token context window.",
|
||||
hasFree: false,
|
||||
},
|
||||
"muse-code": {
|
||||
id: "muse-code",
|
||||
alias: "mc",
|
||||
name: "Muse Code (Meta)",
|
||||
icon: "auto_awesome",
|
||||
color: "#0866FF",
|
||||
textIcon: "MC",
|
||||
website: "https://github.com/meta-llama/llama-stack",
|
||||
authHint:
|
||||
"Use your META_API_KEY env var as a Bearer token. Muse Code CLI uses the OpenAI Responses API wire format (POST /responses).",
|
||||
apiHint:
|
||||
"Muse Code is OpenAI-compatible. OmniRoute routes chat traffic through the Responses API and exposes the proprietary model catalog at /v1/muse-code/models.",
|
||||
passthroughModels: true,
|
||||
hasFree: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1348,7 +1348,7 @@ async function handleSingleModelChat(
|
||||
const breakerFailureStatus = Number(lastStatus ?? credentials?.lastErrorCode);
|
||||
if (
|
||||
!forceLiveComboTest &&
|
||||
isAllRateLimited &&
|
||||
credentials?.allRateLimited &&
|
||||
PROVIDER_BREAKER_FAILURE_STATUSES.has(breakerFailureStatus)
|
||||
) {
|
||||
breaker._onFailure();
|
||||
|
||||
@@ -39,9 +39,7 @@
|
||||
"incremental": true,
|
||||
"incrementalFile": "reports/mutation/stryker-incremental.json",
|
||||
"testRunner": "tap",
|
||||
"plugins": [
|
||||
"@stryker-mutator/tap-runner"
|
||||
],
|
||||
"plugins": ["@stryker-mutator/tap-runner"],
|
||||
"tap": {
|
||||
"testFiles": [
|
||||
"tests/unit/7993-noauth-proxy-routing.test.ts",
|
||||
@@ -52,6 +50,7 @@
|
||||
"tests/unit/8376-econnrefused-breaker.test.ts",
|
||||
"tests/unit/8396-cooldown-429-cap.test.ts",
|
||||
"tests/unit/8488-capability-filter-fail-closed.test.ts",
|
||||
"tests/unit/8779-agy-prefix-credential-lookup.test.ts",
|
||||
"tests/unit/account-fallback-anthropic-quota.test.ts",
|
||||
"tests/unit/account-fallback-cf1010-no-retry-8775.test.ts",
|
||||
"tests/unit/account-fallback-lockout-eviction.test.ts",
|
||||
@@ -85,6 +84,7 @@
|
||||
"tests/unit/auto-combo-engine.test.ts",
|
||||
"tests/unit/auto-combo-scoring-clamp.test.ts",
|
||||
"tests/unit/bug-7940-gemini-retrydelay.test.ts",
|
||||
"tests/unit/bug-9204-agy-provider-alias-credentials.test.ts",
|
||||
"tests/unit/build/check-circular-deps.test.ts",
|
||||
"tests/unit/cache-sweeps.test.ts",
|
||||
"tests/unit/cc-bridge-openai-image-7777.test.ts",
|
||||
@@ -191,6 +191,7 @@
|
||||
"tests/unit/combo/combo-target-timeout-standards.test.ts",
|
||||
"tests/unit/combo/effective-max-concurrency.test.ts",
|
||||
"tests/unit/combo/recovery-hint.test.ts",
|
||||
"tests/unit/combo/reset-window-strategy-9330.test.ts",
|
||||
"tests/unit/complexity-aware-scoring-wiring.test.ts",
|
||||
"tests/unit/compression-header-verification.test.ts",
|
||||
"tests/unit/context-pinning-tool-calls.test.ts",
|
||||
@@ -250,6 +251,7 @@
|
||||
"tests/unit/observability-payloads.test.ts",
|
||||
"tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts",
|
||||
"tests/unit/openapi-security-tiers.test.ts",
|
||||
"tests/unit/openrouter-passthrough-models.test.ts",
|
||||
"tests/unit/openrouter-quota-6842.test.ts",
|
||||
"tests/unit/persist-429-cooldown-account-fallback.test.ts",
|
||||
"tests/unit/plan3-p0.test.ts",
|
||||
@@ -271,6 +273,7 @@
|
||||
"tests/unit/rate-limit-manager.test.ts",
|
||||
"tests/unit/rate-limit-queue-timeout-lockout.test.ts",
|
||||
"tests/unit/repro-7503-no-choices.test.ts",
|
||||
"tests/unit/repro-9630-combo-false-503.test.ts",
|
||||
"tests/unit/repro-antigravity-404-family-cooldown-hijack.test.ts",
|
||||
"tests/unit/responses-handler.test.ts",
|
||||
"tests/unit/rotation-config-omniroute.test.ts",
|
||||
@@ -428,11 +431,7 @@
|
||||
".worktrees",
|
||||
".stryker-tmp"
|
||||
],
|
||||
"reporters": [
|
||||
"progress",
|
||||
"html",
|
||||
"json"
|
||||
],
|
||||
"reporters": ["progress", "html", "json"],
|
||||
"htmlReporter": {
|
||||
"fileName": "reports/mutation/mutation.html"
|
||||
},
|
||||
|
||||
@@ -3405,26 +3405,6 @@
|
||||
"stream": "https://api.morphllm.com/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"muse-code": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"url": {}
|
||||
},
|
||||
"muse-spark-web": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* This test verifies the payload normalization fix is present in the source code
|
||||
* and that the correct contract keys are read by loadData().
|
||||
*
|
||||
* Run: node --import tsx/esm --test tests/unit/free-pool-frontend-repro.test.tsx
|
||||
* Run: node --import tsx/esm --test tests/unit/free-pool-frontend-repro.test.ts
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
@@ -20,11 +20,10 @@ const ROOT = new URL("../..", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/
|
||||
*/
|
||||
describe("omniroute --mcp stdio transport", () => {
|
||||
it("writes only valid JSON-RPC to stdout — no DB init or other startup logging leaks through", async () => {
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
[join(ROOT, "bin", "omniroute.mjs"), "--mcp"],
|
||||
{ cwd: ROOT, env: process.env }
|
||||
);
|
||||
const child = spawn(process.execPath, [join(ROOT, "bin", "omniroute.mjs"), "--mcp"], {
|
||||
cwd: ROOT,
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
@@ -48,11 +47,23 @@ describe("omniroute --mcp stdio transport", () => {
|
||||
})}\n`
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 4000));
|
||||
// The full chain (omniroute.mjs CLI startup + spawned MCP child, each paying a tsx
|
||||
// import + the child's DB init/migrations) takes ~10s on a warm dev box and longer on
|
||||
// loaded CI runners — a fixed 4s sleep made this test red from birth. Poll for the
|
||||
// first stdout line instead, then give the stream a short settle window so any
|
||||
// late startup logging that WOULD corrupt the protocol still gets caught.
|
||||
const deadline = Date.now() + 60_000;
|
||||
while (!stdout.includes("\n") && Date.now() < deadline) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
child.kill();
|
||||
|
||||
const stdoutLines = stdout.split("\n").filter((line) => line.trim().length > 0);
|
||||
assert.ok(stdoutLines.length > 0, "expected at least one line on stdout (the initialize response)");
|
||||
assert.ok(
|
||||
stdoutLines.length > 0,
|
||||
"expected at least one line on stdout (the initialize response)"
|
||||
);
|
||||
|
||||
for (const line of stdoutLines) {
|
||||
assert.doesNotThrow(
|
||||
@@ -61,9 +72,7 @@ describe("omniroute --mcp stdio transport", () => {
|
||||
);
|
||||
}
|
||||
|
||||
const initResponse = stdoutLines
|
||||
.map((line) => JSON.parse(line))
|
||||
.find((msg) => msg.id === 1);
|
||||
const initResponse = stdoutLines.map((line) => JSON.parse(line)).find((msg) => msg.id === 1);
|
||||
assert.ok(initResponse, "expected an initialize response with id 1 on stdout");
|
||||
assert.equal(initResponse.jsonrpc, "2.0");
|
||||
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/**
|
||||
* Tests for Muse Code CLI model catalog endpoint.
|
||||
*
|
||||
* Verifies GET /v1/muse-code/models returns the proprietary Muse format.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { muse_codeProvider } from "../../open-sse/config/providers/registry/muse-code/index.ts";
|
||||
|
||||
// ── Model catalog shape ─────────────────────────────────────────────────────
|
||||
|
||||
test("muse-code provider has at least one model", () => {
|
||||
assert.ok(muse_codeProvider.models.length >= 1);
|
||||
});
|
||||
|
||||
test("muse-code models have unique ids", () => {
|
||||
const ids = muse_codeProvider.models.map((m) => m.id);
|
||||
const unique = new Set(ids);
|
||||
assert.equal(unique.size, ids.length, "model IDs must be unique");
|
||||
});
|
||||
|
||||
test("muse-code models include llama-4-maverick", () => {
|
||||
const ids = muse_codeProvider.models.map((m) => m.id);
|
||||
assert.ok(ids.includes("llama-4-maverick"), "must include llama-4-maverick");
|
||||
});
|
||||
|
||||
test("muse-code models include llama-4-scout", () => {
|
||||
const ids = muse_codeProvider.models.map((m) => m.id);
|
||||
assert.ok(ids.includes("llama-4-scout"), "must include llama-4-scout");
|
||||
});
|
||||
|
||||
test("muse-code models include llama-3.3-70b", () => {
|
||||
const ids = muse_codeProvider.models.map((m) => m.id);
|
||||
assert.ok(ids.includes("llama-3.3-70b"), "must include llama-3.3-70b");
|
||||
});
|
||||
|
||||
test("llama-4 models have supportsXHighEffort", () => {
|
||||
const maverick = muse_codeProvider.models.find((m) => m.id === "llama-4-maverick");
|
||||
assert.ok(maverick, "llama-4-maverick must exist");
|
||||
assert.equal(maverick.supportsXHighEffort, true);
|
||||
|
||||
const scout = muse_codeProvider.models.find((m) => m.id === "llama-4-scout");
|
||||
assert.ok(scout, "llama-4-scout must exist");
|
||||
assert.equal(scout.supportsXHighEffort, true);
|
||||
});
|
||||
|
||||
test("llama-3.3-70b does not support reasoning", () => {
|
||||
const model = muse_codeProvider.models.find((m) => m.id === "llama-3.3-70b");
|
||||
assert.ok(model, "llama-3.3-70b must exist");
|
||||
assert.equal(model.supportsReasoning, false);
|
||||
});
|
||||
|
||||
test("non-reasoning models do not declare supportsXHighEffort", () => {
|
||||
for (const model of muse_codeProvider.models) {
|
||||
if (!model.supportsReasoning) {
|
||||
assert.equal(
|
||||
model.supportsXHighEffort,
|
||||
undefined,
|
||||
`${model.id} is not a reasoning model but has supportsXHighEffort`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── Vision models ───────────────────────────────────────────────────────────
|
||||
|
||||
test("vision models have supportsVision: true", () => {
|
||||
const expectedVision = [
|
||||
"llama-4-maverick",
|
||||
"llama-4-scout",
|
||||
"llama-3.2-90b-vision",
|
||||
"llama-3.2-11b-vision",
|
||||
];
|
||||
for (const model of muse_codeProvider.models) {
|
||||
if (expectedVision.includes(model.id)) {
|
||||
assert.equal(model.supportsVision, true, `${model.id} should have supportsVision`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,91 +0,0 @@
|
||||
/**
|
||||
* Tests for Muse Code CLI provider registry entry.
|
||||
*
|
||||
* Verifies the provider entry loads correctly with expected config.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { muse_codeProvider } from "../../open-sse/config/providers/registry/muse-code/index.ts";
|
||||
import { getRegistryEntry } from "../../open-sse/config/providerRegistry.ts";
|
||||
|
||||
// ── Registry entry structure ────────────────────────────────────────────────
|
||||
|
||||
test("muse-code provider entry has id", () => {
|
||||
assert.equal(muse_codeProvider.id, "muse-code");
|
||||
});
|
||||
|
||||
test("muse-code provider entry has alias", () => {
|
||||
assert.equal(muse_codeProvider.alias, "mc");
|
||||
});
|
||||
|
||||
test("muse-code provider uses openai format", () => {
|
||||
assert.equal(muse_codeProvider.format, "openai");
|
||||
});
|
||||
|
||||
test("muse-code provider uses apikey auth", () => {
|
||||
assert.equal(muse_codeProvider.authType, "apikey");
|
||||
assert.equal(muse_codeProvider.authHeader, "bearer");
|
||||
});
|
||||
|
||||
test("muse-code provider has passthroughModels enabled", () => {
|
||||
assert.equal(muse_codeProvider.passthroughModels, true);
|
||||
});
|
||||
|
||||
// ── Model entries ───────────────────────────────────────────────────────────
|
||||
|
||||
test("muse-code provider has curated models", () => {
|
||||
assert.ok(muse_codeProvider.models.length > 0);
|
||||
});
|
||||
|
||||
test("all muse-code models have contextLength", () => {
|
||||
for (const model of muse_codeProvider.models) {
|
||||
assert.ok(
|
||||
typeof model.contextLength === "number" && model.contextLength > 0,
|
||||
`${model.id} must have positive contextLength`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("all muse-code models have toolCalling: true", () => {
|
||||
for (const model of muse_codeProvider.models) {
|
||||
assert.equal(model.toolCalling, true, `${model.id} must have toolCalling enabled`);
|
||||
}
|
||||
});
|
||||
|
||||
test("all muse-code models have targetFormat: openai-responses", () => {
|
||||
for (const model of muse_codeProvider.models) {
|
||||
assert.equal(
|
||||
model.targetFormat,
|
||||
"openai-responses",
|
||||
`${model.id} must use openai-responses target format`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("reasoning models have supportsXHighEffort", () => {
|
||||
for (const model of muse_codeProvider.models) {
|
||||
if (model.supportsReasoning) {
|
||||
assert.equal(
|
||||
model.supportsXHighEffort,
|
||||
true,
|
||||
`${model.id} is a reasoning model but missing supportsXHighEffort`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── Registry discovery ──────────────────────────────────────────────────────
|
||||
|
||||
test("muse-code is discoverable via getRegistryEntry", () => {
|
||||
const entry = getRegistryEntry("muse-code");
|
||||
assert.ok(entry, "getRegistryEntry must return muse-code entry");
|
||||
assert.equal(entry.id, "muse-code");
|
||||
});
|
||||
|
||||
test("muse-code is discoverable via alias", () => {
|
||||
const entry = getRegistryEntry("mc");
|
||||
assert.ok(entry, "getRegistryEntry must find muse-code by alias mc");
|
||||
assert.equal(entry.id, "muse-code");
|
||||
});
|
||||
@@ -154,6 +154,7 @@ test("findMissingArtifactPaths flags missing root runtime files in the tarball",
|
||||
"bin/cli/utils/storageKeyProvision.mjs",
|
||||
"bin/cli/utils/versionFastPath.mjs",
|
||||
"bin/mcp-server.mjs",
|
||||
"bin/mcpStdioConsoleGuard.mjs",
|
||||
"bin/nodeRuntimeSupport.mjs",
|
||||
"dist/head-response-guard.cjs",
|
||||
"dist/http-method-guard.cjs",
|
||||
|
||||
@@ -42,6 +42,10 @@ const PLUGIN_ROUTES: Array<{ rel: string; label: string }> = [
|
||||
rel: "src/app/api/plugins/marketplace/route.ts",
|
||||
label: "GET /api/plugins/marketplace",
|
||||
},
|
||||
{
|
||||
rel: "src/app/api/plugins/marketplace/install/route.ts",
|
||||
label: "POST /api/plugins/marketplace/install",
|
||||
},
|
||||
];
|
||||
|
||||
for (const { rel, label } of PLUGIN_ROUTES) {
|
||||
|
||||
@@ -51,9 +51,8 @@ function createFixturePlugin(name: string, opts?: { onResponse?: boolean; onRequ
|
||||
// ── Manifest validation ──
|
||||
|
||||
test("plugin manifest validation", async (t) => {
|
||||
const { validateManifest, safeValidateManifest, applyDefaults } = await import(
|
||||
"../../src/lib/plugins/manifest.ts"
|
||||
);
|
||||
const { validateManifest, safeValidateManifest, applyDefaults } =
|
||||
await import("../../src/lib/plugins/manifest.ts");
|
||||
|
||||
await t.test("valid manifest parses with defaults", () => {
|
||||
const result = validateManifest({
|
||||
@@ -136,8 +135,22 @@ test("plugin hooks system", async (t) => {
|
||||
|
||||
await t.test("registerHook registers and sorts by priority", () => {
|
||||
const calls: string[] = [];
|
||||
registerHook("onRequest", "plugin-b", () => { calls.push("b"); }, 200);
|
||||
registerHook("onRequest", "plugin-a", () => { calls.push("a"); }, 100);
|
||||
registerHook(
|
||||
"onRequest",
|
||||
"plugin-b",
|
||||
() => {
|
||||
calls.push("b");
|
||||
},
|
||||
200
|
||||
);
|
||||
registerHook(
|
||||
"onRequest",
|
||||
"plugin-a",
|
||||
() => {
|
||||
calls.push("a");
|
||||
},
|
||||
100
|
||||
);
|
||||
const hooks = getHooks("onRequest");
|
||||
assert.equal(hooks.length, 2);
|
||||
assert.equal(hooks[0].pluginName, "plugin-a");
|
||||
@@ -174,9 +187,30 @@ test("plugin hooks system", async (t) => {
|
||||
|
||||
await t.test("emitHook calls all handlers in order", async () => {
|
||||
const order: number[] = [];
|
||||
registerHook("onTest", "h1", () => { order.push(1); }, 100);
|
||||
registerHook("onTest", "h2", () => { order.push(2); }, 200);
|
||||
registerHook("onTest", "h3", () => { order.push(3); }, 150);
|
||||
registerHook(
|
||||
"onTest",
|
||||
"h1",
|
||||
() => {
|
||||
order.push(1);
|
||||
},
|
||||
100
|
||||
);
|
||||
registerHook(
|
||||
"onTest",
|
||||
"h2",
|
||||
() => {
|
||||
order.push(2);
|
||||
},
|
||||
200
|
||||
);
|
||||
registerHook(
|
||||
"onTest",
|
||||
"h3",
|
||||
() => {
|
||||
order.push(3);
|
||||
},
|
||||
150
|
||||
);
|
||||
await emitHook("onTest", {});
|
||||
assert.deepEqual(order, [1, 3, 2]);
|
||||
resetHooks();
|
||||
@@ -184,8 +218,22 @@ test("plugin hooks system", async (t) => {
|
||||
|
||||
await t.test("emitHook swallows handler errors", async () => {
|
||||
const calls: string[] = [];
|
||||
registerHook("onErr", "bad", () => { throw new Error("boom"); }, 100);
|
||||
registerHook("onErr", "good", () => { calls.push("ok"); }, 200);
|
||||
registerHook(
|
||||
"onErr",
|
||||
"bad",
|
||||
() => {
|
||||
throw new Error("boom");
|
||||
},
|
||||
100
|
||||
);
|
||||
registerHook(
|
||||
"onErr",
|
||||
"good",
|
||||
() => {
|
||||
calls.push("ok");
|
||||
},
|
||||
200
|
||||
);
|
||||
await emitHook("onErr", {});
|
||||
assert.deepEqual(calls, ["ok"]);
|
||||
resetHooks();
|
||||
@@ -203,7 +251,14 @@ test("plugin hooks system", async (t) => {
|
||||
await t.test("emitHookBlocking returns early on blocked", async () => {
|
||||
const calls: string[] = [];
|
||||
registerHook("onBlock2", "blocker", () => ({ blocked: true, response: { error: "no" } }), 100);
|
||||
registerHook("onBlock2", "after", () => { calls.push("after"); }, 200);
|
||||
registerHook(
|
||||
"onBlock2",
|
||||
"after",
|
||||
() => {
|
||||
calls.push("after");
|
||||
},
|
||||
200
|
||||
);
|
||||
const result = await emitHookBlocking("onBlock2", {});
|
||||
assert.equal(result.blocked, true);
|
||||
assert.equal(calls.length, 0);
|
||||
@@ -212,7 +267,13 @@ test("plugin hooks system", async (t) => {
|
||||
|
||||
await t.test("runOnRequest delegates to emitHookBlocking", async () => {
|
||||
registerHook("onRequest", "req", () => ({ metadata: { seen: true } }), 100);
|
||||
const result = await runOnRequest({ requestId: "1", body: {}, model: "gpt-4", provider: "openai", metadata: {} });
|
||||
const result = await runOnRequest({
|
||||
requestId: "1",
|
||||
body: {},
|
||||
model: "gpt-4",
|
||||
provider: "openai",
|
||||
metadata: {},
|
||||
});
|
||||
assert.deepEqual(result.metadata, { seen: true });
|
||||
resetHooks();
|
||||
});
|
||||
@@ -230,7 +291,14 @@ test("plugin hooks system", async (t) => {
|
||||
|
||||
await t.test("runOnError is fire-and-forget", async () => {
|
||||
let called = false;
|
||||
registerHook("onError", "err-handler", () => { called = true; }, 100);
|
||||
registerHook(
|
||||
"onError",
|
||||
"err-handler",
|
||||
() => {
|
||||
called = true;
|
||||
},
|
||||
100
|
||||
);
|
||||
await runOnError(
|
||||
{ requestId: "1", body: {}, model: "gpt-4", provider: "openai", metadata: {} },
|
||||
new Error("test")
|
||||
@@ -257,6 +325,8 @@ test("plugin hooks system", async (t) => {
|
||||
"onActivate",
|
||||
"onDeactivate",
|
||||
"onUninstall",
|
||||
// #9668: fire-and-forget stream telemetry hook (runOnStreamCompleteHooks)
|
||||
"onStreamComplete",
|
||||
]);
|
||||
resetHooks();
|
||||
});
|
||||
@@ -309,9 +379,7 @@ test("welcome banner PoC plugin lifecycle", async (t) => {
|
||||
await t.test("onResponse injects banner into response", async () => {
|
||||
const mod = await import(join(pluginDir, "index.mjs"));
|
||||
const response = {
|
||||
choices: [
|
||||
{ message: { role: "assistant", content: "Hello!" } },
|
||||
],
|
||||
choices: [{ message: { role: "assistant", content: "Hello!" } }],
|
||||
};
|
||||
const result = await mod.plugin.onResponse({}, response);
|
||||
assert.ok(result.choices[0].message.content.includes("[Welcome to OmniRoute"));
|
||||
@@ -321,9 +389,7 @@ test("welcome banner PoC plugin lifecycle", async (t) => {
|
||||
await t.test("onResponse handles streaming delta", async () => {
|
||||
const mod = await import(join(pluginDir, "index.mjs"));
|
||||
const response = {
|
||||
choices: [
|
||||
{ delta: { content: "stream chunk" } },
|
||||
],
|
||||
choices: [{ delta: { content: "stream chunk" } }],
|
||||
};
|
||||
const result = await mod.plugin.onResponse({}, response);
|
||||
assert.ok(result.choices[0].delta.content.includes("[Welcome to OmniRoute"));
|
||||
|
||||
@@ -181,10 +181,11 @@ test("provider models route merges live Codex models with the local catalog then
|
||||
// merge conservatively — the smaller of live vs. pinned wins, never the
|
||||
// larger, so a stale/inflated live number can never make OmniRoute promise
|
||||
// more context than the account can actually serve (#7012). Here the pinned
|
||||
// GPT-5.6 Codex contract (272000/128000, see GPT_5_6_CODEX_CAPABILITIES) is
|
||||
// smaller than the live payload's 999999/999999, so the pinned value wins.
|
||||
// GPT-5.6 Codex contract (922000/128000, see GPT_5_6_CODEX_CAPABILITIES —
|
||||
// raised from 272000 in #9432) is smaller than the live payload's
|
||||
// 999999/999999, so the pinned value wins.
|
||||
assert.equal(liveModel?.name, "GPT 5.6 Sol Live");
|
||||
assert.equal(liveModel?.inputTokenLimit, 272000);
|
||||
assert.equal(liveModel?.inputTokenLimit, 922000);
|
||||
assert.equal(liveModel?.outputTokenLimit, 128000);
|
||||
assert.equal(liveModel?.apiFormat, "responses");
|
||||
assert.deepEqual(liveModel?.supportedEndpoints, ["responses"]);
|
||||
|
||||
167
tests/unit/quality-validation-benign-error.test.ts
Normal file
167
tests/unit/quality-validation-benign-error.test.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* TDD regression guard — quality validation false-positive on benign `error`
|
||||
* fields in streaming SSE chunks.
|
||||
*
|
||||
* `isStreamingUpstreamError` treats ANY non-null `error` field as an upstream
|
||||
* failure: `parsed.error != null` is true for `{}`, `""`, `false`, and `0`.
|
||||
* When a client like opencode issues a tool-call turn, the upstream SSE opens
|
||||
* with role-only frames (no recognized content) and a later chunk that carries
|
||||
* real tool_calls content PLUS a benign empty `error` field (a field some
|
||||
* backends emit on every chunk). The error gate runs BEFORE the content
|
||||
* recognizers, so that single frame short-circuits to "error" → 502
|
||||
* "streaming upstream error" — while the same combo via kilocode (different
|
||||
* wire format) never emits the empty `error` field and works fine.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { validateResponseQuality } = await import("../../open-sse/services/combo.ts");
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const silentLog = { warn: () => {} };
|
||||
|
||||
function openAiSseStream(events: string[]): ReadableStream<Uint8Array> {
|
||||
const body = events.join("\n") + "\n";
|
||||
return new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(encoder.encode(body));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI-compatible tool-call stream that ALSO carries a benign empty `error`
|
||||
* field on the tool_calls chunk. Some backends emit `"error": {}` or
|
||||
* `"error": ""` alongside every chunk; that is not a real upstream failure.
|
||||
* The frame must be treated as CONTENT (valid), not ERROR.
|
||||
*/
|
||||
function makeToolCallStreamWithBenignError(): Response {
|
||||
const events = [
|
||||
// role-only first chunk — no recognized content, widens the peek window
|
||||
`data: ${JSON.stringify({
|
||||
id: "chatcmpl_1",
|
||||
object: "chat.completion.chunk",
|
||||
created: 123,
|
||||
model: "gpt-4o",
|
||||
choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }],
|
||||
})}`,
|
||||
"",
|
||||
// tool_calls delta + benign empty `error` field (the bug trigger)
|
||||
`data: ${JSON.stringify({
|
||||
id: "chatcmpl_2",
|
||||
object: "chat.completion.chunk",
|
||||
created: 123,
|
||||
model: "gpt-4o",
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
tool_calls: [
|
||||
{ index: 0, id: "call_1", type: "function", function: { name: "Bash", arguments: "" } },
|
||||
],
|
||||
},
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
error: {},
|
||||
})}`,
|
||||
"",
|
||||
`data: [DONE]`,
|
||||
"",
|
||||
];
|
||||
return new Response(openAiSseStream(events), {
|
||||
status: 200,
|
||||
headers: { "content-type": "text/event-stream" },
|
||||
});
|
||||
}
|
||||
|
||||
test("OpenAI stream with tool_calls + benign empty error:{} field is VALID (not 502)", async () => {
|
||||
const res = makeToolCallStreamWithBenignError();
|
||||
const out = await validateResponseQuality(res, true, silentLog);
|
||||
assert.equal(
|
||||
out.valid,
|
||||
true,
|
||||
`expected valid for tool_calls chunk with benign error:{}, got valid=false (reason: ${out.reason})`
|
||||
);
|
||||
assert.ok(out.clonedResponse, "clonedResponse must be present for valid streaming response");
|
||||
});
|
||||
|
||||
test("OpenAI stream with tool_calls + benign empty error:'' field is VALID", async () => {
|
||||
const events = [
|
||||
`data: ${JSON.stringify({
|
||||
id: "chatcmpl_3",
|
||||
object: "chat.completion.chunk",
|
||||
created: 123,
|
||||
model: "gpt-4o",
|
||||
choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }],
|
||||
})}`,
|
||||
"",
|
||||
`data: ${JSON.stringify({
|
||||
id: "chatcmpl_4",
|
||||
object: "chat.completion.chunk",
|
||||
created: 123,
|
||||
model: "gpt-4o",
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
tool_calls: [
|
||||
{ index: 0, id: "call_2", type: "function", function: { name: "Read", arguments: "" } },
|
||||
],
|
||||
},
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
error: "",
|
||||
})}`,
|
||||
"",
|
||||
`data: [DONE]`,
|
||||
"",
|
||||
];
|
||||
const res = new Response(openAiSseStream(events), {
|
||||
status: 200,
|
||||
headers: { "content-type": "text/event-stream" },
|
||||
});
|
||||
const out = await validateResponseQuality(res, true, silentLog);
|
||||
assert.equal(
|
||||
out.valid,
|
||||
true,
|
||||
`expected valid for tool_calls chunk with benign error:"", got valid=false (reason: ${out.reason})`
|
||||
);
|
||||
});
|
||||
|
||||
test("Stream with a REAL non-empty error object is still flagged as invalid", async () => {
|
||||
const events = [
|
||||
`data: ${JSON.stringify({
|
||||
id: "chatcmpl_5",
|
||||
object: "chat.completion.chunk",
|
||||
created: 123,
|
||||
model: "gpt-4o",
|
||||
choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }],
|
||||
})}`,
|
||||
"",
|
||||
`data: ${JSON.stringify({
|
||||
id: "chatcmpl_6",
|
||||
object: "chat.completion.chunk",
|
||||
created: 123,
|
||||
model: "gpt-4o",
|
||||
choices: [{ index: 0, delta: {}, finish_reason: null }],
|
||||
error: { message: "upstream quota exceeded", code: "rate_limit_exceeded" },
|
||||
})}`,
|
||||
"",
|
||||
`data: [DONE]`,
|
||||
"",
|
||||
];
|
||||
const res = new Response(openAiSseStream(events), {
|
||||
status: 200,
|
||||
headers: { "content-type": "text/event-stream" },
|
||||
});
|
||||
const out = await validateResponseQuality(res, true, silentLog);
|
||||
assert.equal(
|
||||
out.valid,
|
||||
false,
|
||||
`expected invalid for real error object, got valid=true (reason: ${out.reason})`
|
||||
);
|
||||
assert.match(out.reason ?? "", /streaming upstream error/, "reason should mention the upstream error");
|
||||
});
|
||||
@@ -755,11 +755,13 @@ describe("Reasoning Replay Cache — Translator Replay", () => {
|
||||
assert.equal(translated.messages[1].reasoning_content, undefined);
|
||||
});
|
||||
|
||||
it("should replace empty-string reasoning_content with NON_ANTHROPIC_THINKING_PLACEHOLDER on cache miss", async () => {
|
||||
it("should drop empty-string reasoning_content on cache miss", async () => {
|
||||
// Regression: injectEmptyReasoningContentForToolCalls (schemaCoercion.ts) pre-sets
|
||||
// reasoning_content="" before the cache lookup. The old condition
|
||||
// `msg.reasoning_content === undefined` never fired on cache miss, leaving the
|
||||
// empty string in place. DeepSeek V4+ rejects "" with a 400.
|
||||
// reasoning_content="" before the cache lookup, and DeepSeek V4+ rejects "" with a
|
||||
// 400 — so the empty string must not survive the miss. #9573/#9610 replaced the
|
||||
// former NON_ANTHROPIC_THINKING_PLACEHOLDER injection with omitting the field: the
|
||||
// placeholder was echoed back by the model as its own reasoning (empty stop) and
|
||||
// re-poisoned cache + client history, while an ABSENT field is accepted.
|
||||
clearReasoningCacheAll();
|
||||
clearModelsDevCapabilities();
|
||||
saveModelsDevCapabilities({
|
||||
@@ -772,9 +774,6 @@ describe("Reasoning Replay Cache — Translator Replay", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { NON_ANTHROPIC_THINKING_PLACEHOLDER } =
|
||||
await import("../../open-sse/translator/helpers/claudeHelper.ts");
|
||||
|
||||
// No cache entry → cache miss
|
||||
const translated = translateRequest(
|
||||
FORMATS.OPENAI,
|
||||
@@ -805,16 +804,17 @@ describe("Reasoning Replay Cache — Translator Replay", () => {
|
||||
|
||||
assert.equal(
|
||||
translated.messages[1].reasoning_content,
|
||||
NON_ANTHROPIC_THINKING_PLACEHOLDER,
|
||||
"empty reasoning_content should be replaced with placeholder on cache miss"
|
||||
undefined,
|
||||
"empty reasoning_content should be dropped (not placeholder-filled) on cache miss"
|
||||
);
|
||||
});
|
||||
|
||||
it("should inject placeholder for a plain (non-tool-call) DeepSeek turn missing reasoning_content (#1682)", async () => {
|
||||
it("should omit reasoning_content for a plain (non-tool-call) DeepSeek turn missing it (#1682)", async () => {
|
||||
// Regression (#1682): a multi-turn text conversation where the prior assistant
|
||||
// turn has NO tool calls and the client (e.g. Cursor) stripped reasoning_content
|
||||
// from history. DeepSeek V4+ still requires reasoning_content on every assistant
|
||||
// message in thinking mode, so without a placeholder the upstream returns 400.
|
||||
// from history. #9573/#9610 established that DeepSeek's 400 is specific to an
|
||||
// EMPTY-STRING reasoning_content, not an absent field — so the field is now
|
||||
// omitted here instead of carrying the self-poisoning placeholder.
|
||||
clearReasoningCacheAll();
|
||||
clearModelsDevCapabilities();
|
||||
saveModelsDevCapabilities({
|
||||
@@ -827,9 +827,6 @@ describe("Reasoning Replay Cache — Translator Replay", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { NON_ANTHROPIC_THINKING_PLACEHOLDER } =
|
||||
await import("../../open-sse/translator/helpers/claudeHelper.ts");
|
||||
|
||||
const translated = translateRequest(
|
||||
FORMATS.OPENAI,
|
||||
FORMATS.OPENAI,
|
||||
@@ -849,8 +846,8 @@ describe("Reasoning Replay Cache — Translator Replay", () => {
|
||||
|
||||
assert.equal(
|
||||
translated.messages[1].reasoning_content,
|
||||
NON_ANTHROPIC_THINKING_PLACEHOLDER,
|
||||
"plain DeepSeek assistant turn missing reasoning_content should get the placeholder"
|
||||
undefined,
|
||||
"plain DeepSeek assistant turn missing reasoning_content should keep the field absent"
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
handleComboChat,
|
||||
} from "../../open-sse/services/combo.ts";
|
||||
import { handleComboChat } from "../../open-sse/services/combo.ts";
|
||||
import { getCircuitBreaker, STATE } from "../../src/shared/utils/circuitBreaker.js";
|
||||
|
||||
function okResponse() {
|
||||
@@ -27,14 +25,18 @@ test("#9630: combo returns 503 when circuit breaker is OPEN but other healthy ta
|
||||
strategy: "priority",
|
||||
models: ["openai/gpt-4", "anthropic/claude-opus-5"],
|
||||
},
|
||||
handleSingleModel: async (_body: any, modelStr: string) => {
|
||||
assert.equal(modelStr, "anthropic/claude-opus-5", "should skip openai breaker and try anthropic");
|
||||
handleSingleModel: async (_body, modelStr) => {
|
||||
assert.equal(
|
||||
modelStr,
|
||||
"anthropic/claude-opus-5",
|
||||
"should skip openai breaker and try anthropic"
|
||||
);
|
||||
return okResponse();
|
||||
},
|
||||
isModelAvailable: async () => true,
|
||||
log: { info: () => {}, warn: () => {}, debug: () => {}, error: () => {} } as any,
|
||||
log: { info: () => {}, warn: () => {}, debug: () => {}, error: () => {} },
|
||||
settings: null,
|
||||
relayOptions: null as any,
|
||||
relayOptions: null,
|
||||
allCombos: null,
|
||||
});
|
||||
|
||||
@@ -63,17 +65,22 @@ test("#9630: combo returns truthful error, not false ALL_ACCOUNTS_INACTIVE, when
|
||||
strategy: "priority",
|
||||
models: ["openai/gpt-4", "anthropic/claude-opus-5"],
|
||||
},
|
||||
handleSingleModel: async () => { throw new Error("should not be called"); },
|
||||
handleSingleModel: async () => {
|
||||
throw new Error("should not be called");
|
||||
},
|
||||
isModelAvailable: async () => true,
|
||||
log: { info: () => {}, warn: () => {}, debug: () => {}, error: () => {} } as any,
|
||||
log: { info: () => {}, warn: () => {}, debug: () => {}, error: () => {} },
|
||||
settings: null,
|
||||
relayOptions: null as any,
|
||||
relayOptions: null,
|
||||
allCombos: null,
|
||||
});
|
||||
|
||||
assert.equal(result.status, 503);
|
||||
const body = await result.json();
|
||||
// The diagnostic should NOT claim ALL_ACCOUNTS_INACTIVE when no real dispatch was attempted
|
||||
assert.notEqual(body.error?.code, "ALL_ACCOUNTS_INACTIVE",
|
||||
"should not claim ALL_ACCOUNTS_INACTIVE when all targets were gated by pre-dispatch checks");
|
||||
assert.notEqual(
|
||||
body.error?.code,
|
||||
"ALL_ACCOUNTS_INACTIVE",
|
||||
"should not claim ALL_ACCOUNTS_INACTIVE when all targets were gated by pre-dispatch checks"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -89,17 +89,15 @@ test("#9293 hidden OpenRouter specialty models are excluded from /v1/models cata
|
||||
new Request("http://localhost/v1/models")
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
const body = (await response.json()) as any;
|
||||
const body = (await response.json()) as { data: Array<{ id: string; type?: string }> };
|
||||
assert.ok(Array.isArray(body.data), "response has data array");
|
||||
|
||||
// Find audio and image models
|
||||
const audioModels = body.data.filter((m: any) => m.type === "audio");
|
||||
const imageModels = body.data.filter((m: any) => m.type === "image");
|
||||
const audioModels = body.data.filter((m) => m.type === "audio");
|
||||
const imageModels = body.data.filter((m) => m.type === "image");
|
||||
|
||||
// chirp-3 model ID from the audio registry is openrouter/google/chirp-3
|
||||
const hiddenAudio = audioModels.find((m: any) =>
|
||||
String(m.id).endsWith("google/chirp-3")
|
||||
);
|
||||
const hiddenAudio = audioModels.find((m) => String(m.id).endsWith("google/chirp-3"));
|
||||
assert.equal(
|
||||
hiddenAudio,
|
||||
undefined,
|
||||
@@ -107,7 +105,7 @@ test("#9293 hidden OpenRouter specialty models are excluded from /v1/models cata
|
||||
);
|
||||
|
||||
// flux.2-pro model ID from the image registry is openrouter/black-forest-labs/flux.2-pro
|
||||
const hiddenImage = imageModels.find((m: any) =>
|
||||
const hiddenImage = imageModels.find((m) =>
|
||||
String(m.id).endsWith("black-forest-labs/flux.2-pro")
|
||||
);
|
||||
assert.equal(
|
||||
@@ -118,11 +116,6 @@ test("#9293 hidden OpenRouter specialty models are excluded from /v1/models cata
|
||||
|
||||
// Verify non-hidden audio models from OpenRouter still appear
|
||||
// deepgram/nova-3 is not hidden, so it should be present
|
||||
const visibleAudio = audioModels.find((m: any) =>
|
||||
String(m.id).endsWith("deepgram/nova-3")
|
||||
);
|
||||
assert.ok(
|
||||
visibleAudio,
|
||||
"non-hidden audio model deepgram/nova-3 should still appear in catalog"
|
||||
);
|
||||
});
|
||||
const visibleAudio = audioModels.find((m) => String(m.id).endsWith("deepgram/nova-3"));
|
||||
assert.ok(visibleAudio, "non-hidden audio model deepgram/nova-3 should still appear in catalog");
|
||||
});
|
||||
|
||||
@@ -148,7 +148,7 @@ test("T24: all inactive accounts return 503 service_unavailable (not 406)", asyn
|
||||
|
||||
assert.equal(result.status, 503);
|
||||
const body = (await result.json()) as any;
|
||||
assert.equal(body.error?.code, "ALL_ACCOUNTS_INACTIVE");
|
||||
assert.equal(body.error?.code, "ALL_TARGETS_SKIPPED");
|
||||
});
|
||||
|
||||
test("combo falls through 400s and reaches the next model", async () => {
|
||||
|
||||
@@ -9,9 +9,6 @@ const {
|
||||
injectEmptyReasoningContentForToolCalls,
|
||||
} = await import("../../open-sse/translator/helpers/schemaCoercion.ts");
|
||||
const { translateRequest } = await import("../../open-sse/translator/index.ts");
|
||||
const { NON_ANTHROPIC_THINKING_PLACEHOLDER } = await import(
|
||||
"../../open-sse/translator/helpers/claudeHelper.ts"
|
||||
);
|
||||
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
|
||||
const { clearModelsDevCapabilities, saveModelsDevCapabilities } =
|
||||
await import("../../src/lib/modelsDevSync.ts");
|
||||
@@ -198,7 +195,7 @@ test("tool sanitization: injects empty reasoning_content only for DeepSeek tool-
|
||||
assert.equal(openaiMessages[1].reasoning_content, undefined);
|
||||
});
|
||||
|
||||
test("translateRequest injects reasoning_content for DeepSeek assistant tool calls", () => {
|
||||
test("translateRequest omits reasoning_content for DeepSeek assistant tool calls on cache miss", () => {
|
||||
clearModelsDevCapabilities();
|
||||
saveModelsDevCapabilities({
|
||||
deepseek: {
|
||||
@@ -231,6 +228,10 @@ test("translateRequest injects reasoning_content for DeepSeek assistant tool cal
|
||||
"deepseek"
|
||||
);
|
||||
|
||||
assert.equal(translated.messages[1].reasoning_content, NON_ANTHROPIC_THINKING_PLACEHOLDER);
|
||||
// #9573/#9610: the former NON_ANTHROPIC_THINKING_PLACEHOLDER injection was the root
|
||||
// cause of the echo → empty-stop bug (the model continued its chain of thought from
|
||||
// the placeholder and re-poisoned cache + history). On a cache miss the field is now
|
||||
// omitted; DeepSeek's 400 is specific to an empty string, not an absent field.
|
||||
assert.equal(translated.messages[1].reasoning_content, undefined);
|
||||
clearModelsDevCapabilities();
|
||||
});
|
||||
|
||||
@@ -42,3 +42,53 @@ test("translateRequest replays reasoning_content on plain xiaomi-mimo assistant
|
||||
"plain xiaomi-mimo assistant turn must carry a non-empty reasoning_content"
|
||||
);
|
||||
});
|
||||
|
||||
// Scope guard for the #9573/#9610 <-> 9router#1321 conflict. #9610 removed the
|
||||
// placeholder injection globally on the strength of ONE provider's behavior
|
||||
// (deepseek-v4-flash was verified to accept an absent reasoning_content), which
|
||||
// silently re-broke MiMo. The placeholder is now provider-scoped, so both halves
|
||||
// need pinning: widening the scope back to DeepSeek re-opens #9573, narrowing it
|
||||
// away from MiMo re-opens 9router#1321.
|
||||
test("the reasoning_content placeholder stays scoped: MiMo keeps it, DeepSeek does not (#9573 vs 9router#1321)", () => {
|
||||
const plainHistory = () => ({
|
||||
messages: [
|
||||
{ role: "user", content: "hi" },
|
||||
// Plain assistant turn whose reasoning_content the client stripped.
|
||||
{ role: "assistant", content: "Hello! How can I help?" },
|
||||
{ role: "user", content: "continue" },
|
||||
],
|
||||
});
|
||||
|
||||
const mimo = translateRequest(
|
||||
FORMATS.OPENAI,
|
||||
FORMATS.OPENAI,
|
||||
"mimo-v2.5-pro",
|
||||
plainHistory(),
|
||||
true,
|
||||
null,
|
||||
"xiaomi-mimo"
|
||||
);
|
||||
const mimoAssistant = mimo.messages.find((m) => m.role === "assistant");
|
||||
assert.equal(
|
||||
typeof mimoAssistant.reasoning_content === "string" &&
|
||||
mimoAssistant.reasoning_content.length > 0,
|
||||
true,
|
||||
"MiMo 400s on an absent reasoning_content — the placeholder must survive the cache miss"
|
||||
);
|
||||
|
||||
const deepseek = translateRequest(
|
||||
FORMATS.OPENAI,
|
||||
FORMATS.OPENAI,
|
||||
"deepseek-v4-flash",
|
||||
plainHistory(),
|
||||
true,
|
||||
null,
|
||||
"deepseek"
|
||||
);
|
||||
const deepseekAssistant = deepseek.messages.find((m) => m.role === "assistant");
|
||||
assert.equal(
|
||||
deepseekAssistant.reasoning_content,
|
||||
undefined,
|
||||
"DeepSeek accepts an absent field; sending the placeholder there is the #9573 echo bug"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -128,9 +128,9 @@ test("vscode raw models route exposes native GPT-5.6 IDs and effort tiers", asyn
|
||||
assert.equal(typeof defaultModel.created, "number");
|
||||
assert.equal(defaultModel.owned_by, "codex");
|
||||
assert.equal(defaultModel.name, "Codex GPT 5.6 Sol");
|
||||
assert.equal(defaultModel.context_length, 272000);
|
||||
assert.equal(defaultModel.context_length, 1050000);
|
||||
assert.equal(defaultModel.max_output_tokens, 128000);
|
||||
assert.equal(defaultModel.max_input_tokens, 272000);
|
||||
assert.equal(defaultModel.max_input_tokens, 922000);
|
||||
assert.deepEqual(defaultModel.capabilities, {
|
||||
vision: true,
|
||||
tool_calling: true,
|
||||
|
||||
@@ -255,7 +255,7 @@ test("vscode combos route resolves combo names through Ollama api/show", async (
|
||||
assert.equal(body.model, "show-combo");
|
||||
assert.equal(body.modelfile, "FROM show-combo");
|
||||
assert.equal(body.details.family, "show-combo");
|
||||
assert.equal(body.model_info.context_length, 272000);
|
||||
assert.equal(body.model_info.context_length, 1050000);
|
||||
assert.deepEqual(body.supportsReasoningEffort, ["none", "low", "medium", "high", "xhigh"]);
|
||||
assert.equal(body.model_info.capabilities.reasoning, true);
|
||||
});
|
||||
@@ -290,7 +290,7 @@ test("vscode tokenized combos root route exposes importable combo metadata", asy
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(combo, "expected balanced-load in combo root response");
|
||||
assert.equal(combo.url.includes("/responses#models.ai.azure.com"), true);
|
||||
assert.equal(combo.maxInputTokens, 272000);
|
||||
assert.equal(combo.maxInputTokens, 922000);
|
||||
assert.equal(combo.toolCalling, true);
|
||||
assert.deepEqual(combo.supportsReasoningEffort, ["none", "low", "medium", "high", "xhigh"]);
|
||||
});
|
||||
@@ -767,9 +767,7 @@ test("vscode tokenized tags route only exposes usable canonical chat models", as
|
||||
);
|
||||
assert.ok(
|
||||
!catalogModel.api_format ||
|
||||
["chat-completions", "responses", "openai-responses"].includes(
|
||||
catalogModel.api_format
|
||||
),
|
||||
["chat-completions", "responses", "openai-responses"].includes(catalogModel.api_format),
|
||||
`tag ${tagModel.name} should use a text-generation API format`
|
||||
);
|
||||
assert.ok(
|
||||
@@ -1075,7 +1073,7 @@ test("vscode tokenized api/show route exposes explicit reasoning effort metadata
|
||||
assert.equal(body.configurationSchema?.properties?.reasoningEffort?.default, "low");
|
||||
assert.equal(body.model_info["general.basename"], "Codex GPT 5.6 Sol (Default)");
|
||||
assert.equal(body.model_info["general.architecture"], "codex");
|
||||
assert.equal(body.model_info["codex.context_length"], 272000);
|
||||
assert.equal(body.model_info["codex.context_length"], 1050000);
|
||||
assert.deepEqual(body.model_info.supports_reasoning_effort, [
|
||||
"low",
|
||||
"medium",
|
||||
|
||||
Reference in New Issue
Block a user