mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 09:42:15 +03:00
Compare commits
6 Commits
compressio
...
v3.2.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fb72b973a | ||
|
|
7b1c945324 | ||
|
|
1a7c363413 | ||
|
|
107a572174 | ||
|
|
89bd31c670 | ||
|
|
2e29e45a3a |
19
CHANGELOG.md
19
CHANGELOG.md
@@ -2,6 +2,25 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [3.2.8] - 2026-03-29
|
||||||
|
|
||||||
|
### ✨ Enhancements & Refactoring
|
||||||
|
|
||||||
|
- **Docker Auto-Update UI** — Integrated a detached background update process for Docker Compose deployments. The Dashboard UI now seamlessly tracks update lifecycle events combining JSON REST responses with SSE streaming progress overlays for robust cross-environment reliability.
|
||||||
|
- **Cache Analytics** — Repaired zero-metrics visualization mapping by migrating Semantic Cache telemetry logs directly into the centralized tracking SQLite module.
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **Authentication Logic** — Fixed a bug where saving dashboard settings or adding models failed with a 401 Unauthorized error when `requireLogin` was disabled. API endpoints now correctly evaluate the global authentication toggle. Resolved global redirection by reactivating `src/middleware.ts`.
|
||||||
|
- **CLI Tool Detection (Windows)** — Prevented fatal initialization exceptions during CLI environment detection by catching `cross-spawn` ENOENT errors correctly. Adds explicit detection paths for `\AppData\Local\droid\droid.exe`.
|
||||||
|
- **Codex Native Passthrough** — Normalized model translation parameters preventing context poisoning in proxy pass-through mode, enforcing generic `store: false` constraints explicitly for all Codex-originated requests.
|
||||||
|
- **SSE Token Reporting** — Normalized provider tool-call chunk `finish_reason` detection, fixing 0% Usage analytics for stream-only responses missing strict `<DONE>` indicators.
|
||||||
|
- **DeepSeek <think> Tags** — Implemented an explicit `<think>` extraction mapping inside `responsesHandler.ts`, ensuring DeepSeek reasoning streams map equivalently to native Anthropic `<thinking>` structures.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [3.2.7] - 2026-03-29
|
## [3.2.7] - 2026-03-29
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
openapi: 3.1.0
|
openapi: 3.1.0
|
||||||
info:
|
info:
|
||||||
title: OmniRoute API
|
title: OmniRoute API
|
||||||
version: 3.2.7
|
version: 3.2.8
|
||||||
description: |
|
description: |
|
||||||
OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible
|
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,
|
endpoint that routes requests to multiple AI providers with load balancing,
|
||||||
|
|||||||
@@ -43,13 +43,8 @@ import {
|
|||||||
} from "@/lib/localDb";
|
} from "@/lib/localDb";
|
||||||
import { getExecutor } from "../executors/index.ts";
|
import { getExecutor } from "../executors/index.ts";
|
||||||
import { getCacheControlSettings } from "@/lib/cacheControlSettings";
|
import { getCacheControlSettings } from "@/lib/cacheControlSettings";
|
||||||
import {
|
import { shouldPreserveCacheControl, recordCacheHit } from "../utils/cacheControlPolicy.ts";
|
||||||
shouldPreserveCacheControl,
|
import { getCacheMetrics } from "@/lib/db/settings.ts";
|
||||||
trackCacheMetrics,
|
|
||||||
recordCacheHit,
|
|
||||||
type CacheControlMetrics,
|
|
||||||
} from "../utils/cacheControlPolicy.ts";
|
|
||||||
import { getCacheMetrics, updateCacheMetrics } from "@/lib/db/settings.ts";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
parseCodexQuotaHeaders,
|
parseCodexQuotaHeaders,
|
||||||
@@ -701,27 +696,6 @@ export async function handleChatCore({
|
|||||||
settings: { alwaysPreserveClientCache: cacheControlMode },
|
settings: { alwaysPreserveClientCache: cacheControlMode },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Track cache metrics for this request
|
|
||||||
let currentMetrics = await getCacheMetrics().catch(() => ({
|
|
||||||
totalRequests: 0,
|
|
||||||
requestsWithCacheControl: 0,
|
|
||||||
totalInputTokens: 0,
|
|
||||||
totalCachedTokens: 0,
|
|
||||||
totalCacheCreationTokens: 0,
|
|
||||||
tokensSaved: 0,
|
|
||||||
estimatedCostSaved: 0,
|
|
||||||
byProvider: {},
|
|
||||||
byStrategy: {},
|
|
||||||
lastUpdated: new Date().toISOString(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
currentMetrics = trackCacheMetrics({
|
|
||||||
preserved: preserveCacheControl,
|
|
||||||
provider,
|
|
||||||
strategy: comboStrategy,
|
|
||||||
metrics: currentMetrics,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (preserveCacheControl) {
|
if (preserveCacheControl) {
|
||||||
log?.debug?.(
|
log?.debug?.(
|
||||||
"CACHE",
|
"CACHE",
|
||||||
@@ -1473,18 +1447,6 @@ export async function handleChatCore({
|
|||||||
(usage as any).prompt_tokens_details?.cache_creation_tokens
|
(usage as any).prompt_tokens_details?.cache_creation_tokens
|
||||||
);
|
);
|
||||||
|
|
||||||
if (cachedTokens > 0 || cacheCreationTokens > 0) {
|
|
||||||
currentMetrics = updateCacheTokenMetrics({
|
|
||||||
metrics: currentMetrics,
|
|
||||||
provider,
|
|
||||||
strategy: comboStrategy,
|
|
||||||
inputTokens,
|
|
||||||
cachedTokens,
|
|
||||||
cacheCreationTokens,
|
|
||||||
costSaved: 0, // Will be calculated based on pricing
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
saveRequestUsage({
|
saveRequestUsage({
|
||||||
provider: provider || "unknown",
|
provider: provider || "unknown",
|
||||||
model: model || "unknown",
|
model: model || "unknown",
|
||||||
@@ -1649,17 +1611,22 @@ export async function handleChatCore({
|
|||||||
(streamUsage as any).prompt_tokens_details?.cache_creation_tokens
|
(streamUsage as any).prompt_tokens_details?.cache_creation_tokens
|
||||||
);
|
);
|
||||||
|
|
||||||
if (cachedTokens > 0 || cacheCreationTokens > 0) {
|
saveRequestUsage({
|
||||||
currentMetrics = updateCacheTokenMetrics({
|
provider: provider || "unknown",
|
||||||
metrics: currentMetrics,
|
model: model || "unknown",
|
||||||
provider,
|
tokens: streamUsage,
|
||||||
strategy: comboStrategy,
|
status: String(streamStatus || 200),
|
||||||
inputTokens,
|
success: streamStatus === 200,
|
||||||
cachedTokens,
|
latencyMs: Date.now() - startTime,
|
||||||
cacheCreationTokens,
|
timeToFirstTokenMs: ttft,
|
||||||
costSaved: 0,
|
errorCode: null,
|
||||||
});
|
timestamp: new Date().toISOString(),
|
||||||
}
|
connectionId: connectionId || undefined,
|
||||||
|
apiKeyId: apiKeyInfo?.id || undefined,
|
||||||
|
apiKeyName: apiKeyInfo?.name || undefined,
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error("Failed to save usage stats:", err.message);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
persistAttemptLogs({
|
persistAttemptLogs({
|
||||||
@@ -1673,11 +1640,6 @@ export async function handleChatCore({
|
|||||||
claudeCacheUsageMeta: cacheUsageLogMeta,
|
claudeCacheUsageMeta: cacheUsageLogMeta,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Persist cache metrics to database
|
|
||||||
updateCacheMetrics(currentMetrics).catch((err) => {
|
|
||||||
log?.debug?.("CACHE", `Failed to persist cache metrics: ${err?.message || "unknown"}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (apiKeyInfo?.id && streamUsage) {
|
if (apiKeyInfo?.id && streamUsage) {
|
||||||
calculateCost(provider, model, streamUsage)
|
calculateCost(provider, model, streamUsage)
|
||||||
.then((estimatedCost) => {
|
.then((estimatedCost) => {
|
||||||
|
|||||||
@@ -80,16 +80,24 @@ export async function handleEmbedding({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build upstream request
|
// Build upstream request — start with standard fields, then forward any extras
|
||||||
|
// the client sent (e.g. input_type, user, truncate for NVIDIA NIM asymmetric models).
|
||||||
|
const KNOWN_FIELDS = new Set(["model", "input", "dimensions", "encoding_format"]);
|
||||||
|
|
||||||
const upstreamBody: Record<string, unknown> = {
|
const upstreamBody: Record<string, unknown> = {
|
||||||
model: model,
|
model: model,
|
||||||
input: body.input,
|
input: body.input,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pass optional parameters
|
|
||||||
if (body.dimensions !== undefined) upstreamBody.dimensions = body.dimensions;
|
if (body.dimensions !== undefined) upstreamBody.dimensions = body.dimensions;
|
||||||
if (body.encoding_format !== undefined) upstreamBody.encoding_format = body.encoding_format;
|
if (body.encoding_format !== undefined) upstreamBody.encoding_format = body.encoding_format;
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(body)) {
|
||||||
|
if (!KNOWN_FIELDS.has(key) && value !== undefined) {
|
||||||
|
upstreamBody[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build headers
|
// Build headers
|
||||||
const headers = {
|
const headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -104,6 +112,12 @@ export async function handleEmbedding({
|
|||||||
} else if (providerConfig.authHeader === "x-api-key") {
|
} else if (providerConfig.authHeader === "x-api-key") {
|
||||||
headers["x-api-key"] = token;
|
headers["x-api-key"] = token;
|
||||||
}
|
}
|
||||||
|
} else if (providerConfig.authType !== "none") {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
status: 401,
|
||||||
|
error: `No valid authentication token for provider ${provider}. Check provider credentials.`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (log) {
|
if (log) {
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "omniroute",
|
"name": "omniroute",
|
||||||
"version": "3.2.6",
|
"version": "3.2.8",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "omniroute",
|
"name": "omniroute",
|
||||||
"version": "3.2.6",
|
"version": "3.2.8",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "omniroute",
|
"name": "omniroute",
|
||||||
"version": "3.2.7",
|
"version": "3.2.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.",
|
"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",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
17
release_notes.md
Normal file
17
release_notes.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
## [3.2.8] - 2026-03-29
|
||||||
|
|
||||||
|
### ✨ Enhancements & Refactoring
|
||||||
|
|
||||||
|
- **Docker Auto-Update UI** — Integrated a detached background update process for Docker Compose deployments. The Dashboard UI now seamlessly tracks update lifecycle events combining JSON REST responses with SSE streaming progress overlays for robust cross-environment reliability.
|
||||||
|
- **Cache Analytics** — Repaired zero-metrics visualization mapping by migrating Semantic Cache telemetry logs directly into the centralized tracking SQLite module.
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **Authentication Logic** — Fixed a bug where saving dashboard settings or adding models failed with a 401 Unauthorized error when `requireLogin` was disabled. API endpoints now correctly evaluate the global authentication toggle. Resolved global redirection by reactivating `src/middleware.ts`.
|
||||||
|
- **CLI Tool Detection (Windows)** — Prevented fatal initialization exceptions during CLI environment detection by catching `cross-spawn` ENOENT errors correctly. Adds explicit detection paths for `\AppData\Local\droid\droid.exe`.
|
||||||
|
- **Codex Native Passthrough** — Normalized model translation parameters preventing context poisoning in proxy pass-through mode, enforcing generic `store: false` constraints explicitly for all Codex-originated requests.
|
||||||
|
- **SSE Token Reporting** — Normalized provider tool-call chunk `finish_reason` detection, fixing 0% Usage analytics for stream-only responses missing strict `<DONE>` indicators.
|
||||||
|
- **DeepSeek <think> Tags** — Implemented an explicit `<think>` extraction mapping inside `responsesHandler.ts`, ensuring DeepSeek reasoning streams map equivalently to native Anthropic `<thinking>` structures.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
type EmbeddingProviderNodeRow,
|
type EmbeddingProviderNodeRow,
|
||||||
type EmbeddingProvider,
|
type EmbeddingProvider,
|
||||||
} from "@omniroute/open-sse/config/embeddingRegistry.ts";
|
} from "@omniroute/open-sse/config/embeddingRegistry.ts";
|
||||||
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
|
import { errorResponse, unavailableResponse } from "@omniroute/open-sse/utils/error.ts";
|
||||||
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
|
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
|
||||||
import * as log from "@/sse/utils/logger";
|
import * as log from "@/sse/utils/logger";
|
||||||
import { toJsonErrorPayload } from "@/shared/utils/upstreamError";
|
import { toJsonErrorPayload } from "@/shared/utils/upstreamError";
|
||||||
@@ -209,6 +209,14 @@ export async function POST(request) {
|
|||||||
`No credentials for embedding provider: ${provider}`
|
`No credentials for embedding provider: ${provider}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (credentials.allRateLimited) {
|
||||||
|
return unavailableResponse(
|
||||||
|
HTTP_STATUS.RATE_LIMITED,
|
||||||
|
`[${provider}] All accounts rate limited`,
|
||||||
|
credentials.retryAfter,
|
||||||
|
credentials.retryAfterHuman
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await handleEmbedding({
|
const result = await handleEmbedding({
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
getAllImageModels,
|
getAllImageModels,
|
||||||
getImageProvider,
|
getImageProvider,
|
||||||
} from "@omniroute/open-sse/config/imageRegistry.ts";
|
} from "@omniroute/open-sse/config/imageRegistry.ts";
|
||||||
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
|
import { errorResponse, unavailableResponse } from "@omniroute/open-sse/utils/error.ts";
|
||||||
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
|
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
|
||||||
import * as log from "@/sse/utils/logger";
|
import * as log from "@/sse/utils/logger";
|
||||||
import { toJsonErrorPayload } from "@/shared/utils/upstreamError";
|
import { toJsonErrorPayload } from "@/shared/utils/upstreamError";
|
||||||
@@ -156,8 +156,15 @@ export async function POST(request) {
|
|||||||
`No credentials for image provider: ${provider}`
|
`No credentials for image provider: ${provider}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (credentials.allRateLimited) {
|
||||||
|
return unavailableResponse(
|
||||||
|
HTTP_STATUS.RATE_LIMITED,
|
||||||
|
`[${provider}] All accounts rate limited`,
|
||||||
|
credentials.retryAfter,
|
||||||
|
credentials.retryAfterHuman
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if (isCustomModel) {
|
} else if (isCustomModel) {
|
||||||
// Custom models need credentials from the provider connection
|
|
||||||
credentials = await getProviderCredentials(provider);
|
credentials = await getProviderCredentials(provider);
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
return errorResponse(
|
return errorResponse(
|
||||||
@@ -165,6 +172,14 @@ export async function POST(request) {
|
|||||||
`No credentials for custom image provider: ${provider}`
|
`No credentials for custom image provider: ${provider}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (credentials.allRateLimited) {
|
||||||
|
return unavailableResponse(
|
||||||
|
HTTP_STATUS.RATE_LIMITED,
|
||||||
|
`[${provider}] All accounts rate limited`,
|
||||||
|
credentials.retryAfter,
|
||||||
|
credentials.retryAfterHuman
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await handleImageGeneration({
|
const result = await handleImageGeneration({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CORS_ORIGIN } from "@/shared/utils/cors";
|
import { CORS_ORIGIN } from "@/shared/utils/cors";
|
||||||
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
|
import { errorResponse, unavailableResponse } from "@omniroute/open-sse/utils/error.ts";
|
||||||
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
|
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
|
||||||
import { getRegistryEntry } from "@omniroute/open-sse/config/providerRegistry.ts";
|
import { getRegistryEntry } from "@omniroute/open-sse/config/providerRegistry.ts";
|
||||||
import {
|
import {
|
||||||
@@ -85,6 +85,14 @@ export async function POST(request, { params }) {
|
|||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
return errorResponse(HTTP_STATUS.BAD_REQUEST, `No credentials for provider: ${rawProvider}`);
|
return errorResponse(HTTP_STATUS.BAD_REQUEST, `No credentials for provider: ${rawProvider}`);
|
||||||
}
|
}
|
||||||
|
if (credentials.allRateLimited) {
|
||||||
|
return unavailableResponse(
|
||||||
|
HTTP_STATUS.RATE_LIMITED,
|
||||||
|
`[${rawProvider}] All accounts rate limited`,
|
||||||
|
credentials.retryAfter,
|
||||||
|
credentials.retryAfterHuman
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const result = await handleEmbedding({ body, credentials, log });
|
const result = await handleEmbedding({ body, credentials, log });
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { CORS_ORIGIN } from "@/shared/utils/cors";
|
import { CORS_ORIGIN } from "@/shared/utils/cors";
|
||||||
import { handleImageGeneration } from "@omniroute/open-sse/handlers/imageGeneration.ts";
|
import { handleImageGeneration } from "@omniroute/open-sse/handlers/imageGeneration.ts";
|
||||||
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
|
import { errorResponse, unavailableResponse } from "@omniroute/open-sse/utils/error.ts";
|
||||||
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
|
import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
|
||||||
import {
|
import {
|
||||||
getProviderCredentials,
|
getProviderCredentials,
|
||||||
@@ -85,6 +85,14 @@ export async function POST(request, { params }) {
|
|||||||
`No credentials for image provider: ${rawProvider}`
|
`No credentials for image provider: ${rawProvider}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (credentials.allRateLimited) {
|
||||||
|
return unavailableResponse(
|
||||||
|
HTTP_STATUS.RATE_LIMITED,
|
||||||
|
`[${rawProvider}] All accounts rate limited`,
|
||||||
|
credentials.retryAfter,
|
||||||
|
credentials.retryAfterHuman
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const result = await handleImageGeneration({ body, credentials, log });
|
const result = await handleImageGeneration({ body, credentials, log });
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { isModelSyncInternalRequest } from "./shared/services/modelSyncScheduler
|
|||||||
|
|
||||||
const SECRET = new TextEncoder().encode(process.env.JWT_SECRET || "");
|
const SECRET = new TextEncoder().encode(process.env.JWT_SECRET || "");
|
||||||
|
|
||||||
export async function proxy(request) {
|
export async function proxy(request: any) {
|
||||||
const { pathname } = request.nextUrl;
|
const { pathname } = request.nextUrl;
|
||||||
|
|
||||||
// Pipeline: Add request ID header for end-to-end tracing
|
// Pipeline: Add request ID header for end-to-end tracing
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ export async function verifyAuth(request: any): Promise<string | null> {
|
|||||||
* need to conditionally skip auth should check that separately.
|
* need to conditionally skip auth should check that separately.
|
||||||
*/
|
*/
|
||||||
export async function isAuthenticated(request: Request): Promise<boolean> {
|
export async function isAuthenticated(request: Request): Promise<boolean> {
|
||||||
|
// If settings say login/auth is disabled, treat all requests as authenticated
|
||||||
|
if (!(await isAuthRequired())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// 1. Check API key (for external clients)
|
// 1. Check API key (for external clients)
|
||||||
const authHeader = request.headers.get("authorization");
|
const authHeader = request.headers.get("authorization");
|
||||||
if (authHeader?.startsWith("Bearer ")) {
|
if (authHeader?.startsWith("Bearer ")) {
|
||||||
|
|||||||
207
test_out.txt
Normal file
207
test_out.txt
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
[CREDENTIALS] No external credentials file found, using defaults.
|
||||||
|
[DB] SQLite database ready: /home/diegosouzapw/.omniroute/storage.sqlite
|
||||||
|
[MODEL] Ambiguous model 'claude-haiku-4.5'. Use provider/model prefix (ex: gh/claude-haiku-4.5 or kr/claude-haiku-4.5). Candidates: gh, kr, anthropic
|
||||||
|
TAP version 13
|
||||||
|
# Subtest: getModelInfoCore resolves unique non-openai unprefixed model
|
||||||
|
ok 1 - getModelInfoCore resolves unique non-openai unprefixed model
|
||||||
|
---
|
||||||
|
duration_ms: 3.403766
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: getModelInfoCore keeps openai fallback for gpt-4o
|
||||||
|
ok 2 - getModelInfoCore keeps openai fallback for gpt-4o
|
||||||
|
---
|
||||||
|
duration_ms: 0.535726
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: getModelInfoCore resolves gpt-5.4 to codex
|
||||||
|
ok 3 - getModelInfoCore resolves gpt-5.4 to codex
|
||||||
|
---
|
||||||
|
duration_ms: 0.321781
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: getModelInfoCore returns explicit ambiguity metadata for ambiguous unprefixed model
|
||||||
|
ok 4 - getModelInfoCore returns explicit ambiguity metadata for ambiguous unprefixed model
|
||||||
|
---
|
||||||
|
duration_ms: 1.079896
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: getModelInfoCore canonicalizes github legacy alias with explicit provider prefix
|
||||||
|
ok 5 - getModelInfoCore canonicalizes github legacy alias with explicit provider prefix
|
||||||
|
---
|
||||||
|
duration_ms: 0.370547
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: GithubExecutor routes codex-family model to /responses
|
||||||
|
ok 6 - GithubExecutor routes codex-family model to /responses
|
||||||
|
---
|
||||||
|
duration_ms: 0.47113
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: GithubExecutor keeps non-codex model on /chat/completions
|
||||||
|
ok 7 - GithubExecutor keeps non-codex model on /chat/completions
|
||||||
|
---
|
||||||
|
duration_ms: 0.38457
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: DefaultExecutor uses x-api-key for kimi-coding-apikey
|
||||||
|
ok 8 - DefaultExecutor uses x-api-key for kimi-coding-apikey
|
||||||
|
---
|
||||||
|
duration_ms: 0.451443
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: CodexExecutor forces stream=true for upstream compatibility
|
||||||
|
ok 9 - CodexExecutor forces stream=true for upstream compatibility
|
||||||
|
---
|
||||||
|
duration_ms: 1.203259
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: Claude native messages can be round-tripped through OpenAI into Claude OAuth format
|
||||||
|
ok 10 - Claude native messages can be round-tripped through OpenAI into Claude OAuth format
|
||||||
|
---
|
||||||
|
duration_ms: 7.232512
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: CodexExecutor maps fast service tier to priority
|
||||||
|
ok 11 - CodexExecutor maps fast service tier to priority
|
||||||
|
---
|
||||||
|
duration_ms: 0.489993
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: shouldUseNativeCodexPassthrough only enables responses-native Codex requests
|
||||||
|
ok 12 - shouldUseNativeCodexPassthrough only enables responses-native Codex requests
|
||||||
|
---
|
||||||
|
duration_ms: 0.441911
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: CodexExecutor can force fast service tier from settings
|
||||||
|
ok 13 - CodexExecutor can force fast service tier from settings
|
||||||
|
---
|
||||||
|
duration_ms: 0.299575
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: CodexExecutor always requests SSE accept header
|
||||||
|
ok 14 - CodexExecutor always requests SSE accept header
|
||||||
|
---
|
||||||
|
duration_ms: 0.602914
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: CodexExecutor does not request SSE accept header for compact requests
|
||||||
|
ok 15 - CodexExecutor does not request SSE accept header for compact requests
|
||||||
|
---
|
||||||
|
duration_ms: 0.322611
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: CodexExecutor preserves native responses payloads for Codex passthrough
|
||||||
|
not ok 16 - CodexExecutor preserves native responses payloads for Codex passthrough
|
||||||
|
---
|
||||||
|
duration_ms: 1.856261
|
||||||
|
type: 'test'
|
||||||
|
location: '/home/diegosouzapw/dev/proxys/9router/tests/unit/plan3-p0.test.mjs:221:1'
|
||||||
|
failureType: 'testCodeFailure'
|
||||||
|
error: |-
|
||||||
|
Expected values to be strictly equal:
|
||||||
|
|
||||||
|
false !== true
|
||||||
|
|
||||||
|
code: 'ERR_ASSERTION'
|
||||||
|
name: 'AssertionError'
|
||||||
|
expected: true
|
||||||
|
actual: false
|
||||||
|
operator: 'strictEqual'
|
||||||
|
stack: |-
|
||||||
|
TestContext.<anonymous> (file:///home/diegosouzapw/dev/proxys/9router/tests/unit/plan3-p0.test.mjs:242:10)
|
||||||
|
Test.runInAsyncScope (node:async_hooks:214:14)
|
||||||
|
Test.run (node:internal/test_runner/test:1047:25)
|
||||||
|
Test.processPendingSubtests (node:internal/test_runner/test:744:18)
|
||||||
|
Test.postRun (node:internal/test_runner/test:1173:19)
|
||||||
|
Test.run (node:internal/test_runner/test:1101:12)
|
||||||
|
async Test.processPendingSubtests (node:internal/test_runner/test:744:7)
|
||||||
|
...
|
||||||
|
# Subtest: CodexExecutor strips streaming fields for compact passthrough
|
||||||
|
ok 17 - CodexExecutor strips streaming fields for compact passthrough
|
||||||
|
---
|
||||||
|
duration_ms: 0.296176
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: CodexExecutor routes responses subpaths to matching upstream paths
|
||||||
|
ok 18 - CodexExecutor routes responses subpaths to matching upstream paths
|
||||||
|
---
|
||||||
|
duration_ms: 0.546657
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: translateNonStreamingResponse converts Responses API payload to OpenAI chat.completion
|
||||||
|
ok 19 - translateNonStreamingResponse converts Responses API payload to OpenAI chat.completion
|
||||||
|
---
|
||||||
|
duration_ms: 1.483788
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: extractUsageFromResponse reads usage from Responses API payload
|
||||||
|
ok 20 - extractUsageFromResponse reads usage from Responses API payload
|
||||||
|
---
|
||||||
|
duration_ms: 0.398039
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: detectFormat identifies OpenAI Responses when input is string
|
||||||
|
ok 21 - detectFormat identifies OpenAI Responses when input is string
|
||||||
|
---
|
||||||
|
duration_ms: 0.359174
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: detectFormat identifies OpenAI Responses by max_output_tokens without input array
|
||||||
|
ok 22 - detectFormat identifies OpenAI Responses by max_output_tokens without input array
|
||||||
|
---
|
||||||
|
duration_ms: 0.271215
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: detectFormatFromEndpoint forces OpenAI for /v1/chat/completions
|
||||||
|
ok 23 - detectFormatFromEndpoint forces OpenAI for /v1/chat/completions
|
||||||
|
---
|
||||||
|
duration_ms: 0.52054
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: detectFormatFromEndpoint forces Claude for /v1/messages
|
||||||
|
ok 24 - detectFormatFromEndpoint forces Claude for /v1/messages
|
||||||
|
---
|
||||||
|
duration_ms: 0.433035
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: translateRequest normalizes openai-responses input string into list payload
|
||||||
|
ok 25 - translateRequest normalizes openai-responses input string into list payload
|
||||||
|
---
|
||||||
|
duration_ms: 0.358109
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: translateRequest preserves service_tier when converting openai to openai-responses
|
||||||
|
ok 26 - translateRequest preserves service_tier when converting openai to openai-responses
|
||||||
|
---
|
||||||
|
duration_ms: 1.10454
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: parseSSEToResponsesOutput parses completed response from SSE payload
|
||||||
|
ok 27 - parseSSEToResponsesOutput parses completed response from SSE payload
|
||||||
|
---
|
||||||
|
duration_ms: 0.575476
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: parseSSEToResponsesOutput returns null for invalid payload
|
||||||
|
ok 28 - parseSSEToResponsesOutput returns null for invalid payload
|
||||||
|
---
|
||||||
|
duration_ms: 0.302714
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: parseSSEToOpenAIResponse merges split tool call chunks by id without duplication
|
||||||
|
ok 29 - parseSSEToOpenAIResponse merges split tool call chunks by id without duplication
|
||||||
|
---
|
||||||
|
duration_ms: 0.916032
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..29
|
||||||
|
# tests 29
|
||||||
|
# suites 0
|
||||||
|
# pass 28
|
||||||
|
# fail 1
|
||||||
|
# cancelled 0
|
||||||
|
# skipped 0
|
||||||
|
# todo 0
|
||||||
|
# duration_ms 65.394285
|
||||||
@@ -120,7 +120,11 @@ test("isAuthenticated accepts bearer API keys", async () => {
|
|||||||
assert.equal(result, true);
|
assert.equal(result, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("isAuthenticated returns false without valid credentials", async () => {
|
test("isAuthenticated returns false when auth is required without valid credentials", async () => {
|
||||||
|
// Force requireLogin to be active
|
||||||
|
process.env.INITIAL_PASSWORD = "bootstrap-password";
|
||||||
|
await localDb.updateSettings({ requireLogin: true, password: "" });
|
||||||
|
|
||||||
const request = new Request("https://example.com/api/providers");
|
const request = new Request("https://example.com/api/providers");
|
||||||
|
|
||||||
const result = await apiAuth.isAuthenticated(request);
|
const result = await apiAuth.isAuthenticated(request);
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ test("CodexExecutor preserves native responses payloads for Codex passthrough",
|
|||||||
assert.equal(transformed.stream, true);
|
assert.equal(transformed.stream, true);
|
||||||
assert.equal(transformed.service_tier, "priority");
|
assert.equal(transformed.service_tier, "priority");
|
||||||
assert.equal(transformed.instructions, "custom system prompt");
|
assert.equal(transformed.instructions, "custom system prompt");
|
||||||
assert.equal(transformed.store, true);
|
assert.equal(transformed.store, false);
|
||||||
assert.deepEqual(transformed.metadata, { source: "codex-client" });
|
assert.deepEqual(transformed.metadata, { source: "codex-client" });
|
||||||
assert.equal(transformed.reasoning_effort, "high");
|
assert.equal(transformed.reasoning_effort, "high");
|
||||||
assert.ok(!("_nativeCodexPassthrough" in transformed));
|
assert.ok(!("_nativeCodexPassthrough" in transformed));
|
||||||
|
|||||||
Reference in New Issue
Block a user