chore(release): v3.1.10 — OmniRoute v3.1.9 remaining bug fixes sprint (#720)

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-03-28 19:54:45 -03:00
committed by GitHub
parent 9ffad1005e
commit 437cf9bab0
9 changed files with 42 additions and 32 deletions

View File

@@ -4,6 +4,15 @@
---
## [3.1.10] — 2026-03-28
### 🐛 Bug Fixes
- **Fix #706** — Fixed icon fallback rendering caused by Tailwind V4 `font-sans` override by applying `!important` to `.material-symbols-outlined`.
- **Fix #703** — Fixed GitHub Copilot broken streams by enabling `responses` to `openai` format translation for any custom models leveraging `apiFormat: "responses"`.
- **Fix #702** — Replaced flat-rate usage tracking with accurate DB pricing calculations for both streaming and non-streaming responses.
- **Fix #716** — Cleaned up Claude tool-call translation state, correctly parsing streaming arguments and preventing OpenAI `tool_calls` chunks from repeating the `id` field.
## [3.1.9] — 2026-03-28
### ✨ New Features
@@ -31,12 +40,12 @@
### 📁 New Files
| File | Purpose |
|------|---------|
| `open-sse/translator/helpers/schemaCoercion.ts` | Schema coercion and tool description sanitization utilities |
| `tests/unit/schema-coercion.test.mjs` | Unit tests for schema coercion |
| `tests/unit/t40-opencode-cli-tools-integration.test.mjs` | CLI tool integration tests |
| `COVERAGE_PLAN.md` | Test coverage planning document |
| File | Purpose |
| -------------------------------------------------------- | ----------------------------------------------------------- |
| `open-sse/translator/helpers/schemaCoercion.ts` | Schema coercion and tool description sanitization utilities |
| `tests/unit/schema-coercion.test.mjs` | Unit tests for schema coercion |
| `tests/unit/t40-opencode-cli-tools-integration.test.mjs` | CLI tool integration tests |
| `COVERAGE_PLAN.md` | Test coverage planning document |
## [3.1.8] - 2026-03-27

View File

@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: OmniRoute API
version: 3.1.9
version: 3.1.10
description: |
OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible
endpoint that routes requests to multiple AI providers with load balancing,

View File

@@ -27,6 +27,8 @@ import {
saveCallLog,
} from "@/lib/usageDb";
import { getLoggedInputTokens, getLoggedOutputTokens } from "@/lib/usage/tokenAccounting";
import { recordCost } from "@/domain/costRules";
import { calculateCost } from "@/lib/usage/costCalculator";
import { CLAUDE_OAUTH_TOOL_PREFIX } from "../translator/request/openai-to-claude.ts";
import {
getModelNormalizeToolCallId,
@@ -1315,6 +1317,11 @@ export async function handleChatCore({
});
}
if (apiKeyInfo?.id && usage) {
const estimatedCost = await calculateCost(provider, model, usage);
if (estimatedCost > 0) recordCost(apiKeyInfo.id, estimatedCost);
}
// Translate response to client's expected format (usually OpenAI)
// Pass toolNameMap so Claude OAuth proxy_ prefix is stripped in tool_use blocks (#605)
let translatedResponse = needsTranslation(targetFormat, sourceFormat)
@@ -1447,22 +1454,29 @@ export async function handleChatCore({
apiKeyName: apiKeyInfo?.name || null,
noLog: apiKeyInfo?.noLog === true,
}).catch(() => {});
if (apiKeyInfo?.id && streamUsage) {
calculateCost(provider, model, streamUsage)
.then((estimatedCost) => {
if (estimatedCost > 0) recordCost(apiKeyInfo.id, estimatedCost);
})
.catch(() => {});
}
};
// For Codex provider, translate response from openai-responses to openai (Chat Completions) format
// For providers using Responses API format, translate stream back to openai (Chat Completions) format
// UNLESS client is Droid CLI which expects openai-responses format back
const isDroidCLI =
userAgent?.toLowerCase().includes("droid") || userAgent?.toLowerCase().includes("codex-cli");
const needsCodexTranslation =
provider === "codex" &&
const needsResponsesTranslation =
targetFormat === FORMATS.OPENAI_RESPONSES &&
sourceFormat === FORMATS.OPENAI &&
!isResponsesEndpoint &&
!isDroidCLI;
if (needsCodexTranslation) {
// Codex returns openai-responses, translate to openai (Chat Completions) that clients expect
log?.debug?.("STREAM", `Codex translation mode: openai-responses → openai`);
if (needsResponsesTranslation) {
// Provider returns openai-responses, translate to openai (Chat Completions) that clients expect
log?.debug?.("STREAM", `Responses translation mode: openai-responses → openai`);
transformStream = createSSETransformStreamWithLogger(
"openai-responses",
"openai",

View File

@@ -90,7 +90,6 @@ export function claudeToOpenAIResponse(chunk, state) {
tool_calls: [
{
index: toolCall.index,
id: toolCall.id,
function: { arguments: delta.partial_json },
},
],

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "omniroute",
"version": "3.1.9",
"version": "3.1.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "omniroute",
"version": "3.1.9",
"version": "3.1.10",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [

View File

@@ -1,6 +1,6 @@
{
"name": "omniroute",
"version": "3.1.9",
"version": "3.1.10",
"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": {

View File

@@ -219,7 +219,7 @@ body {
/* Material Symbols */
.material-symbols-outlined {
font-family: "Material Symbols Outlined", sans-serif;
font-family: "Material Symbols Outlined", sans-serif !important;
font-weight: normal;
font-style: normal;
font-size: 24px;

View File

@@ -42,7 +42,6 @@ import {
import { markAccountExhaustedFrom429 } from "../../domain/quotaCache";
import { RequestTelemetry, recordTelemetry } from "../../shared/utils/requestTelemetry";
import { generateRequestId } from "../../shared/utils/requestId";
import { recordCost } from "../../domain/costRules";
import { logAuditEvent } from "../../lib/compliance/index";
import { enforceApiKeyPolicy } from "../../shared/utils/apiKeyPolicy";
import {
@@ -421,7 +420,6 @@ async function handleSingleModelChat(
if (result.success) {
clearModelUnavailability(provider, model);
recordCostIfNeeded(apiKeyInfo, result);
if (telemetry) telemetry.startPhase("finalize");
if (telemetry) telemetry.endPhase();
return result.response;
@@ -670,18 +668,6 @@ async function executeChatWithBreaker({
}
}
/**
* Record cost if API key has budget tracking enabled.
*/
function recordCostIfNeeded(apiKeyInfo: any, result: any) {
if (!apiKeyInfo?.id) return;
try {
const usage = result.usage || {};
const estimatedCost = ((usage.prompt_tokens || 0) + (usage.completion_tokens || 0)) * 0.000001;
if (estimatedCost > 0) recordCost(apiKeyInfo.id, estimatedCost);
} catch {}
}
// ──── Extracted helpers (T-28) ────
function handleNoCredentials(

View File

@@ -79,6 +79,8 @@ test("updateModelComboMapping merges fields and returns the refreshed mapping",
priority: 1,
});
await new Promise((r) => setTimeout(r, 10));
const updated = await mappingsDb.updateModelComboMapping(created.id, {
pattern: "claude-*",
comboId: comboB.id,