mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 14:52:09 +03:00
Merge pull request #633 from diegosouzapw/release/v3.0.8
chore(release): v3.0.8 — fix translation failures for OpenAI-format providers (#632)
This commit is contained in:
13
CHANGELOG.md
13
CHANGELOG.md
@@ -4,6 +4,19 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.0.8] — 2026-03-25
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **Translation Failures for OpenAI-format Providers in Claude CLI (#632):**
|
||||
- Handle `reasoning_details[]` array format from StepFun/OpenRouter — converts to `reasoning_content`
|
||||
- Handle `reasoning` field alias from some providers → normalized to `reasoning_content`
|
||||
- Cross-map usage field names: `input_tokens`↔`prompt_tokens`, `output_tokens`↔`completion_tokens` in `filterUsageForFormat`
|
||||
- Fix `extractUsage` to accept both `input_tokens`/`output_tokens` and `prompt_tokens`/`completion_tokens` as valid usage fields
|
||||
- Applied to both streaming (`sanitizeStreamingChunk`, `openai-to-claude.ts` translator) and non-streaming (`sanitizeMessage`) paths
|
||||
|
||||
---
|
||||
|
||||
## [3.0.7] — 2026-03-25
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: OmniRoute API
|
||||
version: 3.0.7
|
||||
version: 3.0.8
|
||||
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,
|
||||
|
||||
@@ -172,6 +172,44 @@ function sanitizeMessage(msg: unknown): unknown {
|
||||
sanitized.reasoning_content = msgRecord.reasoning_content;
|
||||
}
|
||||
|
||||
// Handle 'reasoning' field alias (some providers use this instead of reasoning_content)
|
||||
if (
|
||||
msgRecord.reasoning &&
|
||||
typeof msgRecord.reasoning === "string" &&
|
||||
!sanitized.reasoning_content
|
||||
) {
|
||||
sanitized.reasoning_content = msgRecord.reasoning;
|
||||
}
|
||||
|
||||
// Handle reasoning_details[] array (StepFun/OpenRouter format)
|
||||
// Structure: [{ type: "reasoning.text", text: "...", format: "unknown", index: 0 }]
|
||||
if (Array.isArray(msgRecord.reasoning_details) && !sanitized.reasoning_content) {
|
||||
const reasoningParts: string[] = [];
|
||||
for (const detail of msgRecord.reasoning_details) {
|
||||
const detailObj = detail && typeof detail === "object" ? (detail as JsonRecord) : null;
|
||||
if (!detailObj) continue;
|
||||
const detailType = typeof detailObj.type === "string" ? detailObj.type : "";
|
||||
const detailText =
|
||||
typeof detailObj.text === "string"
|
||||
? detailObj.text
|
||||
: typeof detailObj.content === "string"
|
||||
? detailObj.content
|
||||
: "";
|
||||
if (
|
||||
detailText &&
|
||||
(detailType === "reasoning" ||
|
||||
detailType === "reasoning.text" ||
|
||||
detailType === "thinking" ||
|
||||
detailType === "")
|
||||
) {
|
||||
reasoningParts.push(detailText);
|
||||
}
|
||||
}
|
||||
if (reasoningParts.length > 0) {
|
||||
sanitized.reasoning_content = reasoningParts.join("");
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve tool_calls
|
||||
if (msgRecord.tool_calls) {
|
||||
sanitized.tool_calls = msgRecord.tool_calls;
|
||||
@@ -258,6 +296,26 @@ export function sanitizeStreamingChunk(parsed: unknown): unknown {
|
||||
if (deltaRecord.content !== undefined) delta.content = deltaRecord.content;
|
||||
if (deltaRecord.reasoning_content !== undefined) {
|
||||
delta.reasoning_content = deltaRecord.reasoning_content;
|
||||
} else if (typeof deltaRecord.reasoning === "string" && deltaRecord.reasoning) {
|
||||
// Alias: some providers use 'reasoning' instead of 'reasoning_content'
|
||||
delta.reasoning_content = deltaRecord.reasoning;
|
||||
} else if (Array.isArray(deltaRecord.reasoning_details)) {
|
||||
// StepFun/OpenRouter: reasoning_details[{type:"reasoning.text", text:"..."}]
|
||||
const parts: string[] = [];
|
||||
for (const detail of deltaRecord.reasoning_details) {
|
||||
const d = detail && typeof detail === "object" ? (detail as JsonRecord) : null;
|
||||
if (!d) continue;
|
||||
const text =
|
||||
typeof d.text === "string"
|
||||
? d.text
|
||||
: typeof d.content === "string"
|
||||
? d.content
|
||||
: "";
|
||||
if (text) parts.push(text);
|
||||
}
|
||||
if (parts.length > 0) {
|
||||
delta.reasoning_content = parts.join("");
|
||||
}
|
||||
}
|
||||
if (deltaRecord.tool_calls !== undefined) delta.tool_calls = deltaRecord.tool_calls;
|
||||
if (deltaRecord.function_call !== undefined)
|
||||
|
||||
@@ -93,7 +93,18 @@ export function openaiToClaudeResponse(chunk, state) {
|
||||
}
|
||||
|
||||
// Handle reasoning_content (thinking) - GLM, DeepSeek, etc.
|
||||
const reasoningContent = delta?.reasoning_content || delta?.reasoning;
|
||||
// Also supports 'reasoning' field alias and reasoning_details[] (StepFun/OpenRouter)
|
||||
let reasoningContent = delta?.reasoning_content || delta?.reasoning;
|
||||
if (!reasoningContent && Array.isArray(delta?.reasoning_details)) {
|
||||
const parts: string[] = [];
|
||||
for (const detail of delta.reasoning_details) {
|
||||
if (detail && typeof detail === "object") {
|
||||
const text = detail.text || detail.content;
|
||||
if (typeof text === "string" && text) parts.push(text);
|
||||
}
|
||||
}
|
||||
if (parts.length > 0) reasoningContent = parts.join("");
|
||||
}
|
||||
if (reasoningContent) {
|
||||
stopTextBlock(state, results);
|
||||
|
||||
|
||||
@@ -66,12 +66,47 @@ export function addBufferToUsage(usage) {
|
||||
export function filterUsageForFormat(usage, targetFormat) {
|
||||
if (!usage || typeof usage !== "object") return usage;
|
||||
|
||||
// Cross-map between Claude-style and OpenAI-style field names before filtering.
|
||||
// Some providers return input_tokens/output_tokens even when using OpenAI format.
|
||||
const convertedUsage = { ...usage };
|
||||
if (targetFormat === FORMATS.CLAUDE || targetFormat === FORMATS.OPENAI_RESPONSES) {
|
||||
// OpenAI → Claude: prompt_tokens → input_tokens
|
||||
if (convertedUsage.prompt_tokens !== undefined && convertedUsage.input_tokens === undefined) {
|
||||
convertedUsage.input_tokens = convertedUsage.prompt_tokens;
|
||||
}
|
||||
if (
|
||||
convertedUsage.completion_tokens !== undefined &&
|
||||
convertedUsage.output_tokens === undefined
|
||||
) {
|
||||
convertedUsage.output_tokens = convertedUsage.completion_tokens;
|
||||
}
|
||||
} else {
|
||||
// Claude → OpenAI: input_tokens → prompt_tokens
|
||||
if (convertedUsage.input_tokens !== undefined && convertedUsage.prompt_tokens === undefined) {
|
||||
convertedUsage.prompt_tokens = convertedUsage.input_tokens;
|
||||
}
|
||||
if (
|
||||
convertedUsage.output_tokens !== undefined &&
|
||||
convertedUsage.completion_tokens === undefined
|
||||
) {
|
||||
convertedUsage.completion_tokens = convertedUsage.output_tokens;
|
||||
}
|
||||
// Ensure total_tokens is set
|
||||
if (
|
||||
convertedUsage.total_tokens === undefined &&
|
||||
convertedUsage.prompt_tokens !== undefined &&
|
||||
convertedUsage.completion_tokens !== undefined
|
||||
) {
|
||||
convertedUsage.total_tokens = convertedUsage.prompt_tokens + convertedUsage.completion_tokens;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to pick only defined fields from usage
|
||||
const pickFields = (fields) => {
|
||||
const filtered = {};
|
||||
for (const field of fields) {
|
||||
if (usage[field] !== undefined) {
|
||||
filtered[field] = usage[field];
|
||||
if (convertedUsage[field] !== undefined) {
|
||||
filtered[field] = convertedUsage[field];
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
@@ -230,10 +265,14 @@ export function extractUsage(chunk) {
|
||||
}
|
||||
|
||||
// OpenAI format
|
||||
if (chunk.usage && typeof chunk.usage === "object" && chunk.usage.prompt_tokens !== undefined) {
|
||||
if (
|
||||
chunk.usage &&
|
||||
typeof chunk.usage === "object" &&
|
||||
(chunk.usage.prompt_tokens !== undefined || chunk.usage.input_tokens !== undefined)
|
||||
) {
|
||||
return normalizeUsage({
|
||||
prompt_tokens: chunk.usage.prompt_tokens,
|
||||
completion_tokens: chunk.usage.completion_tokens || 0,
|
||||
prompt_tokens: chunk.usage.prompt_tokens ?? chunk.usage.input_tokens ?? 0,
|
||||
completion_tokens: chunk.usage.completion_tokens ?? chunk.usage.output_tokens ?? 0,
|
||||
cached_tokens: chunk.usage.prompt_tokens_details?.cached_tokens,
|
||||
reasoning_tokens: chunk.usage.completion_tokens_details?.reasoning_tokens,
|
||||
});
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "3.0.7",
|
||||
"version": "3.0.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "omniroute",
|
||||
"version": "3.0.7",
|
||||
"version": "3.0.8",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "3.0.7",
|
||||
"version": "3.0.8",
|
||||
"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": {
|
||||
|
||||
@@ -27,21 +27,21 @@ export async function getCombos() {
|
||||
.map((row) => JSON.parse(row));
|
||||
}
|
||||
|
||||
export async function getComboById(id) {
|
||||
export async function getComboById(id: string) {
|
||||
const db = getDbInstance();
|
||||
const row = db.prepare("SELECT data FROM combos WHERE id = ?").get(id);
|
||||
const payload = getSerializedData(row);
|
||||
return payload ? JSON.parse(payload) : null;
|
||||
}
|
||||
|
||||
export async function getComboByName(name) {
|
||||
export async function getComboByName(name: string) {
|
||||
const db = getDbInstance();
|
||||
const row = db.prepare("SELECT data FROM combos WHERE name = ?").get(name);
|
||||
const payload = getSerializedData(row);
|
||||
return payload ? JSON.parse(payload) : null;
|
||||
}
|
||||
|
||||
export async function createCombo(data) {
|
||||
export async function createCombo(data: JsonRecord) {
|
||||
const db = getDbInstance();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
@@ -63,7 +63,7 @@ export async function createCombo(data) {
|
||||
return combo;
|
||||
}
|
||||
|
||||
export async function updateCombo(id, data) {
|
||||
export async function updateCombo(id: string, data: JsonRecord) {
|
||||
const db = getDbInstance();
|
||||
const existing = db.prepare("SELECT data FROM combos WHERE id = ?").get(id);
|
||||
if (!existing) return null;
|
||||
@@ -84,7 +84,7 @@ export async function updateCombo(id, data) {
|
||||
return merged;
|
||||
}
|
||||
|
||||
export async function deleteCombo(id) {
|
||||
export async function deleteCombo(id: string) {
|
||||
const db = getDbInstance();
|
||||
const result = db.prepare("DELETE FROM combos WHERE id = ?").run(id);
|
||||
if (result.changes === 0) return false;
|
||||
|
||||
@@ -247,7 +247,7 @@ export async function getModelAliases() {
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function setModelAlias(alias, model) {
|
||||
export async function setModelAlias(alias: string, model: unknown) {
|
||||
const db = getDbInstance();
|
||||
db.prepare(
|
||||
"INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('modelAliases', ?, ?)"
|
||||
@@ -255,7 +255,7 @@ export async function setModelAlias(alias, model) {
|
||||
backupDbFile("pre-write");
|
||||
}
|
||||
|
||||
export async function deleteModelAlias(alias) {
|
||||
export async function deleteModelAlias(alias: string) {
|
||||
const db = getDbInstance();
|
||||
db.prepare("DELETE FROM key_value WHERE namespace = 'modelAliases' AND key = ?").run(alias);
|
||||
backupDbFile("pre-write");
|
||||
@@ -263,7 +263,7 @@ export async function deleteModelAlias(alias) {
|
||||
|
||||
// ──────────────── MITM Alias ────────────────
|
||||
|
||||
export async function getMitmAlias(toolName) {
|
||||
export async function getMitmAlias(toolName?: string) {
|
||||
const db = getDbInstance();
|
||||
if (toolName) {
|
||||
const row = db
|
||||
@@ -282,7 +282,7 @@ export async function getMitmAlias(toolName) {
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function setMitmAliasAll(toolName, mappings) {
|
||||
export async function setMitmAliasAll(toolName: string, mappings: unknown) {
|
||||
const db = getDbInstance();
|
||||
db.prepare(
|
||||
"INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('mitmAlias', ?, ?)"
|
||||
@@ -292,7 +292,7 @@ export async function setMitmAliasAll(toolName, mappings) {
|
||||
|
||||
// ──────────────── Custom Models ────────────────
|
||||
|
||||
export async function getCustomModels(providerId) {
|
||||
export async function getCustomModels(providerId?: string) {
|
||||
const db = getDbInstance();
|
||||
if (providerId) {
|
||||
const row = db
|
||||
@@ -342,7 +342,7 @@ export async function addCustomModel(
|
||||
const value = getKeyValue(row).value;
|
||||
const models = value ? JSON.parse(value) : [];
|
||||
|
||||
const exists = models.find((m) => m.id === modelId);
|
||||
const exists = models.find((m: JsonRecord) => m.id === modelId);
|
||||
if (exists) return exists;
|
||||
|
||||
const model = {
|
||||
@@ -430,7 +430,7 @@ export async function replaceCustomModels(
|
||||
return merged;
|
||||
}
|
||||
|
||||
export async function removeCustomModel(providerId, modelId) {
|
||||
export async function removeCustomModel(providerId: string, modelId: string) {
|
||||
const db = getDbInstance();
|
||||
const row = db
|
||||
.prepare("SELECT value FROM key_value WHERE namespace = 'customModels' AND key = ?")
|
||||
@@ -441,7 +441,7 @@ export async function removeCustomModel(providerId, modelId) {
|
||||
if (!value) return false;
|
||||
const models = JSON.parse(value);
|
||||
const before = models.length;
|
||||
const filtered = models.filter((m) => m.id !== modelId);
|
||||
const filtered = models.filter((m: JsonRecord) => m.id !== modelId);
|
||||
|
||||
if (filtered.length === before) return false;
|
||||
|
||||
@@ -476,7 +476,7 @@ export async function updateCustomModel(
|
||||
if (!value) return null;
|
||||
|
||||
const models = JSON.parse(value);
|
||||
const index = models.findIndex((m) => m.id === modelId);
|
||||
const index = models.findIndex((m: JsonRecord) => m.id === modelId);
|
||||
if (index === -1) return null;
|
||||
|
||||
const current = models[index];
|
||||
|
||||
Reference in New Issue
Block a user