Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70465ada4d | |||
| 8ddea153d3 | |||
| 8dca8fba6b | |||
| f21ba7df64 | |||
| ef917e42d1 | |||
| 865a1b9b2c | |||
| de8a0836a8 | |||
| b8272c55d7 | |||
| 8d93c13f9a | |||
| 8152b030bf | |||
| 9352ac767f | |||
| 5f20029ff7 | |||
| dbd00117c8 |
@@ -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,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
---
|
||||
|
||||
## [1.8.1] — 2026-03-03
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **Usage API Proxy Support** — Quota/usage fetch calls (`/api/usage/[connectionId]`) now route through the dashboard-configured proxy (Global → Provider → Key level). Previously, usage fetchers used bare `fetch()` which bypassed the Global Proxy setting, causing "fetch failed" errors in Docker deployments behind a proxy. Fixes #194
|
||||
|
||||
## [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
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "1.7.12",
|
||||
"version": "1.8.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "omniroute",
|
||||
"version": "1.7.12",
|
||||
"version": "1.8.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "1.7.12",
|
||||
"version": "1.8.1",
|
||||
"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": {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { getProviderConnectionById, updateProviderConnection } from "@/lib/localDb";
|
||||
import { getProviderConnectionById, updateProviderConnection, resolveProxyForConnection } from "@/lib/localDb";
|
||||
import { getMachineId } from "@/shared/utils/machine";
|
||||
import { getUsageForProvider } from "@omniroute/open-sse/services/usage.ts";
|
||||
import { getExecutor } from "@omniroute/open-sse/executors/index.ts";
|
||||
import { syncToCloud } from "@/lib/cloudSync";
|
||||
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
|
||||
|
||||
/**
|
||||
* Sync to cloud if enabled
|
||||
@@ -139,8 +140,13 @@ export async function GET(request: Request, { params }: { params: Promise<{ conn
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch usage from provider API
|
||||
const usage = await getUsageForProvider(connection);
|
||||
// Resolve proxy for this connection (key → combo → provider → global → direct)
|
||||
const proxyInfo = await resolveProxyForConnection(connectionId);
|
||||
|
||||
// Fetch usage from provider API, wrapped in proxy context
|
||||
const usage = await runWithProxyContext(proxyInfo?.proxy || null, () =>
|
||||
getUsageForProvider(connection)
|
||||
);
|
||||
return Response.json(usage);
|
||||
} catch (error) {
|
||||
console.error("[Usage API] Error fetching usage:", error);
|
||||
|
||||
Reference in New Issue
Block a user