Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f21ba7df64 | |||
| ef917e42d1 | |||
| 865a1b9b2c | |||
| de8a0836a8 | |||
| b8272c55d7 | |||
| 8d93c13f9a | |||
| 8152b030bf | |||
| 9352ac767f | |||
| 5f20029ff7 | |||
| dbd00117c8 | |||
| 2902a0fe26 | |||
| 7ba57634c1 |
@@ -110,9 +110,13 @@ jobs:
|
||||
for file in *${{ matrix.ext }}; do
|
||||
[ -f "$file" ] && cp "$file" ../../release-assets/
|
||||
done
|
||||
# Windows: also copy portable standalone exe
|
||||
# Windows: also copy portable standalone exe as OmniRoute.exe
|
||||
if [ "${{ matrix.platform }}" = "windows" ]; then
|
||||
[ -f "OmniRoute.exe" ] && cp OmniRoute.exe ../../release-assets/
|
||||
for file in *.exe; do
|
||||
# Skip the NSIS installer (contains "Setup")
|
||||
case "$file" in *Setup*) continue ;; esac
|
||||
[ -f "$file" ] && cp "$file" "../../release-assets/OmniRoute.exe" && break
|
||||
done
|
||||
fi
|
||||
|
||||
- name: Upload artifacts
|
||||
|
||||
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
---
|
||||
|
||||
## [1.8.0] — 2026-03-03
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **Empty `tool_use.name` Validation** — Fixed intermittent HTTP 400 errors when using Claude Code through OmniRoute. Assistant messages with empty `tool_use.name` fields (from interrupted tool calls or malformed history) are now validated and filtered at two layers: the `openai-to-claude` request translator and the `prepareClaudeRequest` sanitizer. Fixes #191
|
||||
- **Windows Electron Release** — Fixed the "Collect installers" step failing in every Windows build since v1.7.5+. `electron-builder` produces versioned portable exe filenames (e.g., `OmniRoute 1.6.9.exe`), not the hardcoded `OmniRoute.exe` the workflow expected. Now finds the portable exe dynamically by pattern. PR #190 by @benzntech
|
||||
|
||||
## [1.7.14] — 2026-03-02
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **Responses SSE Passthrough** — Passthrough mode is now format-aware: Responses SSE payloads (`response.*` type) skip Chat Completions-specific sanitization (`sanitizeStreamingChunk`, `fixInvalidId`, `hasValuableContent`), preventing potential stream corruption for Responses-native clients. Usage extraction still works for both formats. Fixes #186
|
||||
|
||||
### ✨ Features
|
||||
|
||||
- **Blackbox AI Dashboard** — Added blackbox.ai provider to the dashboard frontend (providers page, pricing, models endpoint). Completes #175
|
||||
|
||||
## [1.7.11] — 2026-03-02
|
||||
|
||||
### ✨ Features
|
||||
|
||||
@@ -126,6 +126,15 @@ export function prepareClaudeRequest(body, provider = null) {
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 1.4: Filter out tool_use blocks with empty names (causes Claude 400 error)
|
||||
for (const msg of filtered) {
|
||||
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
||||
msg.content = msg.content.filter(
|
||||
(block) => block.type !== "tool_use" || (block.name && block.name.trim())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 1.5: Fix tool_use/tool_result ordering
|
||||
// Each tool_use must have tool_result in the NEXT message (not same message with other content)
|
||||
filtered = fixToolUseOrdering(filtered);
|
||||
|
||||
@@ -228,7 +228,10 @@ function getContentBlocksFromMessage(msg, toolNameMap = new Map()) {
|
||||
});
|
||||
} else if (part.type === "tool_use") {
|
||||
// Tool name already has prefix from tool declarations, keep as-is
|
||||
blocks.push({ type: "tool_use", id: part.id, name: part.name, input: part.input });
|
||||
// CRITICAL: Skip tool_use blocks with empty name (causes Claude 400 error)
|
||||
if (part.name && part.name.trim()) {
|
||||
blocks.push({ type: "tool_use", id: part.id, name: part.name, input: part.input });
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (msg.content) {
|
||||
@@ -241,8 +244,12 @@ function getContentBlocksFromMessage(msg, toolNameMap = new Map()) {
|
||||
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
|
||||
for (const tc of msg.tool_calls) {
|
||||
if (tc.type === "function") {
|
||||
// CRITICAL: Skip tool_calls with empty function name (causes Claude 400 error)
|
||||
const fnName = tc.function?.name;
|
||||
if (!fnName || !fnName.trim()) continue;
|
||||
|
||||
// Apply prefix to tool name
|
||||
const toolName = CLAUDE_OAUTH_TOOL_PREFIX + tc.function.name;
|
||||
const toolName = CLAUDE_OAUTH_TOOL_PREFIX + fnName;
|
||||
blocks.push({
|
||||
type: "tool_use",
|
||||
id: tc.id,
|
||||
|
||||
+60
-40
@@ -136,51 +136,71 @@ export function createSSEStream(options: any = {}) {
|
||||
try {
|
||||
let parsed = JSON.parse(trimmed.slice(5).trim());
|
||||
|
||||
// Sanitize: strip non-standard fields for OpenAI SDK compatibility
|
||||
parsed = sanitizeStreamingChunk(parsed);
|
||||
// Detect Responses SSE payloads (have a `type` field like "response.created",
|
||||
// "response.output_item.added", etc.) and skip Chat Completions-specific
|
||||
// sanitization to avoid corrupting the stream for Responses-native clients.
|
||||
const isResponsesSSE =
|
||||
parsed.type &&
|
||||
typeof parsed.type === "string" &&
|
||||
parsed.type.startsWith("response.");
|
||||
|
||||
const idFixed = fixInvalidId(parsed);
|
||||
|
||||
if (!hasValuableContent(parsed, FORMATS.OPENAI)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const delta = parsed.choices?.[0]?.delta;
|
||||
|
||||
// Extract <think> tags from streaming content
|
||||
if (delta?.content && typeof delta.content === "string") {
|
||||
const { content, thinking } = extractThinkingFromContent(delta.content);
|
||||
delta.content = content;
|
||||
if (thinking && !delta.reasoning_content) {
|
||||
delta.reasoning_content = thinking;
|
||||
if (isResponsesSSE) {
|
||||
// Responses SSE: only extract usage, forward payload as-is
|
||||
const extracted = extractUsage(parsed);
|
||||
if (extracted) {
|
||||
usage = extracted;
|
||||
}
|
||||
}
|
||||
// Track content length from Responses format
|
||||
if (parsed.delta && typeof parsed.delta === "string") {
|
||||
totalContentLength += parsed.delta.length;
|
||||
}
|
||||
} else {
|
||||
// Chat Completions: full sanitization pipeline
|
||||
parsed = sanitizeStreamingChunk(parsed);
|
||||
|
||||
const content = delta?.content || delta?.reasoning_content;
|
||||
if (content && typeof content === "string") {
|
||||
totalContentLength += content.length;
|
||||
}
|
||||
const idFixed = fixInvalidId(parsed);
|
||||
|
||||
const extracted = extractUsage(parsed);
|
||||
if (extracted) {
|
||||
usage = extracted;
|
||||
}
|
||||
if (!hasValuableContent(parsed, FORMATS.OPENAI)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isFinishChunk = parsed.choices?.[0]?.finish_reason;
|
||||
if (isFinishChunk && !hasValidUsage(parsed.usage)) {
|
||||
const estimated = estimateUsage(body, totalContentLength, FORMATS.OPENAI);
|
||||
parsed.usage = filterUsageForFormat(estimated, FORMATS.OPENAI);
|
||||
output = `data: ${JSON.stringify(parsed)}\n`;
|
||||
usage = estimated;
|
||||
injectedUsage = true;
|
||||
} else if (isFinishChunk && usage) {
|
||||
const buffered = addBufferToUsage(usage);
|
||||
parsed.usage = filterUsageForFormat(buffered, FORMATS.OPENAI);
|
||||
output = `data: ${JSON.stringify(parsed)}\n`;
|
||||
injectedUsage = true;
|
||||
} else if (idFixed) {
|
||||
output = `data: ${JSON.stringify(parsed)}\n`;
|
||||
injectedUsage = true;
|
||||
const delta = parsed.choices?.[0]?.delta;
|
||||
|
||||
// Extract <think> tags from streaming content
|
||||
if (delta?.content && typeof delta.content === "string") {
|
||||
const { content, thinking } = extractThinkingFromContent(delta.content);
|
||||
delta.content = content;
|
||||
if (thinking && !delta.reasoning_content) {
|
||||
delta.reasoning_content = thinking;
|
||||
}
|
||||
}
|
||||
|
||||
const content = delta?.content || delta?.reasoning_content;
|
||||
if (content && typeof content === "string") {
|
||||
totalContentLength += content.length;
|
||||
}
|
||||
|
||||
const extracted = extractUsage(parsed);
|
||||
if (extracted) {
|
||||
usage = extracted;
|
||||
}
|
||||
|
||||
const isFinishChunk = parsed.choices?.[0]?.finish_reason;
|
||||
if (isFinishChunk && !hasValidUsage(parsed.usage)) {
|
||||
const estimated = estimateUsage(body, totalContentLength, FORMATS.OPENAI);
|
||||
parsed.usage = filterUsageForFormat(estimated, FORMATS.OPENAI);
|
||||
output = `data: ${JSON.stringify(parsed)}\n`;
|
||||
usage = estimated;
|
||||
injectedUsage = true;
|
||||
} else if (isFinishChunk && usage) {
|
||||
const buffered = addBufferToUsage(usage);
|
||||
parsed.usage = filterUsageForFormat(buffered, FORMATS.OPENAI);
|
||||
output = `data: ${JSON.stringify(parsed)}\n`;
|
||||
injectedUsage = true;
|
||||
} else if (idFixed) {
|
||||
output = `data: ${JSON.stringify(parsed)}\n`;
|
||||
injectedUsage = true;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
Generated
+14
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "1.7.11",
|
||||
"version": "1.7.14",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "omniroute",
|
||||
"version": "1.7.11",
|
||||
"version": "1.7.14",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
@@ -6180,6 +6180,7 @@
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -8267,6 +8268,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-intl/node_modules/@swc/helpers": {
|
||||
"version": "0.5.19",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz",
|
||||
"integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/next/node_modules/postcss": {
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "1.7.11",
|
||||
"version": "1.8.0",
|
||||
"description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
||||
@@ -143,6 +143,14 @@ const PROVIDER_MODELS_CONFIG = {
|
||||
authPrefix: "Bearer ",
|
||||
parseResponse: (data) => data.data || data.models || [],
|
||||
},
|
||||
blackbox: {
|
||||
url: "https://api.blackbox.ai/v1/models",
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
authHeader: "Authorization",
|
||||
authPrefix: "Bearer ",
|
||||
parseResponse: (data) => data.data || data.models || [],
|
||||
},
|
||||
xai: {
|
||||
url: "https://api.x.ai/v1/models",
|
||||
method: "GET",
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {'openai'|'claude'|'gemini'|'codex'|'qwen'|'deepseek'|'cohere'|'groq'|'mistral'|'openrouter'} ProviderId
|
||||
* @typedef {'openai'|'claude'|'gemini'|'codex'|'qwen'|'deepseek'|'cohere'|'groq'|'blackbox'|'mistral'|'openrouter'} ProviderId
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -528,6 +528,16 @@ export const DEFAULT_PRICING = {
|
||||
"qwen/qwen3-32b": { input: 0, output: 0, cached: 0, reasoning: 0, cache_creation: 0 },
|
||||
},
|
||||
|
||||
// Blackbox AI
|
||||
blackbox: {
|
||||
"gpt-4o": { input: 0, output: 0, cached: 0, reasoning: 0, cache_creation: 0 },
|
||||
"gemini-2.5-flash": { input: 0, output: 0, cached: 0, reasoning: 0, cache_creation: 0 },
|
||||
"claude-sonnet-4": { input: 0, output: 0, cached: 0, reasoning: 0, cache_creation: 0 },
|
||||
"deepseek-v3": { input: 0, output: 0, cached: 0, reasoning: 0, cache_creation: 0 },
|
||||
blackboxai: { input: 0, output: 0, cached: 0, reasoning: 0, cache_creation: 0 },
|
||||
"blackboxai-pro": { input: 0, output: 0, cached: 0, reasoning: 0, cache_creation: 0 },
|
||||
},
|
||||
|
||||
// Fireworks
|
||||
fireworks: {
|
||||
"accounts/fireworks/models/gpt-oss-120b": {
|
||||
|
||||
@@ -145,6 +145,15 @@ export const APIKEY_PROVIDERS = {
|
||||
textIcon: "GQ",
|
||||
website: "https://groq.com",
|
||||
},
|
||||
blackbox: {
|
||||
id: "blackbox",
|
||||
alias: "bb",
|
||||
name: "Blackbox AI",
|
||||
icon: "view_in_ar",
|
||||
color: "#1A1A2E",
|
||||
textIcon: "BB",
|
||||
website: "https://blackbox.ai",
|
||||
},
|
||||
xai: {
|
||||
id: "xai",
|
||||
alias: "xai",
|
||||
|
||||
Reference in New Issue
Block a user