diff --git a/AGENTS.md b/AGENTS.md index c5c2e48254d9..f6fafac1ac2f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,7 @@ Unified AI proxy/router — route any LLM through one endpoint. Multi-provider s ## Stack - **Runtime**: Next.js 16 (App Router), Node.js, ES Modules +- **Language**: TypeScript 5.9 (`src/`) + JavaScript (`open-sse/`) - **Database**: better-sqlite3 (SQLite) — `DATA_DIR` configurable, default `~/.omniroute/` - **Streaming**: SSE via `open-sse` internal package - **Styling**: Tailwind CSS v4 @@ -21,15 +22,15 @@ All persistence uses SQLite through domain-specific modules: | Module | Responsibility | | -------------- | ------------------------------------------ | -| `core.js` | SQLite engine, migrations, WAL, encryption | -| `providers.js` | Provider connections & nodes | -| `models.js` | Model aliases, MITM aliases, custom models | -| `combos.js` | Combo configurations | -| `apiKeys.js` | API key management & validation | -| `settings.js` | Settings, pricing, proxy config | -| `backup.js` | Backup / restore operations | - -`src/lib/localDb.js` is a **re-export layer only** — all 27+ consumers import from it, +| `core.ts` | SQLite engine, migrations, WAL, encryption | +| `providers.ts` | Provider connections & nodes | +| `models.ts` | Model aliases, MITM aliases, custom models | +| `combos.ts` | Combo configurations | +| `apiKeys.ts` | API key management & validation | +| `settings.ts` | Settings, pricing, proxy config | +| `backup.ts` | Backup / restore operations | + +`src/lib/localDb.ts` is a **re-export layer only** — all 27+ consumers import from it, but the real logic lives in `src/lib/db/`. ### Request Pipeline (`open-sse/`) @@ -49,25 +50,25 @@ Translation between provider formats: `open-sse/translator/` ### OAuth & Tokens (`src/lib/oauth/`) 18 modules handling OAuth flows, token refresh, and provider credentials. -Default credentials are hardcoded in `src/lib/oauth/constants/oauth.js`, +Default credentials are hardcoded in `src/lib/oauth/constants/oauth.ts`, overridable via env vars or `data/provider-credentials.json`. ### Supporting Systems | System | Location | | -------------------------- | ------------------------------------------------- | -| Usage tracking & analytics | `src/lib/usageDb.js`, `src/lib/usageAnalytics.js` | -| Token health checks | `src/lib/tokenHealthCheck.js` | -| Cloud sync | `src/lib/cloudSync.js` | -| Proxy logging | `src/lib/proxyLogger.js` | -| Data paths resolution | `src/lib/dataPaths.js` | +| Usage tracking & analytics | `src/lib/usageDb.ts`, `src/lib/usageAnalytics.ts` | +| Token health checks | `src/lib/tokenHealthCheck.ts` | +| Cloud sync | `src/lib/cloudSync.ts` | +| Proxy logging | `src/lib/proxyLogger.ts` | +| Data paths resolution | `src/lib/dataPaths.ts` | ### Adding a New Provider -1. Register in `src/shared/constants/providers.js` +1. Register in `src/shared/constants/providers.ts` 2. Add executor in `open-sse/executors/` 3. Add translator rules in `open-sse/translator/` (if non-OpenAI format) -4. Add OAuth config in `src/lib/oauth/constants/oauth.js` (if OAuth-based) +4. Add OAuth config in `src/lib/oauth/constants/oauth.ts` (if OAuth-based) ## Review Focus @@ -83,7 +84,7 @@ overridable via env vars or `data/provider-credentials.json`. - DB operations go through `src/lib/db/` modules, never raw SQL in routes - Provider requests flow through `open-sse/handlers/` - Translations use `open-sse/translator/` modules -- `localDb.js` is re-exports only — add new functions to the proper `db/*.js` module +- `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module ### Code Quality diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3d72cd4e44c7..014e4c7c443e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -123,7 +123,7 @@ npm run lint npm run check ``` -Current test status: **320+ unit tests** covering: +Current test status: **368+ unit tests** covering: - Provider translators and format conversion - Rate limiting, circuit breaker, and resilience @@ -138,7 +138,7 @@ Current test status: **320+ unit tests** covering: - **ESLint** — Run `npm run lint` before committing - **Prettier** — Auto-formatted via `lint-staged` on commit -- **JSDoc** — Document public functions with `@param`, `@returns`, `@throws` +- **TypeScript** — All `src/` code uses `.ts`/`.tsx`; document with TSDoc (`@param`, `@returns`, `@throws`) - **No `eval()`** — ESLint enforces `no-eval`, `no-implied-eval`, `no-new-func` - **Zod validation** — Use Zod schemas for API input validation @@ -147,34 +147,34 @@ Current test status: **320+ unit tests** covering: ## Project Structure ``` -src/ +src/ # TypeScript (.ts / .tsx) ├── app/ # Next.js App Router -│ ├── (dashboard)/ # Dashboard pages -│ ├── api/ # API routes -│ └── login/ # Auth pages -├── domain/ # Domain types and response helpers -├── lib/ # Core business logic +│ ├── (dashboard)/ # Dashboard pages (.tsx) +│ ├── api/ # API routes (.ts) +│ └── login/ # Auth pages (.tsx) +├── domain/ # Domain types and response helpers (.ts) +├── lib/ # Core business logic (.ts) │ ├── db/ # SQLite database layer │ ├── oauth/ # OAuth services per provider -│ ├── cacheLayer.js # LRU cache -│ ├── semanticCache.js # Semantic response cache -│ ├── idempotencyLayer.js # Request deduplication -│ └── localDb.js # LowDB (JSON) storage +│ ├── cacheLayer.ts # LRU cache +│ ├── semanticCache.ts # Semantic response cache +│ ├── idempotencyLayer.ts # Request deduplication +│ └── localDb.ts # LowDB (JSON) storage ├── shared/ -│ ├── components/ # React components +│ ├── components/ # React components (.tsx) │ ├── middleware/ # Correlation IDs, etc. │ ├── utils/ # Circuit breaker, sanitizer, etc. │ └── validation/ # Zod schemas -└── sse/ # SSE chat handlers +└── sse/ # SSE chat handlers (.ts) -open-sse/ # @omniroute/open-sse workspace +open-sse/ # @omniroute/open-sse workspace (JavaScript) ├── handlers/ # chatCore.js — main request handler ├── services/ # Rate limit, fallback ├── translators/ # Format converters (OpenAI ↔ Claude ↔ Gemini) └── utils/ # Progress tracker, stream helpers tests/ -├── unit/ # Node.js test runner +├── unit/ # Node.js test runner (.test.mjs) └── e2e/ # Playwright tests docs/ # Documentation @@ -191,10 +191,10 @@ docs/ # Documentation ### Step 1: OAuth Service (if using OAuth) -Create `src/lib/oauth/services/your-provider.js` extending `OAuthService`: +Create `src/lib/oauth/services/your-provider.ts` extending `OAuthService`: -```javascript -import { OAuthService } from "../OAuthService.js"; +```typescript +import { OAuthService } from "../OAuthService"; export class YourProviderService extends OAuthService { constructor() { @@ -211,16 +211,16 @@ export class YourProviderService extends OAuthService { ### Step 2: Register Provider -Add to `src/lib/oauth/providers.js`: +Add to `src/lib/oauth/providers.ts`: -```javascript -import { YourProviderService } from "./services/your-provider.js"; +```typescript +import { YourProviderService } from "./services/your-provider"; // Add to the providers map ``` ### Step 3: Add Constants -Add provider constants in `src/lib/providerConstants.js`: +Add provider constants in `src/lib/providerConstants.ts`: - Provider prefix (e.g., `yp/`) - Default models @@ -232,7 +232,7 @@ Create translator in `open-sse/translators/` if the provider uses a custom API f ### Step 5: Add Timeout -Add request timeout configuration in `src/shared/utils/requestTimeout.js`. +Add request timeout configuration in `src/shared/utils/requestTimeout.ts`. ### Step 6: Add Tests @@ -249,7 +249,7 @@ Write unit tests in `tests/unit/` covering at minimum: - [ ] Tests pass (`npm test`) - [ ] Linting passes (`npm run lint`) - [ ] Build succeeds (`npm run build`) -- [ ] JSDoc added for new public functions +- [ ] TypeScript types added for new public functions and interfaces - [ ] No hardcoded secrets or fallback values - [ ] CHANGELOG updated (if user-facing change) - [ ] Documentation updated (if applicable) diff --git a/README.md b/README.md index 048f673fcb56..aa5f48e50cc4 100644 --- a/README.md +++ b/README.md @@ -231,9 +231,9 @@ curl http://localhost:20128/api/evals/golden-set ### Custom Suites -Register custom suites programmatically via `registerSuite()` in `src/lib/evals/evalRunner.js`: +Register custom suites programmatically via `registerSuite()` in `src/lib/evals/evalRunner.ts`: -```javascript +```typescript registerSuite({ id: "my-suite", name: "Custom Eval Suite", @@ -254,11 +254,12 @@ registerSuite({ ## 🛠️ Tech Stack - **Runtime**: Node.js 20+ +- **Language**: TypeScript 5.9 (src/) + JavaScript (open-sse/) - **Framework**: Next.js 16 + React 19 + Tailwind CSS 4 - **Database**: LowDB (JSON) + SQLite (domain state) - **Streaming**: Server-Sent Events (SSE) - **Auth**: OAuth 2.0 (PKCE) + JWT + API Keys -- **Testing**: Node.js test runner (320+ unit tests) +- **Testing**: Node.js test runner (368+ unit tests) - **CI/CD**: GitHub Actions (auto npm publish on release) - **Website**: [omniroute.online](https://omniroute.online) - **Package**: [npmjs.com/package/omniroute](https://www.npmjs.com/package/omniroute) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8ecaadeb9eb2..8ff873e75390 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,6 +1,6 @@ # OmniRoute Architecture -_Last updated: 2026-02-15_ +_Last updated: 2026-02-17_ ## Executive Summary @@ -120,18 +120,18 @@ Main directories: Important compatibility routes: -- `src/app/api/v1/chat/completions/route.js` -- `src/app/api/v1/messages/route.js` -- `src/app/api/v1/responses/route.js` -- `src/app/api/v1/models/route.js` — includes custom models with `custom: true` -- `src/app/api/v1/embeddings/route.js` — embedding generation (6 providers) -- `src/app/api/v1/images/generations/route.js` — image generation (4+ providers incl. Antigravity/Nebius) -- `src/app/api/v1/messages/count_tokens/route.js` -- `src/app/api/v1/providers/[provider]/chat/completions/route.js` — dedicated per-provider chat -- `src/app/api/v1/providers/[provider]/embeddings/route.js` — dedicated per-provider embeddings -- `src/app/api/v1/providers/[provider]/images/generations/route.js` — dedicated per-provider images -- `src/app/api/v1beta/models/route.js` -- `src/app/api/v1beta/models/[...path]/route.js` +- `src/app/api/v1/chat/completions/route.ts` +- `src/app/api/v1/messages/route.ts` +- `src/app/api/v1/responses/route.ts` +- `src/app/api/v1/models/route.ts` — includes custom models with `custom: true` +- `src/app/api/v1/embeddings/route.ts` — embedding generation (6 providers) +- `src/app/api/v1/images/generations/route.ts` — image generation (4+ providers incl. Antigravity/Nebius) +- `src/app/api/v1/messages/count_tokens/route.ts` +- `src/app/api/v1/providers/[provider]/chat/completions/route.ts` — dedicated per-provider chat +- `src/app/api/v1/providers/[provider]/embeddings/route.ts` — dedicated per-provider embeddings +- `src/app/api/v1/providers/[provider]/images/generations/route.ts` — dedicated per-provider images +- `src/app/api/v1beta/models/route.ts` +- `src/app/api/v1beta/models/[...path]/route.ts` Management domains: @@ -166,11 +166,11 @@ Management domains: Main flow modules: -- Entry: `src/sse/handlers/chat.js` +- Entry: `src/sse/handlers/chat.ts` - Core orchestration: `open-sse/handlers/chatCore.js` - Provider execution adapters: `open-sse/executors/*` - Format detection/provider config: `open-sse/services/provider.js` -- Model parse/resolve: `src/sse/services/model.js`, `open-sse/services/model.js` +- Model parse/resolve: `src/sse/services/model.ts`, `open-sse/services/model.js` - Account fallback logic: `open-sse/services/accountFallback.js` - Translation registry: `open-sse/translator/index.js` - Stream transformations: `open-sse/utils/stream.js`, `open-sse/utils/streamHandler.js` @@ -196,59 +196,59 @@ Services (business logic): Domain layer modules: -- Model availability: `src/lib/domain/modelAvailability.js` -- Cost rules/budgets: `src/lib/domain/costRules.js` -- Fallback policy: `src/lib/domain/fallbackPolicy.js` -- Combo resolver: `src/lib/domain/comboResolver.js` -- Lockout policy: `src/lib/domain/lockoutPolicy.js` -- Policy engine: `src/domain/policyEngine.js` — centralized lockout → budget → fallback evaluation -- Error codes catalog: `src/lib/domain/errorCodes.js` -- Request ID: `src/lib/domain/requestId.js` -- Fetch timeout: `src/lib/domain/fetchTimeout.js` -- Request telemetry: `src/lib/domain/requestTelemetry.js` -- Compliance/audit: `src/lib/domain/compliance/index.js` -- Eval runner: `src/lib/domain/evalRunner.js` -- Domain state persistence: `src/lib/db/domainState.js` — SQLite CRUD for fallback chains, budgets, cost history, lockout state, circuit breakers +- Model availability: `src/lib/domain/modelAvailability.ts` +- Cost rules/budgets: `src/lib/domain/costRules.ts` +- Fallback policy: `src/lib/domain/fallbackPolicy.ts` +- Combo resolver: `src/lib/domain/comboResolver.ts` +- Lockout policy: `src/lib/domain/lockoutPolicy.ts` +- Policy engine: `src/domain/policyEngine.ts` — centralized lockout → budget → fallback evaluation +- Error codes catalog: `src/lib/domain/errorCodes.ts` +- Request ID: `src/lib/domain/requestId.ts` +- Fetch timeout: `src/lib/domain/fetchTimeout.ts` +- Request telemetry: `src/lib/domain/requestTelemetry.ts` +- Compliance/audit: `src/lib/domain/compliance/index.ts` +- Eval runner: `src/lib/domain/evalRunner.ts` +- Domain state persistence: `src/lib/db/domainState.ts` — SQLite CRUD for fallback chains, budgets, cost history, lockout state, circuit breakers OAuth provider modules (12 individual files under `src/lib/oauth/providers/`): -- Registry index: `src/lib/oauth/providers/index.js` -- Individual providers: `claude.js`, `codex.js`, `gemini.js`, `antigravity.js`, `iflow.js`, `qwen.js`, `kimi-coding.js`, `github.js`, `kiro.js`, `cursor.js`, `kilocode.js`, `cline.js` -- Thin wrapper: `src/lib/oauth/providers.js` — re-exports from individual modules +- Registry index: `src/lib/oauth/providers/index.ts` +- Individual providers: `claude.ts`, `codex.ts`, `gemini.ts`, `antigravity.ts`, `iflow.ts`, `qwen.ts`, `kimi-coding.ts`, `github.ts`, `kiro.ts`, `cursor.ts`, `kilocode.ts`, `cline.ts` +- Thin wrapper: `src/lib/oauth/providers.ts` — re-exports from individual modules ## 3) Persistence Layer Primary state DB: -- `src/lib/localDb.js` +- `src/lib/localDb.ts` - file: `${DATA_DIR}/db.json` (or `$XDG_CONFIG_HOME/omniroute/db.json` when set, else `~/.omniroute/db.json`) - entities: providerConnections, providerNodes, modelAliases, combos, apiKeys, settings, pricing, **customModels**, **proxyConfig**, **ipFilter**, **thinkingBudget**, **systemPrompt** Usage DB: -- `src/lib/usageDb.js` +- `src/lib/usageDb.ts` - files: `${DATA_DIR}/usage.json`, `${DATA_DIR}/log.txt`, `${DATA_DIR}/call_logs/` - follows same base directory policy as `localDb` (`DATA_DIR`, then `XDG_CONFIG_HOME/omniroute` when set) -- decomposed into focused sub-modules: `migrations.js`, `usageHistory.js`, `costCalculator.js`, `usageStats.js`, `callLogs.js` +- decomposed into focused sub-modules: `migrations.ts`, `usageHistory.ts`, `costCalculator.ts`, `usageStats.ts`, `callLogs.ts` Domain State DB (SQLite): -- `src/lib/db/domainState.js` — CRUD operations for domain state -- Tables (created in `src/lib/db/core.js`): `domain_fallback_chains`, `domain_budgets`, `domain_cost_history`, `domain_lockout_state`, `domain_circuit_breakers` +- `src/lib/db/domainState.ts` — CRUD operations for domain state +- Tables (created in `src/lib/db/core.ts`): `domain_fallback_chains`, `domain_budgets`, `domain_cost_history`, `domain_lockout_state`, `domain_circuit_breakers` - Write-through cache pattern: in-memory Maps are authoritative at runtime; mutations are written synchronously to SQLite; state is restored from DB on cold start ## 4) Auth + Security Surfaces -- Dashboard cookie auth: `src/proxy.js`, `src/app/api/auth/login/route.js` -- API key generation/verification: `src/shared/utils/apiKey.js` +- Dashboard cookie auth: `src/proxy.ts`, `src/app/api/auth/login/route.ts` +- API key generation/verification: `src/shared/utils/apiKey.ts` - Provider secrets persisted in `providerConnections` entries - Outbound proxy support via `open-sse/utils/proxyFetch.js` (env vars) and `open-sse/utils/networkProxy.js` (configurable per-provider or global) ## 5) Cloud Sync -- Scheduler init: `src/lib/initCloudSync.js`, `src/shared/services/initializeCloudSync.js` -- Periodic task: `src/shared/services/cloudSyncScheduler.js` -- Control route: `src/app/api/sync/cloud/route.js` +- Scheduler init: `src/lib/initCloudSync.ts`, `src/shared/services/initializeCloudSync.ts` +- Periodic task: `src/shared/services/cloudSyncScheduler.ts` +- Control route: `src/app/api/sync/cloud/route.ts` ## Request Lifecycle (`/v1/chat/completions`) @@ -559,7 +559,7 @@ flowchart LR ### Routing and Execution Core -- `src/sse/handlers/chat.js`: request parse, combo handling, account selection loop +- `src/sse/handlers/chat.ts`: request parse, combo handling, account selection loop - `open-sse/handlers/chatCore.js`: translation, executor dispatch, retry/refresh handling, stream setup - `open-sse/executors/*`: provider-specific network and format behavior @@ -572,8 +572,8 @@ flowchart LR ### Persistence -- `src/lib/localDb.js`: persistent config/state -- `src/lib/usageDb.js`: usage history and rolling request logs +- `src/lib/localDb.ts`: persistent config/state +- `src/lib/usageDb.ts`: usage history and rolling request logs ## Provider Executor Coverage (Strategy Pattern) @@ -648,7 +648,7 @@ Translations are selected dynamically based on source payload shape and provider | Endpoint | Format | Handler | | -------------------------------------------------- | ------------------ | ---------------------------------------------------- | -| `POST /v1/chat/completions` | OpenAI Chat | `src/sse/handlers/chat.js` | +| `POST /v1/chat/completions` | OpenAI Chat | `src/sse/handlers/chat.ts` | | `POST /v1/messages` | Claude Messages | Same handler (auto-detected) | | `POST /v1/responses` | OpenAI Responses | `open-sse/handlers/responsesHandler.js` | | `POST /v1/embeddings` | OpenAI Embeddings | `open-sse/handlers/embeddings.js` | @@ -714,7 +714,7 @@ Files are written to `/logs//` for each request session. Runtime visibility sources: -- console logs from `src/sse/utils/logger.js` +- console logs from `src/sse/utils/logger.ts` - per-request usage aggregates in `usage.json` - textual request status log in `log.txt` - optional deep request/translation logs under `logs/` when `ENABLE_REQUEST_LOGS=true` @@ -751,7 +751,7 @@ Environment variables actively used by code: 4. Cloud behavior depends on correct `NEXT_PUBLIC_BASE_URL` and cloud endpoint reachability. 5. The `open-sse/` directory is published as the `@omniroute/open-sse` **npm workspace package**. Source code imports it via `@omniroute/open-sse/...` (resolved by Next.js `transpilePackages`). File paths in this document still use the directory name `open-sse/` for consistency. 6. Charts in the dashboard use **Recharts** (SVG-based) for accessible, interactive visualizations (bar charts, donut charts). -7. E2E tests use **Playwright** (`tests/e2e/`), run via `npm run test:e2e`. Unit tests use **Node.js test runner** (`tests/unit/`), run via `npm run test:plan3`. +7. E2E tests use **Playwright** (`tests/e2e/`), run via `npm run test:e2e`. Unit tests use **Node.js test runner** (`tests/unit/`), run via `npm run test:plan3`. Source code under `src/` is **TypeScript** (`.ts`/`.tsx`); the `open-sse/` workspace remains JavaScript (`.js`). ## Operational Verification Checklist diff --git a/docs/CODEBASE_DOCUMENTATION.md b/docs/CODEBASE_DOCUMENTATION.md index 0cdb1e15ed5e..aac7f45767b6 100644 --- a/docs/CODEBASE_DOCUMENTATION.md +++ b/docs/CODEBASE_DOCUMENTATION.md @@ -450,7 +450,7 @@ logs/ | Directory | Purpose | | ------------- | ---------------------------------------------------------------------- | | `src/app/` | Web UI, API routes, Express middleware, OAuth callback handlers | -| `src/lib/` | Database access (`localDb.js`, `usageDb.js`), authentication, shared | +| `src/lib/` | Database access (`localDb.ts`, `usageDb.ts`), authentication, shared | | `src/mitm/` | Man-in-the-middle proxy utilities for intercepting provider traffic | | `src/models/` | Database model definitions | | `src/shared/` | Wrappers around open-sse functions (provider, stream, error, etc.) | diff --git a/docs/TASKS.md b/docs/TASKS.md index b2fb371d9052..fff6dd4eadd4 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -63,23 +63,23 @@ ### Settings Page -- [ ] `settings/page.js` — Adicionar tab "Resilience" (icon: `health_and_safety`) entre Routing e Pricing +- [ ] `settings/page.tsx` — Adicionar tab "Resilience" (icon: `health_and_safety`) entre Routing e Pricing ### Novos Componentes -- [ ] Criar `ResilienceTab.js` — Layout com 3 cards -- [ ] Criar `ProviderProfilesCard.js` — Toggle OAuth/API Key, inputs para cooldowns -- [ ] Criar `CircuitBreakerCard.js` — Status real-time per-provider, auto-refresh 5s, botão reset -- [ ] Criar `RateLimitOverviewCard.js` — Tabela providers × accounts × cooldown +- [ ] Criar `ResilienceTab.tsx` — Layout com 3 cards +- [ ] Criar `ProviderProfilesCard.tsx` — Toggle OAuth/API Key, inputs para cooldowns +- [ ] Criar `CircuitBreakerCard.tsx` — Status real-time per-provider, auto-refresh 5s, botão reset +- [ ] Criar `RateLimitOverviewCard.tsx` — Tabela providers × accounts × cooldown ### API Routes -- [ ] Criar `api/resilience/route.js` — GET (estado completo) + PATCH (salvar perfis) -- [ ] Criar `api/resilience/reset/route.js` — POST (resetar breakers + cooldowns) +- [ ] Criar `api/resilience/route.ts` — GET (estado completo) + PATCH (salvar perfis) +- [ ] Criar `api/resilience/reset/route.ts` — POST (resetar breakers + cooldowns) ### Migração -- [ ] Avaliar se `PoliciesPanel.js` pode ser removido ou simplificado após nova aba +- [ ] Avaliar se `PoliciesPanel.tsx` pode ser removido ou simplificado após nova aba --- diff --git a/open-sse/services/tokenRefresh.js b/open-sse/services/tokenRefresh.js index 298c6cb2031b..6ac7ec4062c0 100644 --- a/open-sse/services/tokenRefresh.js +++ b/open-sse/services/tokenRefresh.js @@ -312,6 +312,23 @@ export async function refreshQwenToken(refreshToken, log) { }; } else { const errorText = await response.text().catch(() => ""); + + // Detect unrecoverable invalid_request (expired/revoked refresh token or bad client_id) + let errorCode = null; + try { + const parsed = JSON.parse(errorText); + errorCode = parsed?.error; + } catch { + // not JSON, ignore + } + + if (errorCode === "invalid_request") { + log?.error?.("TOKEN_REFRESH", "Qwen refresh token is invalid or expired. Re-authentication required.", { + status: response.status, + }); + return { error: "invalid_request" }; + } + log?.warn?.("TOKEN_REFRESH", `Error with Qwen endpoint`, { status: response.status, error: errorText, @@ -703,7 +720,8 @@ export function supportsTokenRefresh(provider) { * Callers should stop retrying and request re-authentication. */ export function isUnrecoverableRefreshError(result) { - return result && typeof result === "object" && result.error === "refresh_token_reused"; + return result && typeof result === "object" && + (result.error === "refresh_token_reused" || result.error === "invalid_request"); } /** diff --git a/src/app/api/providers/[id]/test/route.ts b/src/app/api/providers/[id]/test/route.ts index bd4270eb1cf2..70cd072a10bf 100644 --- a/src/app/api/providers/[id]/test/route.ts +++ b/src/app/api/providers/[id]/test/route.ts @@ -44,17 +44,15 @@ const OAUTH_TEST_CONFIG = { extraHeaders: { "User-Agent": "OmniRoute", Accept: "application/vnd.github+json" }, }, iflow: { - url: "https://iflow.cn/api/oauth/getUserInfo", - method: "GET", - authHeader: "Authorization", - authPrefix: "Bearer ", + // iFlow's getUserInfo endpoint returns 400 without a specific format. + // Use checkExpiry instead — actual connectivity is validated via real requests. + checkExpiry: true, refreshable: true, }, qwen: { - url: "https://portal.qwen.ai/v1/models", - method: "GET", - authHeader: "Authorization", - authPrefix: "Bearer ", + // portal.qwen.ai/v1/models returns 404 — endpoint no longer exists. + // Use checkExpiry instead — actual connectivity is validated via real requests. + checkExpiry: true, refreshable: true, }, cursor: { @@ -70,15 +68,11 @@ const OAUTH_TEST_CONFIG = { checkExpiry: true, }, cline: { - url: "https://api.cline.bot/api/v1/models", - method: "GET", - authHeader: "Authorization", - authPrefix: "Bearer ", + // Cline's /api/v1/models endpoint frequently returns stale auth errors even + // with fresh tokens. Use checkExpiry instead — actual connectivity is validated + // via real requests. + checkExpiry: true, refreshable: true, - extraHeaders: { - "HTTP-Referer": "https://cline.bot", - "X-Title": "Cline", - }, }, kiro: { checkExpiry: true, @@ -520,12 +514,12 @@ export async function testSingleConnection(connectionId) { const startTime = Date.now(); const runtime = await getProviderRuntimeStatus(connection.provider); - if (runtime?.diagnosis) { + if ((runtime as any)?.diagnosis) { result = { valid: false, - error: runtime.error, + error: (runtime as any).error, refreshed: false, - diagnosis: runtime.diagnosis, + diagnosis: (runtime as any).diagnosis, }; } else if (connection.authType === "apikey") { result = await testApiKeyConnection(connection); @@ -543,7 +537,7 @@ export async function testSingleConnection(connectionId) { ? makeDiagnosis("ok", "local", null, null) : classifyFailure({ error: result.error, statusCode: result.statusCode })); - const updateData = { + const updateData: Record = { testStatus: result.valid ? "active" : "error", lastError: result.valid ? null : result.error, lastErrorAt: result.valid ? null : now,