Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a3232bc00c | |||
| b72292eeea | |||
| c7a77d6f62 | |||
| e21fc439b8 | |||
| 6d129072fe | |||
| 7296a6461a | |||
| e98d834016 | |||
| c164ab877f | |||
| c9a26da798 | |||
| c9f9892095 | |||
| 94175fa04a | |||
| 3d4b2ca255 | |||
| d6b95e687d | |||
| a553e0aa88 | |||
| ccd4ab6c8d | |||
| 7729896dc2 | |||
| 3c79cd34eb | |||
| f1319448ac | |||
| 52b025ca5a | |||
| 859f6db36c | |||
| faedce9b4a | |||
| 03c7f66603 | |||
| 3607eceb41 | |||
| 6bff90edf7 | |||
| 5ef97b2622 | |||
| 9739f41366 | |||
| 7ec5a0a527 | |||
| a811c66230 | |||
| 62e8369a12 | |||
| 5291cd3321 | |||
| 00253b795c | |||
| 1d6a918154 | |||
| 7152691755 | |||
| e6c31bf890 | |||
| d0dd7b4b4c | |||
| dc50abc060 | |||
| 835c860585 | |||
| 81f0f4d97c | |||
| f1deb98348 | |||
| d8b409c257 | |||
| bc38805b51 | |||
| 22cf732f08 | |||
| 3b3dce07be | |||
| 127cdc9143 | |||
| d3a4f6be98 | |||
| 23ee13818a | |||
| 9eeeb90dae |
@@ -57,6 +57,11 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
|
||||
# ALL_PROXY=socks5://127.0.0.1:7890
|
||||
# NO_PROXY=localhost,127.0.0.1
|
||||
|
||||
# TLS fingerprint spoofing (opt-in) — mimics Chrome 124 TLS handshake via wreq-js
|
||||
# Reduces risk of JA3/JA4 fingerprint-based blocking by providers (e.g., Google)
|
||||
# Requires wreq-js to be installed (included in dependencies)
|
||||
# ENABLE_TLS_FINGERPRINT=true
|
||||
|
||||
# Optional CLI runtime overrides (Docker/host integration)
|
||||
# CLI_MODE=auto
|
||||
# CLI_EXTRA_PATHS=/host-cli/bin
|
||||
|
||||
@@ -20,6 +20,10 @@ updates:
|
||||
update-types: ["version-update:semver-major"]
|
||||
- dependency-name: "next"
|
||||
update-types: ["version-update:semver-major"]
|
||||
- dependency-name: "eslint"
|
||||
update-types: ["version-update:semver-major"]
|
||||
- dependency-name: "eslint-config-next"
|
||||
update-types: ["version-update:semver-major"]
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
|
||||
+12
-12
@@ -15,8 +15,8 @@ jobs:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
@@ -27,8 +27,8 @@ jobs:
|
||||
name: Security Audit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
@@ -45,8 +45,8 @@ jobs:
|
||||
matrix:
|
||||
node-version: [20, 22]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
@@ -64,8 +64,8 @@ jobs:
|
||||
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
@@ -80,8 +80,8 @@ jobs:
|
||||
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
@@ -99,8 +99,8 @@ jobs:
|
||||
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Extract version from release tag
|
||||
id: version
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Update Docker Hub description
|
||||
uses: peter-evans/dockerhub-description@v4
|
||||
uses: peter-evans/dockerhub-description@v5
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
@@ -14,10 +14,10 @@ jobs:
|
||||
environment: NPM_TOKEN
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
@@ -85,3 +85,6 @@ blob-report/
|
||||
cloud/
|
||||
omnirouteCloud/
|
||||
omnirouteSite/
|
||||
|
||||
# Security Analysis (standalone project with own git)
|
||||
security-analysis/
|
||||
|
||||
@@ -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 |
|
||||
| `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.js` is a **re-export layer only** — all 27+ consumers import from it,
|
||||
`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
|
||||
|
||||
|
||||
@@ -7,6 +7,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
---
|
||||
|
||||
## [0.8.5] — 2026-02-17
|
||||
|
||||
### Added
|
||||
|
||||
- 🔒 **TLS fingerprint spoofing** — Implement browser-like TLS fingerprinting via `wreq-js` to bypass bot detection on providers that enforce TLS client fingerprint checks (`3dd0cc1`, PR #52)
|
||||
- 💾 **SQLite proxy log persistence** — Proxy request/response logs now persist to SQLite database, surviving server restarts. Previously, logs were lost on restart (`f1664fe`, PR #53)
|
||||
- 📋 **Unified test logging** — Shared `Logger` + Proxy logging infrastructure for all provider connection test flows. Consistent log formatting across batch and individual tests (`bce302e`, PR #55)
|
||||
|
||||
### Refactored
|
||||
|
||||
- 🔷 **Full TypeScript migration — `src/`** — Migrated the entire `src/` directory from JavaScript to TypeScript. All `.js`/`.jsx` files converted to `.ts`/`.tsx` with proper type annotations across API routes, lib modules, components, services, stores, domain layer, and shared utilities (`d0ca595`)
|
||||
- **Wave 1**: Shared component interfaces + EventTarget fixes (`dfdd2a2`)
|
||||
- **Wave 2**: Utils & services typed fields, Zustand stores, logger, sync scheduler (`89dd107`, `b2907cd`)
|
||||
- **Wave 3a**: Lib layer, DB, compliance, domain layer typed (`9e13fe2`)
|
||||
- **Wave 3b**: Usage, CLI runtime, SSE auth/logger typed (`a291abd`)
|
||||
- **Wave 3c**: OAuth services + server utils typed (`d62cf8d`)
|
||||
- **Wave 4a**: 7 API routes — providers, cli-tools, oauth (`7cdb923`)
|
||||
- **Wave 4b**: 7 more API routes — providers, test, usage, nodes (`5592c2e`)
|
||||
- **Wave 4c**: 8 files — components, SSE handlers, services (`d8ce9dc`)
|
||||
- **Dashboard hardening**: Resolve all TypeScript errors across dashboard pages (`7a463a3`, PR #61)
|
||||
- 🔷 **Full TypeScript migration — `open-sse/`** — Migrated all 94 `.js` files in the SSE routing engine to TypeScript (PR #62)
|
||||
- **Phase 1**: Rename all 94 `.js` → `.ts` files (`256e443`)
|
||||
- **Phase 6**: Reduce `@ts-ignore` from 231 → 186 with targeted fixes (`6a54b84`)
|
||||
- **Phase 7**: Eliminate ALL `@ts-ignore` annotations (186 → 0) and ALL TypeScript errors (237 → 0) — zero `@ts-ignore`, zero errors (`7b37a3c`)
|
||||
- Typing strategies: `Record<string, any>` for dynamic objects, optional function params, `as any` casts for custom Error/Array properties, `declare var EdgeRuntime` for edge compatibility, proper `fs`/`path` imports
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛 **Qwen token refresh** — Detect `invalid_request` as unrecoverable error and switch broken test endpoints to `checkExpiry` method instead of failing silently (`1e0ffbc`, PR #60)
|
||||
- 🐛 **VPS batch test compatibility** — Eliminate HTTP self-calls in batch provider connection tests for VPS environments where localhost is unreachable (`a3bbbb5`, PR #54)
|
||||
- 🐛 **E2E test assertions** — Correct API endpoints and response format assertions in end-to-end tests (`92b5e66`)
|
||||
- 🐛 **CI coverage thresholds** — Lower coverage thresholds, use production server for E2E, block ESLint major upgrades from breaking CI (`3ca4b6b`, PR #51)
|
||||
|
||||
### Changed
|
||||
|
||||
- 📖 **Documentation update** — Updated all documentation to reflect JS → TS migration, corrected file extensions and import paths (`7ff8aa2`)
|
||||
- ⬆️ **CI/CD** — Bump `actions/checkout` v4 → v6, `actions/setup-node` v4 → v6, `peter-evans/dockerhub-description` v4 → v5
|
||||
|
||||
### Dependencies
|
||||
|
||||
- ⬆️ `undici` 7.21.0 → 7.22.0 (production)
|
||||
- ⬆️ `actions/checkout` 4 → 6
|
||||
- ⬆️ `actions/setup-node` 4 → 6
|
||||
- ⬆️ `peter-evans/dockerhub-description` 4 → 5
|
||||
- 🚫 `eslint` 10.0.0 blocked — major version incompatible with `eslint-config-next`
|
||||
|
||||
---
|
||||
|
||||
## [0.8.0] — 2026-02-16
|
||||
|
||||
### Added
|
||||
@@ -143,6 +191,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
---
|
||||
|
||||
[0.8.5]: https://github.com/diegosouzapw/OmniRoute/compare/v0.8.0...v0.8.5
|
||||
[0.8.0]: https://github.com/diegosouzapw/OmniRoute/compare/v0.7.0...v0.8.0
|
||||
[0.7.0]: https://github.com/diegosouzapw/OmniRoute/compare/v0.6.0...v0.7.0
|
||||
[0.6.0]: https://github.com/diegosouzapw/OmniRoute/compare/v0.5.0...v0.6.0
|
||||
|
||||
+25
-25
@@ -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)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
**Never stop coding. Auto-route to FREE & cheap AI models with smart fallback.**
|
||||
|
||||
**36+ Providers • Embeddings • Image Generation • Think Tag Parsing**
|
||||
**36+ Providers • Embeddings • Image Generation • Audio • Reranking • Full TypeScript**
|
||||
|
||||
**Free AI Provider for OpenClaw.**
|
||||
|
||||
@@ -158,31 +158,69 @@ docker compose --profile cli up -d
|
||||
| Image | Tag | Size | Description |
|
||||
| ------------------------ | -------- | ------ | --------------------- |
|
||||
| `diegosouzapw/omniroute` | `latest` | ~250MB | Latest stable release |
|
||||
| `diegosouzapw/omniroute` | `0.6.0` | ~250MB | Current version |
|
||||
| `diegosouzapw/omniroute` | `0.8.5` | ~250MB | Current version |
|
||||
|
||||
---
|
||||
|
||||
## 💡 Key Features
|
||||
|
||||
| Feature | What It Does |
|
||||
| ------------------------------- | --------------------------------------------- |
|
||||
| 🎯 **Smart 3-Tier Fallback** | Auto-route: Subscription → Cheap → Free |
|
||||
| 📊 **Real-Time Quota Tracking** | Live token count + reset countdown |
|
||||
| 🔄 **Format Translation** | OpenAI ↔ Claude ↔ Gemini seamless |
|
||||
| 👥 **Multi-Account Support** | Multiple accounts per provider |
|
||||
| 🔄 **Auto Token Refresh** | OAuth tokens refresh automatically |
|
||||
| 🎨 **Custom Combos** | Create unlimited model combinations |
|
||||
| 🧩 **Custom Models** | Add any model ID to any provider |
|
||||
| 📝 **Request Logging** | Debug mode with full request/response logs |
|
||||
| 💾 **Cloud Sync** | Sync config across devices |
|
||||
| 📊 **Usage Analytics** | Track tokens, cost, trends over time |
|
||||
| 🌐 **Deploy Anywhere** | Localhost, VPS, Docker, Cloudflare Workers |
|
||||
| 🔌 **Circuit Breaker** | Auto-open/close per-provider with cooldowns |
|
||||
| 🛡️ **Anti-Thundering Herd** | Mutex + auto rate-limit for API key providers |
|
||||
| 🧠 **Semantic Cache** | Two-tier cache reduces cost & latency |
|
||||
| ⚡ **Request Idempotency** | 5s dedup window for duplicate requests |
|
||||
| 📈 **Progress Tracking** | Opt-in SSE progress events for streaming |
|
||||
| 🧪 **LLM Evaluations** | Golden set testing with 4 match strategies |
|
||||
### 🧠 Core Routing & Intelligence
|
||||
|
||||
| Feature | What It Does |
|
||||
| ------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| 🎯 **Smart 4-Tier Fallback** | Auto-route: Subscription → API Key → Cheap → Free |
|
||||
| 📊 **Real-Time Quota Tracking** | Live token count + reset countdown per provider |
|
||||
| 🔄 **Format Translation** | OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro seamless |
|
||||
| 👥 **Multi-Account Support** | Multiple accounts per provider with P2C selection |
|
||||
| 🔄 **Auto Token Refresh** | OAuth tokens refresh automatically with retry |
|
||||
| 🎨 **Custom Combos** | 6 strategies: priority, weighted, round-robin, random, least-used, cost-optimized |
|
||||
| 🧩 **Custom Models** | Add any model ID to any provider |
|
||||
| 🌐 **Wildcard Router** | Route `provider/*` patterns to any provider dynamically |
|
||||
| 🧠 **Thinking Budget** | Passthrough, auto, custom, and adaptive modes for reasoning models |
|
||||
| 💬 **System Prompt Injection** | Global system prompt applied across all requests |
|
||||
| 📄 **Responses API** | Full OpenAI Responses API (`/v1/responses`) support for Codex |
|
||||
|
||||
### 🎵 Multi-Modal APIs
|
||||
|
||||
| Feature | What It Does |
|
||||
| -------------------------- | --------------------------------------------------- |
|
||||
| 🖼️ **Image Generation** | `/v1/images/generations` — 4 providers, 9+ models |
|
||||
| 📐 **Embeddings** | `/v1/embeddings` — 6 providers, 9+ models |
|
||||
| 🎤 **Audio Transcription** | `/v1/audio/transcriptions` — Whisper-compatible |
|
||||
| 🔊 **Text-to-Speech** | `/v1/audio/speech` — Multi-provider audio synthesis |
|
||||
| 🛡️ **Moderations** | `/v1/moderations` — Content safety checks |
|
||||
| 🔀 **Reranking** | `/v1/rerank` — Document relevance reranking |
|
||||
|
||||
### 🛡️ Resilience & Security
|
||||
|
||||
| Feature | What It Does |
|
||||
| ------------------------------- | ------------------------------------------------------------ |
|
||||
| 🔌 **Circuit Breaker** | Auto-open/close per-provider with configurable thresholds |
|
||||
| 🛡️ **Anti-Thundering Herd** | Mutex + semaphore rate-limit for API key providers |
|
||||
| 🧠 **Semantic Cache** | Two-tier cache (signature + semantic) reduces cost & latency |
|
||||
| ⚡ **Request Idempotency** | 5s dedup window for duplicate requests |
|
||||
| 🔒 **TLS Fingerprint Spoofing** | Bypass TLS-based bot detection via wreq-js |
|
||||
| 🌐 **IP Filtering** | Allowlist/blocklist for API access control |
|
||||
| 📋 **Compliance Audit Log** | Tamper-proof request logs with opt-out per API key |
|
||||
|
||||
### 📊 Observability & Analytics
|
||||
|
||||
| Feature | What It Does |
|
||||
| ------------------------ | ------------------------------------------------------ |
|
||||
| 📝 **Request Logging** | Debug mode with full request/response logs |
|
||||
| 💾 **SQLite Proxy Logs** | Persistent proxy logs survive server restarts |
|
||||
| 📊 **Usage Analytics** | Track tokens, cost, trends over time |
|
||||
| 📈 **Progress Tracking** | Opt-in SSE progress events for streaming |
|
||||
| 🧪 **LLM Evaluations** | Golden set testing with 4 match strategies |
|
||||
| 🔍 **Request Telemetry** | p50/p95/p99 latency aggregation + X-Request-Id tracing |
|
||||
|
||||
### ☁️ Deployment & Sync
|
||||
|
||||
| Feature | What It Does |
|
||||
| ------------------------- | ------------------------------------------------- |
|
||||
| 💾 **Cloud Sync** | Sync config across devices via Cloudflare Workers |
|
||||
| 🌐 **Deploy Anywhere** | Localhost, VPS, Docker, Cloudflare Workers |
|
||||
| 🔑 **API Key Management** | Generate, rotate, and scope API keys per provider |
|
||||
|
||||
---
|
||||
|
||||
@@ -231,9 +269,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,16 +292,17 @@ registerSuite({
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
- **Runtime**: Node.js 20+
|
||||
- **Language**: TypeScript 5.9 — **100% TypeScript** across `src/` and `open-sse/` (v0.8.5)
|
||||
- **Framework**: Next.js 16 + React 19 + Tailwind CSS 4
|
||||
- **Database**: LowDB (JSON) + SQLite (domain state)
|
||||
- **Database**: LowDB (JSON) + SQLite (domain state + proxy logs)
|
||||
- **Streaming**: Server-Sent Events (SSE)
|
||||
- **Auth**: OAuth 2.0 (PKCE) + JWT + API Keys
|
||||
- **Testing**: Node.js test runner (320+ unit tests)
|
||||
- **CI/CD**: GitHub Actions (auto npm publish on release)
|
||||
- **Testing**: Node.js test runner (368+ unit tests)
|
||||
- **CI/CD**: GitHub Actions (auto npm publish + Docker Hub on release)
|
||||
- **Website**: [omniroute.online](https://omniroute.online)
|
||||
- **Package**: [npmjs.com/package/omniroute](https://www.npmjs.com/package/omniroute)
|
||||
- **Docker**: [hub.docker.com/r/diegosouzapw/omniroute](https://hub.docker.com/r/diegosouzapw/omniroute)
|
||||
- **Resilience**: Circuit breaker, exponential backoff, anti-thundering herd
|
||||
- **Resilience**: Circuit breaker, exponential backoff, anti-thundering herd, TLS spoofing
|
||||
|
||||
---
|
||||
|
||||
@@ -308,7 +347,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
|
||||
|
||||
```bash
|
||||
# Create a release — npm publish happens automatically
|
||||
gh release create v0.8.0 --title "v0.8.0" --generate-notes
|
||||
gh release create v0.8.5 --title "v0.8.5" --generate-notes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
+81
-81
@@ -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,89 +166,89 @@ Management domains:
|
||||
|
||||
Main flow modules:
|
||||
|
||||
- Entry: `src/sse/handlers/chat.js`
|
||||
- Core orchestration: `open-sse/handlers/chatCore.js`
|
||||
- Entry: `src/sse/handlers/chat.ts`
|
||||
- Core orchestration: `open-sse/handlers/chatCore.ts`
|
||||
- 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`
|
||||
- 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`
|
||||
- Usage extraction/normalization: `open-sse/utils/usageTracking.js`
|
||||
- Think tag parser: `open-sse/utils/thinkTagParser.js`
|
||||
- Embedding handler: `open-sse/handlers/embeddings.js`
|
||||
- Embedding provider registry: `open-sse/config/embeddingRegistry.js`
|
||||
- Image generation handler: `open-sse/handlers/imageGeneration.js`
|
||||
- Image provider registry: `open-sse/config/imageRegistry.js`
|
||||
- Format detection/provider config: `open-sse/services/provider.ts`
|
||||
- Model parse/resolve: `src/sse/services/model.ts`, `open-sse/services/model.ts`
|
||||
- Account fallback logic: `open-sse/services/accountFallback.ts`
|
||||
- Translation registry: `open-sse/translator/index.ts`
|
||||
- Stream transformations: `open-sse/utils/stream.ts`, `open-sse/utils/streamHandler.ts`
|
||||
- Usage extraction/normalization: `open-sse/utils/usageTracking.ts`
|
||||
- Think tag parser: `open-sse/utils/thinkTagParser.ts`
|
||||
- Embedding handler: `open-sse/handlers/embeddings.ts`
|
||||
- Embedding provider registry: `open-sse/config/embeddingRegistry.ts`
|
||||
- Image generation handler: `open-sse/handlers/imageGeneration.ts`
|
||||
- Image provider registry: `open-sse/config/imageRegistry.ts`
|
||||
|
||||
Services (business logic):
|
||||
|
||||
- Account selection/scoring: `open-sse/services/accountSelector.js`
|
||||
- Context lifecycle management: `open-sse/services/contextManager.js`
|
||||
- IP filter enforcement: `open-sse/services/ipFilter.js`
|
||||
- Session tracking: `open-sse/services/sessionManager.js`
|
||||
- Request deduplication: `open-sse/services/signatureCache.js`
|
||||
- System prompt injection: `open-sse/services/systemPrompt.js`
|
||||
- Thinking budget management: `open-sse/services/thinkingBudget.js`
|
||||
- Wildcard model routing: `open-sse/services/wildcardRouter.js`
|
||||
- Rate limit management: `open-sse/services/rateLimitManager.js`
|
||||
- Circuit breaker: `open-sse/services/circuitBreaker.js`
|
||||
- Account selection/scoring: `open-sse/services/accountSelector.ts`
|
||||
- Context lifecycle management: `open-sse/services/contextManager.ts`
|
||||
- IP filter enforcement: `open-sse/services/ipFilter.ts`
|
||||
- Session tracking: `open-sse/services/sessionManager.ts`
|
||||
- Request deduplication: `open-sse/services/signatureCache.ts`
|
||||
- System prompt injection: `open-sse/services/systemPrompt.ts`
|
||||
- Thinking budget management: `open-sse/services/thinkingBudget.ts`
|
||||
- Wildcard model routing: `open-sse/services/wildcardRouter.ts`
|
||||
- Rate limit management: `open-sse/services/rateLimitManager.ts`
|
||||
- Circuit breaker: `open-sse/services/circuitBreaker.ts`
|
||||
|
||||
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)
|
||||
- Outbound proxy support via `open-sse/utils/proxyFetch.ts` (env vars) and `open-sse/utils/networkProxy.ts` (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`)
|
||||
|
||||
@@ -327,7 +327,7 @@ flowchart TD
|
||||
Q -- No --> R[Return all unavailable]
|
||||
```
|
||||
|
||||
Fallback decisions are driven by `open-sse/services/accountFallback.js` using status codes and error-message heuristics.
|
||||
Fallback decisions are driven by `open-sse/services/accountFallback.ts` using status codes and error-message heuristics.
|
||||
|
||||
## OAuth Onboarding and Token Refresh Lifecycle
|
||||
|
||||
@@ -359,7 +359,7 @@ sequenceDiagram
|
||||
Test-->>UI: validation result
|
||||
```
|
||||
|
||||
Refresh during live traffic is executed inside `open-sse/handlers/chatCore.js` via executor `refreshCredentials()`.
|
||||
Refresh during live traffic is executed inside `open-sse/handlers/chatCore.ts` via executor `refreshCredentials()`.
|
||||
|
||||
## Cloud Sync Lifecycle (Enable / Sync / Disable)
|
||||
|
||||
@@ -559,25 +559,25 @@ flowchart LR
|
||||
|
||||
### Routing and Execution Core
|
||||
|
||||
- `src/sse/handlers/chat.js`: request parse, combo handling, account selection loop
|
||||
- `open-sse/handlers/chatCore.js`: translation, executor dispatch, retry/refresh handling, stream setup
|
||||
- `src/sse/handlers/chat.ts`: request parse, combo handling, account selection loop
|
||||
- `open-sse/handlers/chatCore.ts`: translation, executor dispatch, retry/refresh handling, stream setup
|
||||
- `open-sse/executors/*`: provider-specific network and format behavior
|
||||
|
||||
### Translation Registry and Format Converters
|
||||
|
||||
- `open-sse/translator/index.js`: translator registry and orchestration
|
||||
- `open-sse/translator/index.ts`: translator registry and orchestration
|
||||
- Request translators: `open-sse/translator/request/*`
|
||||
- Response translators: `open-sse/translator/response/*`
|
||||
- Format constants: `open-sse/translator/formats.js`
|
||||
- Format constants: `open-sse/translator/formats.ts`
|
||||
|
||||
### 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)
|
||||
|
||||
Each provider has a specialized executor extending `BaseExecutor` (in `open-sse/executors/base.js`), which provides URL building, header construction, retry with exponential backoff, credential refresh hooks, and the `execute()` orchestration method.
|
||||
Each provider has a specialized executor extending `BaseExecutor` (in `open-sse/executors/base.ts`), which provides URL building, header construction, retry with exponential backoff, credential refresh hooks, and the `execute()` orchestration method.
|
||||
|
||||
| Executor | Provider(s) | Special Handling |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
|
||||
@@ -648,12 +648,12 @@ 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` |
|
||||
| `POST /v1/responses` | OpenAI Responses | `open-sse/handlers/responsesHandler.ts` |
|
||||
| `POST /v1/embeddings` | OpenAI Embeddings | `open-sse/handlers/embeddings.ts` |
|
||||
| `GET /v1/embeddings` | Model listing | API route |
|
||||
| `POST /v1/images/generations` | OpenAI Images | `open-sse/handlers/imageGeneration.js` |
|
||||
| `POST /v1/images/generations` | OpenAI Images | `open-sse/handlers/imageGeneration.ts` |
|
||||
| `GET /v1/images/generations` | Model listing | API route |
|
||||
| `POST /v1/providers/{provider}/chat/completions` | OpenAI Chat | Dedicated per-provider with model validation |
|
||||
| `POST /v1/providers/{provider}/embeddings` | OpenAI Embeddings | Dedicated per-provider with model validation |
|
||||
@@ -668,11 +668,11 @@ Translations are selected dynamically based on source payload shape and provider
|
||||
|
||||
## Bypass Handler
|
||||
|
||||
The bypass handler (`open-sse/utils/bypassHandler.js`) intercepts known "throwaway" requests from Claude CLI — warmup pings, title extractions, and token counts — and returns a **fake response** without consuming upstream provider tokens. This is triggered only when `User-Agent` contains `claude-cli`.
|
||||
The bypass handler (`open-sse/utils/bypassHandler.ts`) intercepts known "throwaway" requests from Claude CLI — warmup pings, title extractions, and token counts — and returns a **fake response** without consuming upstream provider tokens. This is triggered only when `User-Agent` contains `claude-cli`.
|
||||
|
||||
## Request Logger Pipeline
|
||||
|
||||
The request logger (`open-sse/utils/requestLogger.js`) provides a 7-stage debug logging pipeline, disabled by default, enabled via `ENABLE_REQUEST_LOGS=true`:
|
||||
The request logger (`open-sse/utils/requestLogger.ts`) provides a 7-stage debug logging pipeline, disabled by default, enabled via `ENABLE_REQUEST_LOGS=true`:
|
||||
|
||||
```
|
||||
1_req_client.json → 2_req_source.json → 3_req_openai.json → 4_req_target.json
|
||||
@@ -714,7 +714,7 @@ Files are written to `<repo>/logs/<session>/` 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`
|
||||
@@ -746,12 +746,12 @@ Environment variables actively used by code:
|
||||
## Known Architectural Notes
|
||||
|
||||
1. `usageDb` and `localDb` now share the same base directory policy (`DATA_DIR` -> `XDG_CONFIG_HOME/omniroute` -> `~/.omniroute`) with legacy file migration.
|
||||
2. `/api/v1/route.js` returns a static model list and is not the main models source used by `/v1/models`.
|
||||
2. `/api/v1/route.ts` returns a static model list and is not the main models source used by `/v1/models`.
|
||||
3. Request logger writes full headers/body when enabled; treat log directory as sensitive.
|
||||
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
|
||||
|
||||
|
||||
@@ -110,18 +110,18 @@ The **single source of truth** for all provider configuration.
|
||||
|
||||
| File | Purpose |
|
||||
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `constants.js` | `PROVIDERS` object with base URLs, OAuth credentials (defaults), headers, and default system prompts for every provider. Also defines `HTTP_STATUS`, `ERROR_TYPES`, `COOLDOWN_MS`, `BACKOFF_CONFIG`, and `SKIP_PATTERNS`. |
|
||||
| `credentialLoader.js` | Loads external credentials from `data/provider-credentials.json` and merges them over the hardcoded defaults in `PROVIDERS`. Keeps secrets out of source control while maintaining backwards compatibility. |
|
||||
| `providerModels.js` | Central model registry: maps provider aliases → model IDs. Functions like `getModels()`, `getProviderByAlias()`. |
|
||||
| `codexInstructions.js` | System instructions injected into Codex requests (editing constraints, sandbox rules, approval policies). |
|
||||
| `defaultThinkingSignature.js` | Default "thinking" signatures for Claude and Gemini models. |
|
||||
| `ollamaModels.js` | Schema definition for local Ollama models (name, size, family, quantization). |
|
||||
| `constants.ts` | `PROVIDERS` object with base URLs, OAuth credentials (defaults), headers, and default system prompts for every provider. Also defines `HTTP_STATUS`, `ERROR_TYPES`, `COOLDOWN_MS`, `BACKOFF_CONFIG`, and `SKIP_PATTERNS`. |
|
||||
| `credentialLoader.ts` | Loads external credentials from `data/provider-credentials.json` and merges them over the hardcoded defaults in `PROVIDERS`. Keeps secrets out of source control while maintaining backwards compatibility. |
|
||||
| `providerModels.ts` | Central model registry: maps provider aliases → model IDs. Functions like `getModels()`, `getProviderByAlias()`. |
|
||||
| `codexInstructions.ts` | System instructions injected into Codex requests (editing constraints, sandbox rules, approval policies). |
|
||||
| `defaultThinkingSignature.ts` | Default "thinking" signatures for Claude and Gemini models. |
|
||||
| `ollamaModels.ts` | Schema definition for local Ollama models (name, size, family, quantization). |
|
||||
|
||||
#### Credential Loading Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["App starts"] --> B["constants.js defines PROVIDERS\nwith hardcoded defaults"]
|
||||
A["App starts"] --> B["constants.ts defines PROVIDERS\nwith hardcoded defaults"]
|
||||
B --> C{"data/provider-credentials.json\nexists?"}
|
||||
C -->|Yes| D["credentialLoader reads JSON"]
|
||||
C -->|No| E["Use hardcoded defaults"]
|
||||
@@ -194,15 +194,15 @@ classDiagram
|
||||
|
||||
| Executor | Provider | Key Specializations |
|
||||
| ---------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| `base.js` | — | Abstract base: URL building, headers, retry logic, credential refresh |
|
||||
| `default.js` | Claude, Gemini, OpenAI, GLM, Kimi, MiniMax | Generic OAuth token refresh for standard providers |
|
||||
| `antigravity.js` | Google Cloud Code | Project/session ID generation, multi-URL fallback, custom retry parsing from error messages ("reset after 2h7m23s") |
|
||||
| `cursor.js` | Cursor IDE | **Most complex**: SHA-256 checksum auth, Protobuf request encoding, binary EventStream → SSE response parsing |
|
||||
| `codex.js` | OpenAI Codex | Injects system instructions, manages thinking levels, removes unsupported parameters |
|
||||
| `gemini-cli.js` | Google Gemini CLI | Custom URL building (`streamGenerateContent`), Google OAuth token refresh |
|
||||
| `github.js` | GitHub Copilot | Dual token system (GitHub OAuth + Copilot token), VSCode header mimicking |
|
||||
| `kiro.js` | AWS CodeWhisperer | AWS EventStream binary parsing, AMZN event frames, token estimation |
|
||||
| `index.js` | — | Factory: maps provider name → executor class, with default fallback |
|
||||
| `base.ts` | — | Abstract base: URL building, headers, retry logic, credential refresh |
|
||||
| `default.ts` | Claude, Gemini, OpenAI, GLM, Kimi, MiniMax | Generic OAuth token refresh for standard providers |
|
||||
| `antigravity.ts` | Google Cloud Code | Project/session ID generation, multi-URL fallback, custom retry parsing from error messages ("reset after 2h7m23s") |
|
||||
| `cursor.ts` | Cursor IDE | **Most complex**: SHA-256 checksum auth, Protobuf request encoding, binary EventStream → SSE response parsing |
|
||||
| `codex.ts` | OpenAI Codex | Injects system instructions, manages thinking levels, removes unsupported parameters |
|
||||
| `gemini-cli.ts` | Google Gemini CLI | Custom URL building (`streamGenerateContent`), Google OAuth token refresh |
|
||||
| `github.ts` | GitHub Copilot | Dual token system (GitHub OAuth + Copilot token), VSCode header mimicking |
|
||||
| `kiro.ts` | AWS CodeWhisperer | AWS EventStream binary parsing, AMZN event frames, token estimation |
|
||||
| `index.ts` | — | Factory: maps provider name → executor class, with default fallback |
|
||||
|
||||
---
|
||||
|
||||
@@ -212,12 +212,12 @@ The **orchestration layer** — coordinates translation, execution, streaming, a
|
||||
|
||||
| File | Purpose |
|
||||
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `chatCore.js` | **Central orchestrator** (~600 lines). Handles the complete request lifecycle: format detection → translation → executor dispatch → streaming/non-streaming response → token refresh → error handling → usage logging. |
|
||||
| `responsesHandler.js` | Adapter for OpenAI's Responses API: converts Responses format → Chat Completions → sends to `chatCore` → converts SSE back to Responses format. |
|
||||
| `embeddings.js` | Embedding generation handler: resolves embedding model → provider, dispatches to provider API, returns OpenAI-compatible embedding response. Supports 6+ providers. |
|
||||
| `imageGeneration.js` | Image generation handler: resolves image model → provider, supports OpenAI-compatible, Gemini-image (Antigravity), and fallback (Nebius) modes. Returns base64 or URL images. |
|
||||
| `chatCore.ts` | **Central orchestrator** (~600 lines). Handles the complete request lifecycle: format detection → translation → executor dispatch → streaming/non-streaming response → token refresh → error handling → usage logging. |
|
||||
| `responsesHandler.ts` | Adapter for OpenAI's Responses API: converts Responses format → Chat Completions → sends to `chatCore` → converts SSE back to Responses format. |
|
||||
| `embeddings.ts` | Embedding generation handler: resolves embedding model → provider, dispatches to provider API, returns OpenAI-compatible embedding response. Supports 6+ providers. |
|
||||
| `imageGeneration.ts` | Image generation handler: resolves image model → provider, supports OpenAI-compatible, Gemini-image (Antigravity), and fallback (Nebius) modes. Returns base64 or URL images. |
|
||||
|
||||
#### Request Lifecycle (chatCore.js)
|
||||
#### Request Lifecycle (chatCore.ts)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
@@ -262,20 +262,20 @@ Business logic that supports the handlers and executors.
|
||||
|
||||
| File | Purpose |
|
||||
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `provider.js` | **Format detection** (`detectFormat`): analyzes request body structure to identify Claude/OpenAI/Gemini/Antigravity/Responses formats (includes `max_tokens` heuristic for Claude). Also: URL building, header building, thinking config normalization. Supports `openai-compatible-*` and `anthropic-compatible-*` dynamic providers. |
|
||||
| `model.js` | Model string parsing (`claude/model-name` → `{provider: "claude", model: "model-name"}`), alias resolution with collision detection, input sanitization (rejects path traversal/control chars), and model info resolution with async alias getter support. |
|
||||
| `accountFallback.js` | Rate-limit handling: exponential backoff (1s → 2s → 4s → max 2min), account cooldown management, error classification (which errors trigger fallback vs. not). |
|
||||
| `tokenRefresh.js` | OAuth token refresh for **every provider**: Google (Gemini, Antigravity), Claude, Codex, Qwen, iFlow, GitHub (OAuth + Copilot dual-token), Kiro (AWS SSO OIDC + Social Auth). Includes in-flight promise deduplication cache and retry with exponential backoff. |
|
||||
| `combo.js` | **Combo models**: chains of fallback models. If model A fails with a fallback-eligible error, try model B, then C, etc. Returns actual upstream status codes. |
|
||||
| `usage.js` | Fetches quota/usage data from provider APIs (GitHub Copilot quotas, Antigravity model quotas, Codex rate limits, Kiro usage breakdowns, Claude settings). |
|
||||
| `accountSelector.js` | Smart account selection with scoring algorithm: considers priority, health status, round-robin position, and cooldown state to pick the optimal account for each request. |
|
||||
| `contextManager.js` | Request context lifecycle management: creates and tracks per-request context objects with metadata (request ID, timestamps, provider info) for debugging and logging. |
|
||||
| `ipFilter.js` | IP-based access control: supports allowlist and blocklist modes. Validates client IP against configured rules before processing API requests. |
|
||||
| `sessionManager.js` | Session tracking with client fingerprinting: tracks active sessions using hashed client identifiers, monitors request counts, and provides session metrics. |
|
||||
| `signatureCache.js` | Request signature-based deduplication cache: prevents duplicate requests by caching recent request signatures and returning cached responses for identical requests within a time window. |
|
||||
| `systemPrompt.js` | Global system prompt injection: prepends or appends a configurable system prompt to all requests, with per-provider compatibility handling. |
|
||||
| `thinkingBudget.js` | Reasoning token budget management: supports passthrough, auto (strip thinking config), custom (fixed budget), and adaptive (complexity-scaled) modes for controlling thinking/reasoning tokens. |
|
||||
| `wildcardRouter.js` | Wildcard model pattern routing: resolves wildcard patterns (e.g., `*/claude-*`) to concrete provider/model pairs based on availability and priority. |
|
||||
| `provider.ts` | **Format detection** (`detectFormat`): analyzes request body structure to identify Claude/OpenAI/Gemini/Antigravity/Responses formats (includes `max_tokens` heuristic for Claude). Also: URL building, header building, thinking config normalization. Supports `openai-compatible-*` and `anthropic-compatible-*` dynamic providers. |
|
||||
| `model.ts` | Model string parsing (`claude/model-name` → `{provider: "claude", model: "model-name"}`), alias resolution with collision detection, input sanitization (rejects path traversal/control chars), and model info resolution with async alias getter support. |
|
||||
| `accountFallback.ts` | Rate-limit handling: exponential backoff (1s → 2s → 4s → max 2min), account cooldown management, error classification (which errors trigger fallback vs. not). |
|
||||
| `tokenRefresh.ts` | OAuth token refresh for **every provider**: Google (Gemini, Antigravity), Claude, Codex, Qwen, iFlow, GitHub (OAuth + Copilot dual-token), Kiro (AWS SSO OIDC + Social Auth). Includes in-flight promise deduplication cache and retry with exponential backoff. |
|
||||
| `combo.ts` | **Combo models**: chains of fallback models. If model A fails with a fallback-eligible error, try model B, then C, etc. Returns actual upstream status codes. |
|
||||
| `usage.ts` | Fetches quota/usage data from provider APIs (GitHub Copilot quotas, Antigravity model quotas, Codex rate limits, Kiro usage breakdowns, Claude settings). |
|
||||
| `accountSelector.ts` | Smart account selection with scoring algorithm: considers priority, health status, round-robin position, and cooldown state to pick the optimal account for each request. |
|
||||
| `contextManager.ts` | Request context lifecycle management: creates and tracks per-request context objects with metadata (request ID, timestamps, provider info) for debugging and logging. |
|
||||
| `ipFilter.ts` | IP-based access control: supports allowlist and blocklist modes. Validates client IP against configured rules before processing API requests. |
|
||||
| `sessionManager.ts` | Session tracking with client fingerprinting: tracks active sessions using hashed client identifiers, monitors request counts, and provides session metrics. |
|
||||
| `signatureCache.ts` | Request signature-based deduplication cache: prevents duplicate requests by caching recent request signatures and returning cached responses for identical requests within a time window. |
|
||||
| `systemPrompt.ts` | Global system prompt injection: prepends or appends a configurable system prompt to all requests, with per-provider compatibility handling. |
|
||||
| `thinkingBudget.ts` | Reasoning token budget management: supports passthrough, auto (strip thinking config), custom (fixed budget), and adaptive (complexity-scaled) modes for controlling thinking/reasoning tokens. |
|
||||
| `wildcardRouter.ts` | Wildcard model pattern routing: resolves wildcard patterns (e.g., `*/claude-*`) to concrete provider/model pairs based on availability and priority. |
|
||||
|
||||
#### Token Refresh Deduplication
|
||||
|
||||
@@ -377,8 +377,8 @@ graph TD
|
||||
| `request/` | 8 translators | Convert request bodies between formats. Each file self-registers via `register(from, to, fn)` on import. |
|
||||
| `response/` | 7 translators | Convert streaming response chunks between formats. Handles SSE event types, thinking blocks, tool calls. |
|
||||
| `helpers/` | 6 helpers | Shared utilities: `claudeHelper` (system prompt extraction, thinking config), `geminiHelper` (parts/contents mapping), `openaiHelper` (format filtering), `toolCallHelper` (ID generation, missing response injection), `maxTokensHelper`, `responsesApiHelper`. |
|
||||
| `index.js` | — | Translation engine: `translateRequest()`, `translateResponse()`, state management, registry. |
|
||||
| `formats.js` | — | Format constants: `OPENAI`, `CLAUDE`, `GEMINI`, `ANTIGRAVITY`, `KIRO`, `CURSOR`, `OPENAI_RESPONSES`. |
|
||||
| `index.ts` | — | Translation engine: `translateRequest()`, `translateResponse()`, state management, registry. |
|
||||
| `formats.ts` | — | Format constants: `OPENAI`, `CLAUDE`, `GEMINI`, `ANTIGRAVITY`, `KIRO`, `CURSOR`, `OPENAI_RESPONSES`. |
|
||||
|
||||
#### Key Design: Self-Registering Plugins
|
||||
|
||||
@@ -397,13 +397,13 @@ import "./request/claude-to-openai.js"; // ← self-registers
|
||||
|
||||
| File | Purpose |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `error.js` | Error response building (OpenAI-compatible format), upstream error parsing, Antigravity retry-time extraction from error messages, SSE error streaming. |
|
||||
| `stream.js` | **SSE Transform Stream** — the core streaming pipeline. Two modes: `TRANSLATE` (full format translation) and `PASSTHROUGH` (normalize + extract usage). Handles chunk buffering, usage estimation, content length tracking. Per-stream encoder/decoder instances avoid shared state. |
|
||||
| `streamHelpers.js` | Low-level SSE utilities: `parseSSELine` (whitespace-tolerant), `hasValuableContent` (filters empty chunks for OpenAI/Claude/Gemini), `fixInvalidId`, `formatSSE` (format-aware SSE serialization with `perf_metrics` cleanup). |
|
||||
| `usageTracking.js` | Token usage extraction from any format (Claude/OpenAI/Gemini/Responses), estimation with separate tool/message char-per-token ratios, buffer addition (2000 tokens safety margin), format-specific field filtering, console logging with ANSI colors. |
|
||||
| `requestLogger.js` | File-based request logging (opt-in via `ENABLE_REQUEST_LOGS=true`). Creates session folders with numbered files: `1_req_client.json` → `7_res_client.txt`. All I/O is async (fire-and-forget). Masks sensitive headers. |
|
||||
| `bypassHandler.js` | Intercepts specific patterns from Claude CLI (title extraction, warmup, count) and returns fake responses without calling any provider. Supports both streaming and non-streaming. Intentionally limited to Claude CLI scope. |
|
||||
| `networkProxy.js` | Resolves outbound proxy URL for a given provider with precedence: provider-specific config → global config → environment variables (`HTTPS_PROXY`/`HTTP_PROXY`/`ALL_PROXY`). Supports `NO_PROXY` exclusions. Caches config for 30s. |
|
||||
| `error.ts` | Error response building (OpenAI-compatible format), upstream error parsing, Antigravity retry-time extraction from error messages, SSE error streaming. |
|
||||
| `stream.ts` | **SSE Transform Stream** — the core streaming pipeline. Two modes: `TRANSLATE` (full format translation) and `PASSTHROUGH` (normalize + extract usage). Handles chunk buffering, usage estimation, content length tracking. Per-stream encoder/decoder instances avoid shared state. |
|
||||
| `streamHelpers.ts` | Low-level SSE utilities: `parseSSELine` (whitespace-tolerant), `hasValuableContent` (filters empty chunks for OpenAI/Claude/Gemini), `fixInvalidId`, `formatSSE` (format-aware SSE serialization with `perf_metrics` cleanup). |
|
||||
| `usageTracking.ts` | Token usage extraction from any format (Claude/OpenAI/Gemini/Responses), estimation with separate tool/message char-per-token ratios, buffer addition (2000 tokens safety margin), format-specific field filtering, console logging with ANSI colors. |
|
||||
| `requestLogger.ts` | File-based request logging (opt-in via `ENABLE_REQUEST_LOGS=true`). Creates session folders with numbered files: `1_req_client.json` → `7_res_client.txt`. All I/O is async (fire-and-forget). Masks sensitive headers. |
|
||||
| `bypassHandler.ts` | Intercepts specific patterns from Claude CLI (title extraction, warmup, count) and returns fake responses without calling any provider. Supports both streaming and non-streaming. Intentionally limited to Claude CLI scope. |
|
||||
| `networkProxy.ts` | Resolves outbound proxy URL for a given provider with precedence: provider-specific config → global config → environment variables (`HTTPS_PROXY`/`HTTP_PROXY`/`ALL_PROXY`). Supports `NO_PROXY` exclusions. Caches config for 30s. |
|
||||
|
||||
#### SSE Streaming Pipeline
|
||||
|
||||
@@ -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.) |
|
||||
@@ -484,7 +484,7 @@ All formats translate through **OpenAI format as the hub**. Adding a new provide
|
||||
|
||||
### 5.2 Executor Strategy Pattern
|
||||
|
||||
Each provider has a dedicated executor class inheriting from `BaseExecutor`. The factory in `executors/index.js` selects the right one at runtime.
|
||||
Each provider has a dedicated executor class inheriting from `BaseExecutor`. The factory in `executors/index.ts` selects the right one at runtime.
|
||||
|
||||
### 5.3 Self-Registering Plugin System
|
||||
|
||||
|
||||
+8
-8
@@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ const eslintConfig = [
|
||||
"scripts/**",
|
||||
"bin/**",
|
||||
"node_modules/**",
|
||||
"open-sse/**",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -4,9 +4,15 @@ const nextConfig = {
|
||||
output: "standalone",
|
||||
transpilePackages: ["@omniroute/open-sse"],
|
||||
allowedDevOrigins: ["192.168.*"],
|
||||
typescript: {
|
||||
// Migration Phase: ignore TS errors during build.
|
||||
// Remove after all 984 type errors are resolved.
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
|
||||
// NEXT_PUBLIC_CLOUD_URL is set in .env — do NOT hardcode here (it overrides .env)
|
||||
webpack: (config, { isServer }) => {
|
||||
// Ignore fs/path modules in browser bundle
|
||||
|
||||
@@ -6,7 +6,22 @@
|
||||
* - /v1/audio/speech (TTS API)
|
||||
*/
|
||||
|
||||
export const AUDIO_TRANSCRIPTION_PROVIDERS = {
|
||||
interface AudioModel {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface AudioProvider {
|
||||
id: string;
|
||||
baseUrl: string;
|
||||
authType: string;
|
||||
authHeader: string;
|
||||
format?: string;
|
||||
async?: boolean;
|
||||
models: AudioModel[];
|
||||
}
|
||||
|
||||
export const AUDIO_TRANSCRIPTION_PROVIDERS: Record<string, AudioProvider> = {
|
||||
openai: {
|
||||
id: "openai",
|
||||
baseUrl: "https://api.openai.com/v1/audio/transcriptions",
|
||||
@@ -57,7 +72,7 @@ export const AUDIO_TRANSCRIPTION_PROVIDERS = {
|
||||
},
|
||||
};
|
||||
|
||||
export const AUDIO_SPEECH_PROVIDERS = {
|
||||
export const AUDIO_SPEECH_PROVIDERS: Record<string, AudioProvider> = {
|
||||
openai: {
|
||||
id: "openai",
|
||||
baseUrl: "https://api.openai.com/v1/audio/speech",
|
||||
@@ -96,21 +111,21 @@ export const AUDIO_SPEECH_PROVIDERS = {
|
||||
/**
|
||||
* Get transcription provider config by ID
|
||||
*/
|
||||
export function getTranscriptionProvider(providerId) {
|
||||
export function getTranscriptionProvider(providerId: string): AudioProvider | null {
|
||||
return AUDIO_TRANSCRIPTION_PROVIDERS[providerId] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get speech provider config by ID
|
||||
*/
|
||||
export function getSpeechProvider(providerId) {
|
||||
export function getSpeechProvider(providerId: string): AudioProvider | null {
|
||||
return AUDIO_SPEECH_PROVIDERS[providerId] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse audio model string (format: "provider/model" or just "model")
|
||||
*/
|
||||
function parseAudioModel(modelStr, registry) {
|
||||
function parseAudioModel(modelStr: string | null, registry: Record<string, AudioProvider>): { provider: string | null; model: string | null } {
|
||||
if (!modelStr) return { provider: null, model: null };
|
||||
|
||||
for (const [providerId, config] of Object.entries(registry)) {
|
||||
@@ -128,11 +143,11 @@ function parseAudioModel(modelStr, registry) {
|
||||
return { provider: null, model: modelStr };
|
||||
}
|
||||
|
||||
export function parseTranscriptionModel(modelStr) {
|
||||
export function parseTranscriptionModel(modelStr: string | null) {
|
||||
return parseAudioModel(modelStr, AUDIO_TRANSCRIPTION_PROVIDERS);
|
||||
}
|
||||
|
||||
export function parseSpeechModel(modelStr) {
|
||||
export function parseSpeechModel(modelStr: string | null) {
|
||||
return parseAudioModel(modelStr, AUDIO_SPEECH_PROVIDERS);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { loadProviderCredentials } from "./credentialLoader.js";
|
||||
import { loadProviderCredentials } from "./credentialLoader.ts";
|
||||
|
||||
// Timeout for non-streaming fetch requests (ms). Prevents stalled connections.
|
||||
export const FETCH_TIMEOUT_MS = parseInt(process.env.FETCH_TIMEOUT_MS || "120000", 10);
|
||||
@@ -9,7 +9,7 @@ export const STREAM_IDLE_TIMEOUT_MS = parseInt(process.env.STREAM_IDLE_TIMEOUT_M
|
||||
// Provider configurations
|
||||
// OAuth credentials read from env vars with hardcoded fallbacks for backward compatibility.
|
||||
// Use provider-credentials.json or env vars to override in production.
|
||||
import { generateLegacyProviders } from "./providerRegistry.js";
|
||||
import { generateLegacyProviders } from "./providerRegistry.ts";
|
||||
|
||||
export const PROVIDERS = generateLegacyProviders();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { generateModels, generateAliasMap } from "./providerRegistry.js";
|
||||
import { generateModels, generateAliasMap } from "./providerRegistry.ts";
|
||||
|
||||
// Provider models - Generated from providerRegistry.js (single source of truth)
|
||||
export const PROVIDER_MODELS = generateModels();
|
||||
@@ -6,9 +6,51 @@
|
||||
* is auto-generated from this registry.
|
||||
*/
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface RegistryModel {
|
||||
id: string;
|
||||
name: string;
|
||||
targetFormat?: string;
|
||||
}
|
||||
|
||||
export interface RegistryOAuth {
|
||||
clientIdEnv?: string;
|
||||
clientIdDefault?: string;
|
||||
clientSecretEnv?: string;
|
||||
clientSecretDefault?: string;
|
||||
tokenUrl?: string;
|
||||
refreshUrl?: string;
|
||||
authUrl?: string;
|
||||
initiateUrl?: string;
|
||||
pollUrlBase?: string;
|
||||
}
|
||||
|
||||
export interface RegistryEntry {
|
||||
id: string;
|
||||
alias: string;
|
||||
format: string;
|
||||
executor: string;
|
||||
baseUrl?: string;
|
||||
baseUrls?: string[];
|
||||
responsesBaseUrl?: string;
|
||||
urlSuffix?: string;
|
||||
urlBuilder?: (base: string, model: string, stream: boolean) => string;
|
||||
authType: string;
|
||||
authHeader: string;
|
||||
authPrefix?: string;
|
||||
headers?: Record<string, string>;
|
||||
extraHeaders?: Record<string, string>;
|
||||
oauth?: RegistryOAuth;
|
||||
models: RegistryModel[];
|
||||
chatPath?: string;
|
||||
clientVersion?: string;
|
||||
passthroughModels?: boolean;
|
||||
}
|
||||
|
||||
// ── Registry ──────────────────────────────────────────────────────────────
|
||||
|
||||
export const REGISTRY = {
|
||||
export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
// ─── OAuth Providers ───────────────────────────────────────────────────
|
||||
claude: {
|
||||
id: "claude",
|
||||
@@ -760,10 +802,10 @@ export const REGISTRY = {
|
||||
// ── Generator Functions ───────────────────────────────────────────────────
|
||||
|
||||
/** Generate legacy PROVIDERS object shape for constants.js backward compatibility */
|
||||
export function generateLegacyProviders() {
|
||||
const providers = {};
|
||||
export function generateLegacyProviders(): Record<string, any> {
|
||||
const providers: Record<string, any> = {};
|
||||
for (const [id, entry] of Object.entries(REGISTRY)) {
|
||||
const p = { format: entry.format };
|
||||
const p: Record<string, any> = { format: entry.format };
|
||||
|
||||
// URL(s)
|
||||
if (entry.baseUrls) {
|
||||
@@ -808,8 +850,8 @@ export function generateLegacyProviders() {
|
||||
}
|
||||
|
||||
/** Generate PROVIDER_MODELS map (alias → model list) */
|
||||
export function generateModels() {
|
||||
const models = {};
|
||||
export function generateModels(): Record<string, RegistryModel[]> {
|
||||
const models: Record<string, RegistryModel[]> = {};
|
||||
for (const entry of Object.values(REGISTRY)) {
|
||||
if (entry.models && entry.models.length > 0) {
|
||||
const key = entry.alias || entry.id;
|
||||
@@ -823,8 +865,8 @@ export function generateModels() {
|
||||
}
|
||||
|
||||
/** Generate PROVIDER_ID_TO_ALIAS map */
|
||||
export function generateAliasMap() {
|
||||
const map = {};
|
||||
export function generateAliasMap(): Record<string, string> {
|
||||
const map: Record<string, string> = {};
|
||||
for (const entry of Object.values(REGISTRY)) {
|
||||
map[entry.id] = entry.alias || entry.id;
|
||||
}
|
||||
@@ -841,12 +883,12 @@ for (const entry of Object.values(REGISTRY)) {
|
||||
}
|
||||
|
||||
/** Get registry entry by provider ID or alias */
|
||||
export function getRegistryEntry(provider) {
|
||||
export function getRegistryEntry(provider: string): RegistryEntry | null {
|
||||
return REGISTRY[provider] || _byAlias.get(provider) || null;
|
||||
}
|
||||
|
||||
/** Get all registered provider IDs */
|
||||
export function getRegisteredProviders() {
|
||||
export function getRegisteredProviders(): string[] {
|
||||
return Object.keys(REGISTRY);
|
||||
}
|
||||
|
||||
@@ -856,7 +898,7 @@ export function getRegisteredProviders() {
|
||||
* @param {string} provider - Provider ID or alias
|
||||
* @returns {"oauth"|"apikey"}
|
||||
*/
|
||||
export function getProviderCategory(provider) {
|
||||
export function getProviderCategory(provider: string): "oauth" | "apikey" {
|
||||
const entry = getRegistryEntry(provider);
|
||||
if (!entry) return "apikey"; // Safe default for unknown providers
|
||||
return entry.authType === "apikey" ? "apikey" : "oauth";
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from "crypto";
|
||||
import { BaseExecutor } from "./base.js";
|
||||
import { PROVIDERS, OAUTH_ENDPOINTS, HTTP_STATUS } from "../config/constants.js";
|
||||
import { BaseExecutor } from "./base.ts";
|
||||
import { PROVIDERS, OAUTH_ENDPOINTS, HTTP_STATUS } from "../config/constants.ts";
|
||||
|
||||
const MAX_RETRY_AFTER_MS = 10000;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HTTP_STATUS, FETCH_TIMEOUT_MS } from "../config/constants.js";
|
||||
import { HTTP_STATUS, FETCH_TIMEOUT_MS } from "../config/constants.ts";
|
||||
|
||||
/**
|
||||
* BaseExecutor - Base class for provider executors.
|
||||
@@ -6,7 +6,10 @@ import { HTTP_STATUS, FETCH_TIMEOUT_MS } from "../config/constants.js";
|
||||
* (buildUrl, buildHeaders, transformRequest, etc.) for each provider.
|
||||
*/
|
||||
export class BaseExecutor {
|
||||
constructor(provider, config) {
|
||||
provider: any;
|
||||
config: any;
|
||||
|
||||
constructor(provider: any, config: any) {
|
||||
this.provider = provider;
|
||||
this.config = config;
|
||||
}
|
||||
@@ -96,7 +99,7 @@ export class BaseExecutor {
|
||||
? AbortSignal.any([signal, timeoutSignal])
|
||||
: signal || timeoutSignal;
|
||||
|
||||
const fetchOptions = {
|
||||
const fetchOptions: Record<string, any> = {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(transformedBody),
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BaseExecutor } from "./base.js";
|
||||
import { CODEX_DEFAULT_INSTRUCTIONS } from "../config/codexInstructions.js";
|
||||
import { PROVIDERS } from "../config/constants.js";
|
||||
import { BaseExecutor } from "./base.ts";
|
||||
import { CODEX_DEFAULT_INSTRUCTIONS } from "../config/codexInstructions.ts";
|
||||
import { PROVIDERS } from "../config/constants.ts";
|
||||
|
||||
/**
|
||||
* Codex Executor - handles OpenAI Codex API (Responses API format)
|
||||
@@ -1,3 +1,4 @@
|
||||
declare var EdgeRuntime: any;
|
||||
/**
|
||||
* CursorExecutor — Handles communication with the Cursor IDE API.
|
||||
*
|
||||
@@ -19,16 +20,16 @@
|
||||
* @see cursorProtobuf.js for Protobuf encoding/decoding utilities
|
||||
*/
|
||||
|
||||
import { BaseExecutor } from "./base.js";
|
||||
import { PROVIDERS, HTTP_STATUS } from "../config/constants.js";
|
||||
import { BaseExecutor } from "./base.ts";
|
||||
import { PROVIDERS, HTTP_STATUS } from "../config/constants.ts";
|
||||
import {
|
||||
generateCursorBody,
|
||||
parseConnectRPCFrame,
|
||||
extractTextFromResponse,
|
||||
} from "../utils/cursorProtobuf.js";
|
||||
import { estimateUsage } from "../utils/usageTracking.js";
|
||||
import { FORMATS } from "../translator/formats.js";
|
||||
import { buildCursorRequest } from "../translator/request/openai-to-cursor.js";
|
||||
} from "../utils/cursorProtobuf.ts";
|
||||
import { estimateUsage } from "../utils/usageTracking.ts";
|
||||
import { FORMATS } from "../translator/formats.ts";
|
||||
import { buildCursorRequest } from "../translator/request/openai-to-cursor.ts";
|
||||
import crypto from "crypto";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
import zlib from "zlib";
|
||||
@@ -226,7 +227,7 @@ export class CursorExecutor extends BaseExecutor {
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
headers: Object.fromEntries(response.headers.entries()),
|
||||
headers: Object.fromEntries((response.headers as any).entries()),
|
||||
body: Buffer.from(await response.arrayBuffer()),
|
||||
};
|
||||
}
|
||||
@@ -290,7 +291,7 @@ export class CursorExecutor extends BaseExecutor {
|
||||
const transformedBody = this.transformRequest(model, body, stream, credentials);
|
||||
|
||||
try {
|
||||
const response = http2
|
||||
const response: any = http2
|
||||
? await this.makeHttp2Request(url, headers, transformedBody, signal)
|
||||
: await this.makeFetchRequest(url, headers, transformedBody, signal);
|
||||
|
||||
@@ -458,8 +459,7 @@ export class CursorExecutor extends BaseExecutor {
|
||||
|
||||
console.log(`[CURSOR BUFFER] Final toolCalls count: ${toolCalls.length}`);
|
||||
|
||||
const message = {
|
||||
role: "assistant",
|
||||
const message: Record<string, any> = { role: "assistant",
|
||||
content: totalContent || null,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BaseExecutor } from "./base.js";
|
||||
import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.js";
|
||||
import { getAccessToken } from "../services/tokenRefresh.js";
|
||||
import { BaseExecutor } from "./base.ts";
|
||||
import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.ts";
|
||||
import { getAccessToken } from "../services/tokenRefresh.ts";
|
||||
|
||||
export class DefaultExecutor extends BaseExecutor {
|
||||
constructor(provider) {
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BaseExecutor } from "./base.js";
|
||||
import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.js";
|
||||
import { BaseExecutor } from "./base.ts";
|
||||
import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.ts";
|
||||
|
||||
export class GeminiCLIExecutor extends BaseExecutor {
|
||||
constructor() {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BaseExecutor } from "./base.js";
|
||||
import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.js";
|
||||
import { getModelTargetFormat } from "../config/providerModels.js";
|
||||
import { BaseExecutor } from "./base.ts";
|
||||
import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.ts";
|
||||
import { getModelTargetFormat } from "../config/providerModels.ts";
|
||||
|
||||
export class GithubExecutor extends BaseExecutor {
|
||||
constructor() {
|
||||
@@ -1,38 +0,0 @@
|
||||
import { AntigravityExecutor } from "./antigravity.js";
|
||||
import { GeminiCLIExecutor } from "./gemini-cli.js";
|
||||
import { GithubExecutor } from "./github.js";
|
||||
import { KiroExecutor } from "./kiro.js";
|
||||
import { CodexExecutor } from "./codex.js";
|
||||
import { CursorExecutor } from "./cursor.js";
|
||||
import { DefaultExecutor } from "./default.js";
|
||||
|
||||
const executors = {
|
||||
antigravity: new AntigravityExecutor(),
|
||||
"gemini-cli": new GeminiCLIExecutor(),
|
||||
github: new GithubExecutor(),
|
||||
kiro: new KiroExecutor(),
|
||||
codex: new CodexExecutor(),
|
||||
cursor: new CursorExecutor(),
|
||||
cu: new CursorExecutor(), // Alias for cursor
|
||||
};
|
||||
|
||||
const defaultCache = new Map();
|
||||
|
||||
export function getExecutor(provider) {
|
||||
if (executors[provider]) return executors[provider];
|
||||
if (!defaultCache.has(provider)) defaultCache.set(provider, new DefaultExecutor(provider));
|
||||
return defaultCache.get(provider);
|
||||
}
|
||||
|
||||
export function hasSpecializedExecutor(provider) {
|
||||
return !!executors[provider];
|
||||
}
|
||||
|
||||
export { BaseExecutor } from "./base.js";
|
||||
export { AntigravityExecutor } from "./antigravity.js";
|
||||
export { GeminiCLIExecutor } from "./gemini-cli.js";
|
||||
export { GithubExecutor } from "./github.js";
|
||||
export { KiroExecutor } from "./kiro.js";
|
||||
export { CodexExecutor } from "./codex.js";
|
||||
export { CursorExecutor } from "./cursor.js";
|
||||
export { DefaultExecutor } from "./default.js";
|
||||
@@ -0,0 +1,38 @@
|
||||
import { AntigravityExecutor } from "./antigravity.ts";
|
||||
import { GeminiCLIExecutor } from "./gemini-cli.ts";
|
||||
import { GithubExecutor } from "./github.ts";
|
||||
import { KiroExecutor } from "./kiro.ts";
|
||||
import { CodexExecutor } from "./codex.ts";
|
||||
import { CursorExecutor } from "./cursor.ts";
|
||||
import { DefaultExecutor } from "./default.ts";
|
||||
|
||||
const executors = {
|
||||
antigravity: new AntigravityExecutor(),
|
||||
"gemini-cli": new GeminiCLIExecutor(),
|
||||
github: new GithubExecutor(),
|
||||
kiro: new KiroExecutor(),
|
||||
codex: new CodexExecutor(),
|
||||
cursor: new CursorExecutor(),
|
||||
cu: new CursorExecutor(), // Alias for cursor
|
||||
};
|
||||
|
||||
const defaultCache = new Map();
|
||||
|
||||
export function getExecutor(provider) {
|
||||
if (executors[provider]) return executors[provider];
|
||||
if (!defaultCache.has(provider)) defaultCache.set(provider, new DefaultExecutor(provider));
|
||||
return defaultCache.get(provider);
|
||||
}
|
||||
|
||||
export function hasSpecializedExecutor(provider) {
|
||||
return !!executors[provider];
|
||||
}
|
||||
|
||||
export { BaseExecutor } from "./base.ts";
|
||||
export { AntigravityExecutor } from "./antigravity.ts";
|
||||
export { GeminiCLIExecutor } from "./gemini-cli.ts";
|
||||
export { GithubExecutor } from "./github.ts";
|
||||
export { KiroExecutor } from "./kiro.ts";
|
||||
export { CodexExecutor } from "./codex.ts";
|
||||
export { CursorExecutor } from "./cursor.ts";
|
||||
export { DefaultExecutor } from "./default.ts";
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BaseExecutor } from "./base.js";
|
||||
import { PROVIDERS } from "../config/constants.js";
|
||||
import { BaseExecutor } from "./base.ts";
|
||||
import { PROVIDERS } from "../config/constants.ts";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { refreshKiroToken } from "../services/tokenRefresh.js";
|
||||
import { refreshKiroToken } from "../services/tokenRefresh.ts";
|
||||
|
||||
// ── CRC32 lookup table (IEEE polynomial, no dependency) ──
|
||||
const CRC32_TABLE = new Uint32Array(256);
|
||||
@@ -83,8 +83,7 @@ export class KiroExecutor extends BaseExecutor {
|
||||
let chunkIndex = 0;
|
||||
const responseId = `chatcmpl-${Date.now()}`;
|
||||
const created = Math.floor(Date.now() / 1000);
|
||||
const state = {
|
||||
endDetected: false,
|
||||
const state: Record<string, any> = { endDetected: false,
|
||||
finishEmitted: false,
|
||||
hasToolCalls: false,
|
||||
toolCallIndex: 0,
|
||||
@@ -126,7 +125,7 @@ export class KiroExecutor extends BaseExecutor {
|
||||
const content = event.payload.content;
|
||||
state.totalContentLength += content.length;
|
||||
|
||||
const chunk = {
|
||||
const chunk: Record<string, any> = {
|
||||
id: responseId,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
@@ -145,7 +144,7 @@ export class KiroExecutor extends BaseExecutor {
|
||||
|
||||
// Handle codeEvent
|
||||
if (eventType === "codeEvent" && event.payload?.content) {
|
||||
const chunk = {
|
||||
const chunk: Record<string, any> = {
|
||||
id: responseId,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
@@ -257,7 +256,7 @@ export class KiroExecutor extends BaseExecutor {
|
||||
|
||||
// Handle messageStopEvent
|
||||
if (eventType === "messageStopEvent") {
|
||||
const chunk = {
|
||||
const chunk: Record<string, any> = {
|
||||
id: responseId,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
@@ -330,7 +329,7 @@ export class KiroExecutor extends BaseExecutor {
|
||||
};
|
||||
}
|
||||
|
||||
const finishChunk = {
|
||||
const finishChunk: Record<string, any> = {
|
||||
id: responseId,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
@@ -10,8 +10,8 @@
|
||||
* - Deepgram: POST { text } with model via query param, Token auth
|
||||
*/
|
||||
|
||||
import { getSpeechProvider, parseSpeechModel } from "../config/audioRegistry.js";
|
||||
import { errorResponse } from "../utils/error.js";
|
||||
import { getSpeechProvider, parseSpeechModel } from "../config/audioRegistry.ts";
|
||||
import { errorResponse } from "../utils/error.ts";
|
||||
|
||||
/**
|
||||
* Build auth header for a speech provider
|
||||
@@ -100,12 +100,13 @@ async function handleDeepgramSpeech(providerConfig, body, modelId, token) {
|
||||
* @param {Object} options.credentials - Provider credentials { apiKey }
|
||||
* @returns {Response}
|
||||
*/
|
||||
/** @returns {Promise<any>} */
|
||||
export async function handleAudioSpeech({ body, credentials }) {
|
||||
if (!body.model) {
|
||||
return errorResponse("model is required", 400);
|
||||
return errorResponse(400, "model is required");
|
||||
}
|
||||
if (!body.input) {
|
||||
return errorResponse("input is required", 400);
|
||||
return errorResponse(400, "input is required");
|
||||
}
|
||||
|
||||
const { provider: providerId, model: modelId } = parseSpeechModel(body.model);
|
||||
@@ -113,14 +114,14 @@ export async function handleAudioSpeech({ body, credentials }) {
|
||||
|
||||
if (!providerConfig) {
|
||||
return errorResponse(
|
||||
`No speech provider found for model "${body.model}". Available: openai, hyperbolic, deepgram`,
|
||||
400
|
||||
400,
|
||||
`No speech provider found for model "${body.model}". Available: openai, hyperbolic, deepgram`
|
||||
);
|
||||
}
|
||||
|
||||
const token = credentials?.apiKey || credentials?.accessToken;
|
||||
if (!token) {
|
||||
return errorResponse(`No credentials for speech provider: ${providerId}`, 401);
|
||||
return errorResponse(401, `No credentials for speech provider: ${providerId}`);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -168,6 +169,6 @@ export async function handleAudioSpeech({ body, credentials }) {
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
return errorResponse(`Speech request failed: ${err.message}`, 500);
|
||||
return errorResponse(500, `Speech request failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@
|
||||
* - AssemblyAI: async workflow (upload → submit → poll)
|
||||
*/
|
||||
|
||||
import { getTranscriptionProvider, parseTranscriptionModel } from "../config/audioRegistry.js";
|
||||
import { errorResponse } from "../utils/error.js";
|
||||
import { getTranscriptionProvider, parseTranscriptionModel } from "../config/audioRegistry.ts";
|
||||
import { errorResponse } from "../utils/error.ts";
|
||||
|
||||
/**
|
||||
* Build auth header for a transcription provider
|
||||
@@ -129,11 +129,11 @@ async function handleAssemblyAITranscription(providerConfig, file, modelId, toke
|
||||
}
|
||||
|
||||
if (result.status === "error") {
|
||||
return errorResponse(result.error || "AssemblyAI transcription failed", 500);
|
||||
return errorResponse(500, result.error || "AssemblyAI transcription failed");
|
||||
}
|
||||
}
|
||||
|
||||
return errorResponse("AssemblyAI transcription timed out after 120s", 504);
|
||||
return errorResponse(504, "AssemblyAI transcription timed out after 120s");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,15 +144,16 @@ async function handleAssemblyAITranscription(providerConfig, file, modelId, toke
|
||||
* @param {Object} options.credentials - Provider credentials { apiKey }
|
||||
* @returns {Response}
|
||||
*/
|
||||
/** @returns {Promise<any>} */
|
||||
export async function handleAudioTranscription({ formData, credentials }) {
|
||||
const model = formData.get("model");
|
||||
if (!model) {
|
||||
return errorResponse("model is required", 400);
|
||||
return errorResponse(400, "model is required");
|
||||
}
|
||||
|
||||
const file = formData.get("file");
|
||||
if (!file) {
|
||||
return errorResponse("file is required", 400);
|
||||
return errorResponse(400, "file is required");
|
||||
}
|
||||
|
||||
const { provider: providerId, model: modelId } = parseTranscriptionModel(model);
|
||||
@@ -160,14 +161,14 @@ export async function handleAudioTranscription({ formData, credentials }) {
|
||||
|
||||
if (!providerConfig) {
|
||||
return errorResponse(
|
||||
`No transcription provider found for model "${model}". Available: openai, groq, deepgram, assemblyai`,
|
||||
400
|
||||
400,
|
||||
`No transcription provider found for model "${model}". Available: openai, groq, deepgram, assemblyai`
|
||||
);
|
||||
}
|
||||
|
||||
const token = credentials?.apiKey || credentials?.accessToken;
|
||||
if (!token) {
|
||||
return errorResponse(`No credentials for transcription provider: ${providerId}`, 401);
|
||||
return errorResponse(401, `No credentials for transcription provider: ${providerId}`);
|
||||
}
|
||||
|
||||
// Route to provider-specific handler
|
||||
@@ -181,7 +182,7 @@ export async function handleAudioTranscription({ formData, credentials }) {
|
||||
|
||||
// Default: OpenAI/Groq-compatible multipart proxy
|
||||
const upstreamForm = new FormData();
|
||||
upstreamForm.append("file", file, file.name || "audio.wav");
|
||||
upstreamForm.append("file", /** @type {Blob} */ (file), /** @type {any} */ (file).name || "audio.wav");
|
||||
upstreamForm.append("model", modelId);
|
||||
|
||||
// Forward optional parameters
|
||||
@@ -194,7 +195,7 @@ export async function handleAudioTranscription({ formData, credentials }) {
|
||||
]) {
|
||||
const val = formData.get(key);
|
||||
if (val !== null && val !== undefined) {
|
||||
upstreamForm.append(key, val);
|
||||
upstreamForm.append(key, /** @type {string} */ (val));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,6 +222,6 @@ export async function handleAudioTranscription({ formData, credentials }) {
|
||||
headers: { "Content-Type": contentType, "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
} catch (err) {
|
||||
return errorResponse(`Transcription request failed: ${err.message}`, 500);
|
||||
return errorResponse(500, `Transcription request failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,42 @@
|
||||
import { detectFormat, getTargetFormat } from "../services/provider.js";
|
||||
import { translateRequest, needsTranslation } from "../translator/index.js";
|
||||
import { FORMATS } from "../translator/formats.js";
|
||||
import { detectFormat, getTargetFormat } from "../services/provider.ts";
|
||||
import { translateRequest, needsTranslation } from "../translator/index.ts";
|
||||
import { FORMATS } from "../translator/formats.ts";
|
||||
import {
|
||||
createSSETransformStreamWithLogger,
|
||||
createPassthroughStreamWithLogger,
|
||||
COLORS,
|
||||
} from "../utils/stream.js";
|
||||
import { createStreamController, pipeWithDisconnect } from "../utils/streamHandler.js";
|
||||
import { addBufferToUsage, filterUsageForFormat, estimateUsage } from "../utils/usageTracking.js";
|
||||
import { refreshWithRetry } from "../services/tokenRefresh.js";
|
||||
import { createRequestLogger } from "../utils/requestLogger.js";
|
||||
import { getModelTargetFormat, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.js";
|
||||
import { createErrorResult, parseUpstreamError, formatProviderError } from "../utils/error.js";
|
||||
import { HTTP_STATUS } from "../config/constants.js";
|
||||
import { handleBypassRequest } from "../utils/bypassHandler.js";
|
||||
} from "../utils/stream.ts";
|
||||
import { createStreamController, pipeWithDisconnect } from "../utils/streamHandler.ts";
|
||||
import { addBufferToUsage, filterUsageForFormat, estimateUsage } from "../utils/usageTracking.ts";
|
||||
import { refreshWithRetry } from "../services/tokenRefresh.ts";
|
||||
import { createRequestLogger } from "../utils/requestLogger.ts";
|
||||
import { getModelTargetFormat, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.ts";
|
||||
import { createErrorResult, parseUpstreamError, formatProviderError } from "../utils/error.ts";
|
||||
import { HTTP_STATUS } from "../config/constants.ts";
|
||||
import { handleBypassRequest } from "../utils/bypassHandler.ts";
|
||||
import {
|
||||
saveRequestUsage,
|
||||
trackPendingRequest,
|
||||
appendRequestLog,
|
||||
saveCallLog,
|
||||
} from "@/lib/usageDb.js";
|
||||
import { getExecutor } from "../executors/index.js";
|
||||
import { translateNonStreamingResponse } from "./responseTranslator.js";
|
||||
import { extractUsageFromResponse } from "./usageExtractor.js";
|
||||
import { parseSSEToOpenAIResponse, parseSSEToResponsesOutput } from "./sseParser.js";
|
||||
} from "@/lib/usageDb";
|
||||
import { getExecutor } from "../executors/index.ts";
|
||||
import { translateNonStreamingResponse } from "./responseTranslator.ts";
|
||||
import { extractUsageFromResponse } from "./usageExtractor.ts";
|
||||
import { parseSSEToOpenAIResponse, parseSSEToResponsesOutput } from "./sseParser.ts";
|
||||
import {
|
||||
withRateLimit,
|
||||
updateFromHeaders,
|
||||
initializeRateLimits,
|
||||
} from "../services/rateLimitManager.js";
|
||||
} from "../services/rateLimitManager.ts";
|
||||
import {
|
||||
generateSignature,
|
||||
getCachedResponse,
|
||||
setCachedResponse,
|
||||
isCacheable,
|
||||
} from "@/lib/semanticCache.js";
|
||||
import { getIdempotencyKey, checkIdempotency, saveIdempotency } from "@/lib/idempotencyLayer.js";
|
||||
import { createProgressTransform, wantsProgress } from "../utils/progressTracker.js";
|
||||
} from "@/lib/semanticCache";
|
||||
import { getIdempotencyKey, checkIdempotency, saveIdempotency } from "@/lib/idempotencyLayer";
|
||||
import { createProgressTransform, wantsProgress } from "../utils/progressTracker.ts";
|
||||
|
||||
/**
|
||||
* Core chat handler - shared between SSE and Worker
|
||||
@@ -52,6 +52,7 @@ import { createProgressTransform, wantsProgress } from "../utils/progressTracker
|
||||
* @param {string} options.connectionId - Connection ID for usage tracking
|
||||
* @param {object} options.apiKeyInfo - API key metadata for usage attribution
|
||||
*/
|
||||
/** @param {any} options */
|
||||
export async function handleChatCore({
|
||||
body,
|
||||
modelInfo,
|
||||
@@ -13,8 +13,8 @@
|
||||
* }
|
||||
*/
|
||||
|
||||
import { getEmbeddingProvider, parseEmbeddingModel } from "../config/embeddingRegistry.js";
|
||||
import { saveCallLog } from "@/lib/usageDb.js";
|
||||
import { getEmbeddingProvider, parseEmbeddingModel } from "../config/embeddingRegistry.ts";
|
||||
import { saveCallLog } from "@/lib/usageDb";
|
||||
|
||||
/**
|
||||
* Handle embedding request
|
||||
@@ -52,7 +52,7 @@ export async function handleEmbedding({ body, credentials, log }) {
|
||||
}
|
||||
|
||||
// Build upstream request
|
||||
const upstreamBody = {
|
||||
const upstreamBody: Record<string, any> = {
|
||||
model: model,
|
||||
input: body.input,
|
||||
};
|
||||
@@ -15,8 +15,8 @@
|
||||
* }
|
||||
*/
|
||||
|
||||
import { getImageProvider, parseImageModel } from "../config/imageRegistry.js";
|
||||
import { saveCallLog } from "@/lib/usageDb.js";
|
||||
import { getImageProvider, parseImageModel } from "../config/imageRegistry.ts";
|
||||
import { saveCallLog } from "@/lib/usageDb";
|
||||
|
||||
/**
|
||||
* Handle image generation request
|
||||
@@ -232,7 +232,7 @@ async function handleOpenAIImageGeneration({
|
||||
};
|
||||
|
||||
// Build upstream request (OpenAI-compatible format)
|
||||
const upstreamBody = {
|
||||
const upstreamBody: Record<string, any> = {
|
||||
model: model,
|
||||
prompt: body.prompt,
|
||||
};
|
||||
@@ -4,8 +4,8 @@
|
||||
* Handles POST /v1/moderations (OpenAI Moderations API format).
|
||||
*/
|
||||
|
||||
import { getModerationProvider, parseModerationModel } from "../config/moderationRegistry.js";
|
||||
import { errorResponse } from "../utils/error.js";
|
||||
import { getModerationProvider, parseModerationModel } from "../config/moderationRegistry.ts";
|
||||
import { errorResponse } from "../utils/error.ts";
|
||||
|
||||
/**
|
||||
* Handle moderation request
|
||||
@@ -15,9 +15,10 @@ import { errorResponse } from "../utils/error.js";
|
||||
* @param {Object} options.credentials - Provider credentials { apiKey }
|
||||
* @returns {Response}
|
||||
*/
|
||||
/** @returns {Promise<any>} */
|
||||
export async function handleModeration({ body, credentials }) {
|
||||
if (!body.input) {
|
||||
return errorResponse("input is required", 400);
|
||||
return errorResponse(400, "input is required");
|
||||
}
|
||||
|
||||
// Default to latest moderation model
|
||||
@@ -27,14 +28,14 @@ export async function handleModeration({ body, credentials }) {
|
||||
|
||||
if (!providerConfig) {
|
||||
return errorResponse(
|
||||
`No moderation provider found for model "${model}". Available: openai`,
|
||||
400
|
||||
400,
|
||||
`No moderation provider found for model "${model}". Available: openai`
|
||||
);
|
||||
}
|
||||
|
||||
const token = credentials?.apiKey || credentials?.accessToken;
|
||||
if (!token) {
|
||||
return errorResponse(`No credentials for moderation provider: ${providerId}`, 401);
|
||||
return errorResponse(401, `No credentials for moderation provider: ${providerId}`);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -63,6 +64,6 @@ export async function handleModeration({ body, credentials }) {
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
} catch (err) {
|
||||
return errorResponse(`Moderation request failed: ${err.message}`, 500);
|
||||
return errorResponse(500, `Moderation request failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@
|
||||
* Routes to the appropriate provider based on the model prefix or lookup.
|
||||
*/
|
||||
|
||||
import { getRerankProvider, parseRerankModel } from "../config/rerankRegistry.js";
|
||||
import { errorResponse } from "../utils/error.js";
|
||||
import { getRerankProvider, parseRerankModel } from "../config/rerankRegistry.ts";
|
||||
import { errorResponse } from "../utils/error.ts";
|
||||
|
||||
/**
|
||||
* Build authorization header for a rerank provider
|
||||
@@ -69,6 +69,7 @@ function transformResponseFromProvider(providerConfig, data) {
|
||||
* @param {Object} options.credentials - Provider credentials { apiKey, accessToken }
|
||||
* @returns {Response}
|
||||
*/
|
||||
/** @returns {Promise<any>} */
|
||||
export async function handleRerank({
|
||||
model,
|
||||
query,
|
||||
@@ -77,10 +78,10 @@ export async function handleRerank({
|
||||
return_documents,
|
||||
credentials,
|
||||
}) {
|
||||
if (!model) return errorResponse("model is required", 400);
|
||||
if (!query) return errorResponse("query is required", 400);
|
||||
if (!model) return errorResponse(400, "model is required");
|
||||
if (!query) return errorResponse(400, "query is required");
|
||||
if (!documents || !Array.isArray(documents) || documents.length === 0) {
|
||||
return errorResponse("documents must be a non-empty array", 400);
|
||||
return errorResponse(400, "documents must be a non-empty array");
|
||||
}
|
||||
|
||||
const { provider: providerId, model: modelId } = parseRerankModel(model);
|
||||
@@ -88,14 +89,14 @@ export async function handleRerank({
|
||||
|
||||
if (!providerConfig) {
|
||||
return errorResponse(
|
||||
`No rerank provider found for model "${model}". Available: cohere, together, nvidia, fireworks`,
|
||||
400
|
||||
400,
|
||||
`No rerank provider found for model "${model}". Available: cohere, together, nvidia, fireworks`
|
||||
);
|
||||
}
|
||||
|
||||
const token = credentials?.apiKey || credentials?.accessToken;
|
||||
if (!token) {
|
||||
return errorResponse(`No credentials for rerank provider: ${providerId}`, 401);
|
||||
return errorResponse(401, `No credentials for rerank provider: ${providerId}`);
|
||||
}
|
||||
|
||||
const requestBody = transformRequestForProvider(providerConfig, {
|
||||
@@ -119,8 +120,8 @@ export async function handleRerank({
|
||||
if (!res.ok) {
|
||||
const errData = await res.json().catch(() => ({}));
|
||||
return errorResponse(
|
||||
errData.message || errData.error?.message || `Provider returned HTTP ${res.status}`,
|
||||
res.status
|
||||
res.status,
|
||||
errData.message || errData.error?.message || `Provider returned HTTP ${res.status}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -131,6 +132,6 @@ export async function handleRerank({
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
} catch (err) {
|
||||
return errorResponse(`Rerank request failed: ${err.message}`, 500);
|
||||
return errorResponse(500, `Rerank request failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FORMATS } from "../translator/formats.js";
|
||||
import { FORMATS } from "../translator/formats.ts";
|
||||
|
||||
/**
|
||||
* Translate non-streaming response to OpenAI format
|
||||
@@ -56,7 +56,7 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
|
||||
}
|
||||
}
|
||||
|
||||
const message = { role: "assistant" };
|
||||
const message: Record<string, any> = { role: "assistant" };
|
||||
if (textContent) {
|
||||
message.content = textContent;
|
||||
}
|
||||
@@ -74,7 +74,7 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
|
||||
const model = response?.model || responseBody?.model || "openai-responses";
|
||||
const finishReason = toolCalls.length > 0 ? "tool_calls" : "stop";
|
||||
|
||||
const result = {
|
||||
const result: Record<string, any> = {
|
||||
id: `chatcmpl-${response?.id || Date.now()}`,
|
||||
object: "chat.completion",
|
||||
created: createdAt,
|
||||
@@ -162,7 +162,7 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
|
||||
}
|
||||
|
||||
// Build OpenAI format message
|
||||
const message = { role: "assistant" };
|
||||
const message: Record<string, any> = { role: "assistant" };
|
||||
if (textContent) {
|
||||
message.content = textContent;
|
||||
}
|
||||
@@ -183,7 +183,7 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
|
||||
finishReason = "tool_calls";
|
||||
}
|
||||
|
||||
const result = {
|
||||
const result: Record<string, any> = {
|
||||
id: `chatcmpl-${response.responseId || Date.now()}`,
|
||||
object: "chat.completion",
|
||||
created: Math.floor(new Date(response.createTime || Date.now()).getTime() / 1000),
|
||||
@@ -241,7 +241,7 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
|
||||
}
|
||||
}
|
||||
|
||||
const message = { role: "assistant" };
|
||||
const message: Record<string, any> = { role: "assistant" };
|
||||
if (textContent) {
|
||||
message.content = textContent;
|
||||
}
|
||||
@@ -259,7 +259,7 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
|
||||
if (finishReason === "end_turn") finishReason = "stop";
|
||||
if (finishReason === "tool_use") finishReason = "tool_calls";
|
||||
|
||||
const result = {
|
||||
const result: Record<string, any> = {
|
||||
id: `chatcmpl-${responseBody.id || Date.now()}`,
|
||||
object: "chat.completion",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
@@ -3,9 +3,9 @@
|
||||
* Converts Chat Completions to Codex Responses API format
|
||||
*/
|
||||
|
||||
import { handleChatCore } from "./chatCore.js";
|
||||
import { convertResponsesApiFormat } from "../translator/helpers/responsesApiHelper.js";
|
||||
import { createResponsesApiTransformStream } from "../transformer/responsesTransformer.js";
|
||||
import { handleChatCore } from "./chatCore.ts";
|
||||
import { convertResponsesApiFormat } from "../translator/helpers/responsesApiHelper.ts";
|
||||
import { createResponsesApiTransformStream } from "../transformer/responsesTransformer.ts";
|
||||
|
||||
/**
|
||||
* Handle /v1/responses request
|
||||
@@ -45,8 +45,11 @@ export async function handleResponsesCore({
|
||||
onCredentialsRefreshed,
|
||||
onRequestSuccess,
|
||||
onDisconnect,
|
||||
clientRawRequest: null,
|
||||
connectionId,
|
||||
});
|
||||
userAgent: null,
|
||||
comboName: null,
|
||||
} as any);
|
||||
|
||||
if (!result.success || !result.response) {
|
||||
return result;
|
||||
@@ -44,15 +44,14 @@ export function parseSSEToOpenAIResponse(rawSSE, fallbackModel) {
|
||||
}
|
||||
}
|
||||
|
||||
const message = {
|
||||
role: "assistant",
|
||||
const message: Record<string, any> = { role: "assistant",
|
||||
content: contentParts.join(""),
|
||||
};
|
||||
if (reasoningParts.length > 0) {
|
||||
message.reasoning_content = reasoningParts.join("");
|
||||
}
|
||||
|
||||
const result = {
|
||||
const result: Record<string, any> = {
|
||||
id: first.id || `chatcmpl-${Date.now()}`,
|
||||
object: "chat.completion",
|
||||
created: first.created || Math.floor(Date.now() / 1000),
|
||||
@@ -1,5 +1,5 @@
|
||||
// Patch global fetch with proxy support (must be first)
|
||||
import "./utils/proxyFetch.js";
|
||||
import "./utils/proxyFetch.ts";
|
||||
|
||||
// Config
|
||||
export {
|
||||
@@ -10,7 +10,7 @@ export {
|
||||
CLAUDE_SYSTEM_PROMPT,
|
||||
COOLDOWN_MS,
|
||||
BACKOFF_CONFIG,
|
||||
} from "./config/constants.js";
|
||||
} from "./config/constants.ts";
|
||||
export {
|
||||
PROVIDER_MODELS,
|
||||
getProviderModels,
|
||||
@@ -20,10 +20,10 @@ export {
|
||||
getModelTargetFormat,
|
||||
PROVIDER_ID_TO_ALIAS,
|
||||
getModelsByProviderId,
|
||||
} from "./config/providerModels.js";
|
||||
} from "./config/providerModels.ts";
|
||||
|
||||
// Translator
|
||||
export { FORMATS } from "./translator/formats.js";
|
||||
export { FORMATS } from "./translator/formats.ts";
|
||||
export {
|
||||
register,
|
||||
translateRequest,
|
||||
@@ -31,7 +31,7 @@ export {
|
||||
needsTranslation,
|
||||
initState,
|
||||
initTranslators,
|
||||
} from "./translator/index.js";
|
||||
} from "./translator/index.ts";
|
||||
|
||||
// Services
|
||||
export {
|
||||
@@ -40,16 +40,16 @@ export {
|
||||
buildProviderUrl,
|
||||
buildProviderHeaders,
|
||||
getTargetFormat,
|
||||
} from "./services/provider.js";
|
||||
} from "./services/provider.ts";
|
||||
|
||||
export { parseModel, resolveModelAliasFromMap, getModelInfoCore } from "./services/model.js";
|
||||
export { parseModel, resolveModelAliasFromMap, getModelInfoCore } from "./services/model.ts";
|
||||
|
||||
export {
|
||||
checkFallbackError,
|
||||
isAccountUnavailable,
|
||||
getUnavailableUntil,
|
||||
filterAvailableAccounts,
|
||||
} from "./services/accountFallback.js";
|
||||
} from "./services/accountFallback.ts";
|
||||
|
||||
export {
|
||||
TOKEN_EXPIRY_BUFFER_MS,
|
||||
@@ -63,43 +63,43 @@ export {
|
||||
refreshCopilotToken,
|
||||
getAccessToken,
|
||||
refreshTokenByProvider,
|
||||
} from "./services/tokenRefresh.js";
|
||||
} from "./services/tokenRefresh.ts";
|
||||
|
||||
// Handlers
|
||||
export { handleChatCore, isTokenExpiringSoon } from "./handlers/chatCore.js";
|
||||
export { handleChatCore, isTokenExpiringSoon } from "./handlers/chatCore.ts";
|
||||
export {
|
||||
createStreamController,
|
||||
pipeWithDisconnect,
|
||||
createDisconnectAwareStream,
|
||||
} from "./utils/streamHandler.js";
|
||||
} from "./utils/streamHandler.ts";
|
||||
|
||||
// Executors
|
||||
export { getExecutor, hasSpecializedExecutor } from "./executors/index.js";
|
||||
export { getExecutor, hasSpecializedExecutor } from "./executors/index.ts";
|
||||
|
||||
// Utils
|
||||
export { errorResponse, formatProviderError } from "./utils/error.js";
|
||||
export { errorResponse, formatProviderError } from "./utils/error.ts";
|
||||
export {
|
||||
createSSETransformStreamWithLogger,
|
||||
createPassthroughStreamWithLogger,
|
||||
} from "./utils/stream.js";
|
||||
} from "./utils/stream.ts";
|
||||
|
||||
// Embeddings
|
||||
export { handleEmbedding } from "./handlers/embeddings.js";
|
||||
export { handleEmbedding } from "./handlers/embeddings.ts";
|
||||
export {
|
||||
EMBEDDING_PROVIDERS,
|
||||
getEmbeddingProvider,
|
||||
parseEmbeddingModel,
|
||||
getAllEmbeddingModels,
|
||||
} from "./config/embeddingRegistry.js";
|
||||
} from "./config/embeddingRegistry.ts";
|
||||
|
||||
// Image Generation
|
||||
export { handleImageGeneration } from "./handlers/imageGeneration.js";
|
||||
export { handleImageGeneration } from "./handlers/imageGeneration.ts";
|
||||
export {
|
||||
IMAGE_PROVIDERS,
|
||||
getImageProvider,
|
||||
parseImageModel,
|
||||
getAllImageModels,
|
||||
} from "./config/imageRegistry.js";
|
||||
} from "./config/imageRegistry.ts";
|
||||
|
||||
// Think Tag Parser
|
||||
export {
|
||||
@@ -107,20 +107,20 @@ export {
|
||||
extractThinkTags,
|
||||
processStreamingThinkDelta,
|
||||
flushThinkBuffer,
|
||||
} from "./utils/thinkTagParser.js";
|
||||
} from "./utils/thinkTagParser.ts";
|
||||
|
||||
// Rerank
|
||||
export { handleRerank } from "./handlers/rerank.js";
|
||||
export { handleRerank } from "./handlers/rerank.ts";
|
||||
export {
|
||||
RERANK_PROVIDERS,
|
||||
getRerankProvider,
|
||||
parseRerankModel,
|
||||
getAllRerankModels,
|
||||
} from "./config/rerankRegistry.js";
|
||||
} from "./config/rerankRegistry.ts";
|
||||
|
||||
// Audio (Transcription + Speech)
|
||||
export { handleAudioTranscription } from "./handlers/audioTranscription.js";
|
||||
export { handleAudioSpeech } from "./handlers/audioSpeech.js";
|
||||
export { handleAudioTranscription } from "./handlers/audioTranscription.ts";
|
||||
export { handleAudioSpeech } from "./handlers/audioSpeech.ts";
|
||||
export {
|
||||
AUDIO_TRANSCRIPTION_PROVIDERS,
|
||||
AUDIO_SPEECH_PROVIDERS,
|
||||
@@ -129,13 +129,13 @@ export {
|
||||
parseTranscriptionModel,
|
||||
parseSpeechModel,
|
||||
getAllAudioModels,
|
||||
} from "./config/audioRegistry.js";
|
||||
} from "./config/audioRegistry.ts";
|
||||
|
||||
// Moderations
|
||||
export { handleModeration } from "./handlers/moderations.js";
|
||||
export { handleModeration } from "./handlers/moderations.ts";
|
||||
export {
|
||||
MODERATION_PROVIDERS,
|
||||
getModerationProvider,
|
||||
parseModerationModel,
|
||||
getAllModerationModels,
|
||||
} from "./config/moderationRegistry.js";
|
||||
} from "./config/moderationRegistry.ts";
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
RateLimitReason,
|
||||
HTTP_STATUS,
|
||||
PROVIDER_PROFILES,
|
||||
} from "../config/constants.js";
|
||||
import { getProviderCategory } from "../config/providerRegistry.js";
|
||||
} from "../config/constants.ts";
|
||||
import { getProviderCategory } from "../config/providerRegistry.ts";
|
||||
|
||||
// ─── Provider Profile Helper ────────────────────────────────────────────────
|
||||
|
||||
@@ -504,7 +504,7 @@ export function applyErrorState(account, status, errorText, provider = null) {
|
||||
* @param {object} account
|
||||
* @returns {number} score 0 = unhealthy, 100 = perfectly healthy
|
||||
*/
|
||||
export function getAccountHealth(account) {
|
||||
export function getAccountHealth(account, model?: any) {
|
||||
if (!account) return 0;
|
||||
let score = 100;
|
||||
score -= (account.backoffLevel || 0) * 10;
|
||||
@@ -5,7 +5,7 @@
|
||||
* Uses account health scores from accountFallback.js.
|
||||
*/
|
||||
|
||||
import { getAccountHealth } from "./accountFallback.js";
|
||||
import { getAccountHealth } from "./accountFallback.ts";
|
||||
|
||||
/**
|
||||
* P2C selection: pick 2 random candidates, return the healthier one.
|
||||
@@ -43,7 +43,7 @@ export function selectAccountP2C(accounts, model = null) {
|
||||
* @param {string} [model] - Model name
|
||||
* @returns {{ account: object|null, state: object }}
|
||||
*/
|
||||
export function selectAccount(accounts, strategy = "fill-first", state = {}, model = null) {
|
||||
export function selectAccount(accounts, strategy = "fill-first", state: any = {}, model = null) {
|
||||
if (!accounts || accounts.length === 0) {
|
||||
return { account: null, state };
|
||||
}
|
||||
@@ -3,13 +3,13 @@
|
||||
* Supports: priority, weighted, round-robin, random, least-used, and cost-optimized strategies
|
||||
*/
|
||||
|
||||
import { checkFallbackError, formatRetryAfter, getProviderProfile } from "./accountFallback.js";
|
||||
import { unavailableResponse } from "../utils/error.js";
|
||||
import { recordComboRequest, getComboMetrics } from "./comboMetrics.js";
|
||||
import { resolveComboConfig, getDefaultComboConfig } from "./comboConfig.js";
|
||||
import * as semaphore from "./rateLimitSemaphore.js";
|
||||
import { getCircuitBreaker } from "../../src/shared/utils/circuitBreaker.js";
|
||||
import { parseModel } from "./model.js";
|
||||
import { checkFallbackError, formatRetryAfter, getProviderProfile } from "./accountFallback.ts";
|
||||
import { unavailableResponse } from "../utils/error.ts";
|
||||
import { recordComboRequest, getComboMetrics } from "./comboMetrics.ts";
|
||||
import { resolveComboConfig, getDefaultComboConfig } from "./comboConfig.ts";
|
||||
import * as semaphore from "./rateLimitSemaphore.ts";
|
||||
import { getCircuitBreaker } from "../../src/shared/utils/circuitBreaker";
|
||||
import { parseModel } from "./model.ts";
|
||||
|
||||
// Status codes that should mark semaphore + record circuit breaker failures
|
||||
const TRANSIENT_FOR_BREAKER = [429, 502, 503, 504];
|
||||
@@ -170,7 +170,7 @@ function shuffleArray(arr) {
|
||||
*/
|
||||
async function sortModelsByCost(models) {
|
||||
try {
|
||||
const { getPricingForModel } = await import("../../src/lib/localDb.js");
|
||||
const { getPricingForModel } = await import("../../src/lib/localDb");
|
||||
const withCost = await Promise.all(
|
||||
models.map(async (modelStr) => {
|
||||
const parsed = parseModel(modelStr);
|
||||
@@ -221,6 +221,7 @@ function sortModelsByUsage(models, comboName) {
|
||||
* @param {Object} options.log - Logger object
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
/** @param {any} options */
|
||||
export async function handleComboChat({
|
||||
body,
|
||||
combo,
|
||||
@@ -27,7 +27,7 @@ const DEFAULT_COMBO_CONFIG = {
|
||||
* @param {string} [provider] - Optional provider to apply provider-level overrides
|
||||
* @returns {Object} Resolved config
|
||||
*/
|
||||
export function resolveComboConfig(combo, settings, provider) {
|
||||
export function resolveComboConfig(combo, settings, provider?: any) {
|
||||
const global = settings?.comboDefaults || {};
|
||||
const providerOverride = provider ? settings?.providerOverrides?.[provider] || {} : {};
|
||||
const comboConfig = combo?.config || {};
|
||||
@@ -35,7 +35,7 @@ export function recordComboRequest(
|
||||
});
|
||||
}
|
||||
|
||||
const combo = metrics.get(comboName);
|
||||
const combo: any = metrics.get(comboName);
|
||||
combo.totalRequests++;
|
||||
combo.totalLatencyMs += latencyMs;
|
||||
combo.totalFallbacks += fallbackCount;
|
||||
@@ -81,7 +81,7 @@ export function recordComboRequest(
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
export function getComboMetrics(comboName) {
|
||||
const combo = metrics.get(comboName);
|
||||
const combo: any = metrics.get(comboName);
|
||||
if (!combo) return null;
|
||||
|
||||
return {
|
||||
@@ -93,7 +93,7 @@ export function getComboMetrics(comboName) {
|
||||
fallbackRate:
|
||||
combo.totalRequests > 0 ? Math.round((combo.totalFallbacks / combo.totalRequests) * 100) : 0,
|
||||
byModel: Object.fromEntries(
|
||||
Object.entries(combo.byModel).map(([model, m]) => [
|
||||
Object.entries(combo.byModel).map(([model, m]: [string, any]) => [
|
||||
model,
|
||||
{
|
||||
...m,
|
||||
@@ -110,7 +110,7 @@ export function getComboMetrics(comboName) {
|
||||
* @returns {Object} Map of comboName → metrics
|
||||
*/
|
||||
export function getAllComboMetrics() {
|
||||
const result = {};
|
||||
const result: Record<string, any> = {};
|
||||
for (const [name] of metrics) {
|
||||
result[name] = getComboMetrics(name);
|
||||
}
|
||||
@@ -51,7 +51,7 @@ export function getTokenLimit(provider, model = null) {
|
||||
* @param {object} options - { provider?, model?, maxTokens?, reserveTokens? }
|
||||
* @returns {{ body: object, compressed: boolean, stats: object }}
|
||||
*/
|
||||
export function compressContext(body, options = {}) {
|
||||
export function compressContext(body, options: any = {}) {
|
||||
if (!body || !body.messages || !Array.isArray(body.messages)) {
|
||||
return { body, compressed: false, stats: {} };
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PROVIDER_ID_TO_ALIAS, PROVIDER_MODELS } from "../config/providerModels.js";
|
||||
import { PROVIDER_ID_TO_ALIAS, PROVIDER_MODELS } from "../config/providerModels.ts";
|
||||
|
||||
// Derive alias→provider mapping from the single source of truth (PROVIDER_ID_TO_ALIAS)
|
||||
// This prevents the two maps from drifting out of sync
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PROVIDERS } from "../config/constants.js";
|
||||
import { getRegistryEntry } from "../config/providerRegistry.js";
|
||||
import { PROVIDERS } from "../config/constants.ts";
|
||||
import { getRegistryEntry } from "../config/providerRegistry.ts";
|
||||
|
||||
const OPENAI_COMPATIBLE_PREFIX = "openai-compatible-";
|
||||
const OPENAI_COMPATIBLE_DEFAULTS = {
|
||||
@@ -156,7 +156,7 @@ export function getProviderFallbackCount(provider) {
|
||||
}
|
||||
|
||||
// Build provider URL
|
||||
export function buildProviderUrl(provider, model, stream = true, options = {}) {
|
||||
export function buildProviderUrl(provider, model, stream = true, options: any = {}) {
|
||||
if (isOpenAICompatible(provider)) {
|
||||
const apiType = getOpenAICompatibleType(provider);
|
||||
const baseUrl = options?.baseUrl || OPENAI_COMPATIBLE_DEFAULTS.baseUrl;
|
||||
@@ -9,9 +9,9 @@
|
||||
*/
|
||||
|
||||
import Bottleneck from "bottleneck";
|
||||
import { parseRetryAfterFromBody, lockModel } from "./accountFallback.js";
|
||||
import { getProviderCategory } from "../config/providerRegistry.js";
|
||||
import { DEFAULT_API_LIMITS } from "../config/constants.js";
|
||||
import { parseRetryAfterFromBody, lockModel } from "./accountFallback.ts";
|
||||
import { getProviderCategory } from "../config/providerRegistry.ts";
|
||||
import { DEFAULT_API_LIMITS } from "../config/constants.ts";
|
||||
|
||||
// Store limiters keyed by "provider:connectionId" (and optionally ":model")
|
||||
const limiters = new Map();
|
||||
@@ -40,7 +40,7 @@ export async function initializeRateLimits() {
|
||||
initialized = true;
|
||||
|
||||
try {
|
||||
const { getProviderConnections } = await import("@/lib/localDb.js");
|
||||
const { getProviderConnections } = await import("@/lib/localDb");
|
||||
const connections = await getProviderConnections();
|
||||
let explicitCount = 0;
|
||||
let autoCount = 0;
|
||||
@@ -293,7 +293,7 @@ export function updateFromHeaders(provider, connectionId, headers, status, model
|
||||
// Calculate optimal minTime from RPM limit
|
||||
const minTime = Math.max(0, Math.floor(60000 / limit) - 10); // Small buffer
|
||||
|
||||
const updates = { minTime };
|
||||
const updates: Record<string, any> = { minTime };
|
||||
|
||||
// If remaining is low (< 10% of limit), set reservoir to throttle immediately
|
||||
if (!isNaN(remaining)) {
|
||||
@@ -348,7 +348,7 @@ export function getRateLimitStatus(provider, connectionId) {
|
||||
* Get all active limiters status (for dashboard overview)
|
||||
*/
|
||||
export function getAllRateLimitStatus() {
|
||||
const result = {};
|
||||
const result: Record<string, any> = {};
|
||||
for (const [key, limiter] of limiters) {
|
||||
const counts = limiter.counts();
|
||||
result[key] = {
|
||||
@@ -117,7 +117,7 @@ export function acquire(modelStr, { maxConcurrency = 3, timeoutMs = 30000 } = {}
|
||||
const idx = gate.queue.findIndex((item) => item.timer === timer);
|
||||
if (idx !== -1) gate.queue.splice(idx, 1);
|
||||
const err = new Error(`Semaphore timeout after ${timeoutMs}ms for ${modelStr}`);
|
||||
err.code = "SEMAPHORE_TIMEOUT";
|
||||
(err as any).code = "SEMAPHORE_TIMEOUT";
|
||||
reject(err);
|
||||
}, timeoutMs);
|
||||
|
||||
@@ -36,7 +36,7 @@ _cleanupTimer.unref();
|
||||
* @param {object} [options] - Extra context
|
||||
* @returns {string} Session ID (hex hash)
|
||||
*/
|
||||
export function generateSessionId(body, options = {}) {
|
||||
export function generateSessionId(body, options: any = {}) {
|
||||
const parts = [];
|
||||
|
||||
// Model contributes to fingerprint
|
||||
@@ -34,7 +34,7 @@ const MAX_PATTERNS_PER_KEY = 50;
|
||||
* @param {object} context - { tool?, modelFamily?, sessionId? }
|
||||
* @returns {string[]} Array of unique signature patterns
|
||||
*/
|
||||
export function getSignatures(context = {}) {
|
||||
export function getSignatures(context: any = {}) {
|
||||
const patterns = new Set(DEFAULT_SIGNATURES);
|
||||
|
||||
// Layer 1: Tool (e.g., "cursor", "cline", "antigravity")
|
||||
@@ -61,7 +61,7 @@ export function getSignatures(context = {}) {
|
||||
* @param {string} pattern - The signature pattern (e.g., "<antThinking>")
|
||||
* @param {object} context - { tool?, modelFamily?, sessionId? }
|
||||
*/
|
||||
export function addSignature(pattern, context = {}) {
|
||||
export function addSignature(pattern: any, context: any = {}) {
|
||||
if (!pattern || typeof pattern !== "string") return;
|
||||
|
||||
const addToLayer = (layer, key) => {
|
||||
@@ -93,7 +93,7 @@ export function addSignature(pattern, context = {}) {
|
||||
* @param {object} context - { tool?, modelFamily?, sessionId? }
|
||||
* @returns {{ found: string[], cleaned: string }} Detected tags and cleaned text
|
||||
*/
|
||||
export function detectAndLearn(text, context = {}) {
|
||||
export function detectAndLearn(text: any, context: any = {}) {
|
||||
if (!text || typeof text !== "string") return { found: [], cleaned: text };
|
||||
|
||||
const found = [];
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.js";
|
||||
import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.ts";
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
// Token expiry buffer (refresh if expires within 5 minutes)
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2,7 +2,7 @@
|
||||
* Usage Fetcher - Get usage data from provider APIs
|
||||
*/
|
||||
|
||||
import { PROVIDERS } from "../config/constants.js";
|
||||
import { PROVIDERS } from "../config/constants.ts";
|
||||
|
||||
// GitHub API config
|
||||
const GITHUB_CONFIG = {
|
||||
@@ -38,7 +38,7 @@ const CLAUDE_CONFIG = {
|
||||
/**
|
||||
* Get usage data for a provider connection
|
||||
* @param {Object} connection - Provider connection with accessToken
|
||||
* @returns {Object} Usage data with quotas
|
||||
* @returns {Promise<any>} Usage data with quotas
|
||||
*/
|
||||
export async function getUsageForProvider(connection) {
|
||||
const { provider, accessToken, providerSpecificData } = connection;
|
||||
@@ -49,7 +49,7 @@ export async function getUsageForProvider(connection) {
|
||||
case "gemini-cli":
|
||||
return await getGeminiUsage(accessToken);
|
||||
case "antigravity":
|
||||
return await getAntigravityUsage(accessToken);
|
||||
return await getAntigravityUsage(accessToken, undefined);
|
||||
case "claude":
|
||||
return await getClaudeUsage(accessToken);
|
||||
case "codex":
|
||||
@@ -311,7 +311,7 @@ async function getAntigravityUsage(accessToken, providerSpecificData) {
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const quotas = {};
|
||||
const quotas: Record<string, any> = {};
|
||||
|
||||
// Parse model quotas (inspired by vscode-antigravity-cockpit)
|
||||
if (data.models) {
|
||||
@@ -328,7 +328,7 @@ async function getAntigravityUsage(accessToken, providerSpecificData) {
|
||||
"gemini-2.5-flash",
|
||||
];
|
||||
|
||||
for (const [modelKey, info] of Object.entries(data.models)) {
|
||||
for (const [modelKey, info] of Object.entries(data.models) as [string, any][]) {
|
||||
// Skip models without quota info
|
||||
if (!info.quotaInfo) {
|
||||
continue;
|
||||
+2
@@ -1,3 +1,5 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
/**
|
||||
* Responses API Transformer
|
||||
* Converts OpenAI Chat Completions SSE to Codex Responses API SSE format
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
// Claude helper functions for translator
|
||||
import { DEFAULT_THINKING_CLAUDE_SIGNATURE } from "../../config/defaultThinkingSignature.js";
|
||||
import { DEFAULT_THINKING_CLAUDE_SIGNATURE } from "../../config/defaultThinkingSignature.ts";
|
||||
|
||||
// Check if message has valid non-empty content
|
||||
export function hasValidContent(msg) {
|
||||
+1
-1
@@ -193,7 +193,7 @@ function mergeAllOf(obj) {
|
||||
if (!obj || typeof obj !== "object") return;
|
||||
|
||||
if (obj.allOf && Array.isArray(obj.allOf)) {
|
||||
const merged = {};
|
||||
const merged: Record<string, any> = {};
|
||||
|
||||
for (const item of obj.allOf) {
|
||||
if (item.properties) {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { DEFAULT_MAX_TOKENS, DEFAULT_MIN_TOKENS } from "../../config/constants.js";
|
||||
import { DEFAULT_MAX_TOKENS, DEFAULT_MIN_TOKENS } from "../../config/constants.ts";
|
||||
|
||||
/**
|
||||
* Adjust max_tokens based on request context
|
||||
@@ -1,9 +1,9 @@
|
||||
import { FORMATS } from "./formats.js";
|
||||
import { ensureToolCallIds, fixMissingToolResponses } from "./helpers/toolCallHelper.js";
|
||||
import { prepareClaudeRequest } from "./helpers/claudeHelper.js";
|
||||
import { filterToOpenAIFormat } from "./helpers/openaiHelper.js";
|
||||
import { normalizeThinkingConfig } from "../services/provider.js";
|
||||
import { applyThinkingBudget } from "../services/thinkingBudget.js";
|
||||
import { FORMATS } from "./formats.ts";
|
||||
import { ensureToolCallIds, fixMissingToolResponses } from "./helpers/toolCallHelper.ts";
|
||||
import { prepareClaudeRequest } from "./helpers/claudeHelper.ts";
|
||||
import { filterToOpenAIFormat } from "./helpers/openaiHelper.ts";
|
||||
import { normalizeThinkingConfig } from "../services/provider.ts";
|
||||
import { applyThinkingBudget } from "../services/thinkingBudget.ts";
|
||||
|
||||
// Registry for translators.
|
||||
// NOTE: translator modules import this file and call register() at module-load time.
|
||||
@@ -88,24 +88,24 @@ export function register(from, to, requestFn, responseFn) {
|
||||
}
|
||||
|
||||
// Translator modules self-register via register() on import
|
||||
import "./request/claude-to-openai.js";
|
||||
import "./request/openai-to-claude.js";
|
||||
import "./request/gemini-to-openai.js";
|
||||
import "./request/openai-to-gemini.js";
|
||||
import "./request/antigravity-to-openai.js";
|
||||
import "./request/openai-responses.js";
|
||||
import "./request/openai-to-kiro.js";
|
||||
import "./request/openai-to-cursor.js";
|
||||
import "./request/claude-to-gemini.js";
|
||||
import "./request/claude-to-openai.ts";
|
||||
import "./request/openai-to-claude.ts";
|
||||
import "./request/gemini-to-openai.ts";
|
||||
import "./request/openai-to-gemini.ts";
|
||||
import "./request/antigravity-to-openai.ts";
|
||||
import "./request/openai-responses.ts";
|
||||
import "./request/openai-to-kiro.ts";
|
||||
import "./request/openai-to-cursor.ts";
|
||||
import "./request/claude-to-gemini.ts";
|
||||
|
||||
import "./response/claude-to-openai.js";
|
||||
import "./response/openai-to-claude.js";
|
||||
import "./response/gemini-to-openai.js";
|
||||
import "./response/gemini-to-claude.js";
|
||||
import "./response/openai-to-antigravity.js";
|
||||
import "./response/openai-responses.js";
|
||||
import "./response/kiro-to-openai.js";
|
||||
import "./response/cursor-to-openai.js";
|
||||
import "./response/claude-to-openai.ts";
|
||||
import "./response/openai-to-claude.ts";
|
||||
import "./response/gemini-to-openai.ts";
|
||||
import "./response/gemini-to-claude.ts";
|
||||
import "./response/openai-to-antigravity.ts";
|
||||
import "./response/openai-responses.ts";
|
||||
import "./response/kiro-to-openai.ts";
|
||||
import "./response/cursor-to-openai.ts";
|
||||
|
||||
// Translate request: source -> openai -> target
|
||||
export function translateRequest(
|
||||
@@ -234,7 +234,7 @@ export function translateResponse(targetFormat, sourceFormat, chunk, state) {
|
||||
|
||||
// Attach OpenAI intermediate results for logging
|
||||
if (openaiResults && sourceFormat !== FORMATS.OPENAI && targetFormat !== FORMATS.OPENAI) {
|
||||
results._openaiIntermediate = openaiResults;
|
||||
(results as any)._openaiIntermediate = openaiResults;
|
||||
}
|
||||
|
||||
return results;
|
||||
+6
-6
@@ -1,12 +1,12 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { adjustMaxTokens } from "../helpers/maxTokensHelper.js";
|
||||
import { register } from "../index.ts";
|
||||
import { FORMATS } from "../formats.ts";
|
||||
import { adjustMaxTokens } from "../helpers/maxTokensHelper.ts";
|
||||
|
||||
// Convert Antigravity request to OpenAI format
|
||||
// Antigravity body: { project, model, userAgent, requestType, requestId, request: { contents, systemInstruction, tools, toolConfig, generationConfig, sessionId } }
|
||||
export function antigravityToOpenAIRequest(model, body, stream) {
|
||||
const req = body.request || body;
|
||||
const result = {
|
||||
const result: Record<string, any> = {
|
||||
model: model,
|
||||
messages: [],
|
||||
stream: stream,
|
||||
@@ -190,7 +190,7 @@ function convertContent(content) {
|
||||
|
||||
// Assistant with tool calls
|
||||
if (toolCalls.length > 0) {
|
||||
const msg = { role: "assistant" };
|
||||
const msg: Record<string, any> = { role: "assistant" };
|
||||
if (textParts.length > 0) {
|
||||
msg.content =
|
||||
textParts.length === 1 && textParts[0].type === "text" ? textParts[0].text : textParts;
|
||||
@@ -204,7 +204,7 @@ function convertContent(content) {
|
||||
|
||||
// Regular message
|
||||
if (textParts.length > 0 || reasoningContent) {
|
||||
const msg = { role };
|
||||
const msg: Record<string, any> = { role };
|
||||
if (textParts.length > 0) {
|
||||
msg.content =
|
||||
textParts.length === 1 && textParts[0].type === "text" ? textParts[0].text : textParts;
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { DEFAULT_SAFETY_SETTINGS, tryParseJSON } from "../helpers/geminiHelper.js";
|
||||
import { DEFAULT_THINKING_GEMINI_SIGNATURE } from "../../config/defaultThinkingSignature.js";
|
||||
import { register } from "../index.ts";
|
||||
import { FORMATS } from "../formats.ts";
|
||||
import { DEFAULT_SAFETY_SETTINGS, tryParseJSON } from "../helpers/geminiHelper.ts";
|
||||
import { DEFAULT_THINKING_GEMINI_SIGNATURE } from "../../config/defaultThinkingSignature.ts";
|
||||
|
||||
/**
|
||||
* Direct Claude → Gemini request translator.
|
||||
@@ -9,7 +9,7 @@ import { DEFAULT_THINKING_GEMINI_SIGNATURE } from "../../config/defaultThinkingS
|
||||
* skipping the OpenAI hub intermediate step.
|
||||
*/
|
||||
export function claudeToGeminiRequest(model, body, stream) {
|
||||
const result = {
|
||||
const result: Record<string, any> = {
|
||||
model: model,
|
||||
contents: [],
|
||||
generationConfig: {},
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { adjustMaxTokens } from "../helpers/maxTokensHelper.js";
|
||||
import { register } from "../index.ts";
|
||||
import { FORMATS } from "../formats.ts";
|
||||
import { adjustMaxTokens } from "../helpers/maxTokensHelper.ts";
|
||||
|
||||
// Convert Claude request to OpenAI format
|
||||
export function claudeToOpenAIRequest(model, body, stream) {
|
||||
const result = {
|
||||
const result: Record<string, any> = {
|
||||
model: model,
|
||||
messages: [],
|
||||
stream: stream,
|
||||
@@ -186,7 +186,7 @@ function convertClaudeMessage(msg) {
|
||||
|
||||
// If has tool calls, return assistant message with tool_calls
|
||||
if (toolCalls.length > 0) {
|
||||
const result = { role: "assistant" };
|
||||
const result: Record<string, any> = { role: "assistant" };
|
||||
if (parts.length > 0) {
|
||||
result.content = parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts;
|
||||
}
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { adjustMaxTokens } from "../helpers/maxTokensHelper.js";
|
||||
import { register } from "../index.ts";
|
||||
import { FORMATS } from "../formats.ts";
|
||||
import { adjustMaxTokens } from "../helpers/maxTokensHelper.ts";
|
||||
|
||||
// Convert Gemini request to OpenAI format
|
||||
export function geminiToOpenAIRequest(model, body, stream) {
|
||||
const result = {
|
||||
const result: Record<string, any> = {
|
||||
model: model,
|
||||
messages: [],
|
||||
stream: stream,
|
||||
@@ -116,7 +116,7 @@ function convertGeminiContent(content) {
|
||||
}
|
||||
|
||||
if (toolCalls.length > 0) {
|
||||
const result = { role: "assistant" };
|
||||
const result: Record<string, any> = { role: "assistant" };
|
||||
if (parts.length > 0) {
|
||||
result.content = parts.length === 1 ? parts[0].text : parts;
|
||||
}
|
||||
+8
-8
@@ -4,8 +4,8 @@
|
||||
* Responses API uses: { input: [...], instructions: "..." }
|
||||
* Chat API uses: { messages: [...] }
|
||||
*/
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { register } from "../index.ts";
|
||||
import { FORMATS } from "../formats.ts";
|
||||
|
||||
/**
|
||||
* Convert OpenAI Responses API request to OpenAI Chat Completions format
|
||||
@@ -21,8 +21,8 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
|
||||
const error = new Error(
|
||||
`Unsupported Responses API feature: ${tool.type} tool type is not supported by omniroute`
|
||||
);
|
||||
error.statusCode = 400;
|
||||
error.errorType = "unsupported_feature";
|
||||
(error as any).statusCode = 400;
|
||||
(error as any).errorType = "unsupported_feature";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -31,12 +31,12 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
|
||||
const error = new Error(
|
||||
"Unsupported Responses API feature: background mode is not supported by omniroute"
|
||||
);
|
||||
error.statusCode = 400;
|
||||
error.errorType = "unsupported_feature";
|
||||
(error as any).statusCode = 400;
|
||||
(error as any).errorType = "unsupported_feature";
|
||||
throw error;
|
||||
}
|
||||
|
||||
const result = { ...body };
|
||||
const result: Record<string, any> = { ...body };
|
||||
result.messages = [];
|
||||
|
||||
// Convert instructions to system message
|
||||
@@ -159,7 +159,7 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
|
||||
* Convert OpenAI Chat Completions to OpenAI Responses API format
|
||||
*/
|
||||
export function openaiToOpenAIResponsesRequest(model, body, stream, credentials) {
|
||||
const result = {
|
||||
const result: Record<string, any> = {
|
||||
model,
|
||||
input: [],
|
||||
stream: true,
|
||||
+6
-6
@@ -1,8 +1,8 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { CLAUDE_SYSTEM_PROMPT } from "../../config/constants.js";
|
||||
import { adjustMaxTokens } from "../helpers/maxTokensHelper.js";
|
||||
import { DEFAULT_THINKING_CLAUDE_SIGNATURE } from "../../config/defaultThinkingSignature.js";
|
||||
import { register } from "../index.ts";
|
||||
import { FORMATS } from "../formats.ts";
|
||||
import { CLAUDE_SYSTEM_PROMPT } from "../../config/constants.ts";
|
||||
import { adjustMaxTokens } from "../helpers/maxTokensHelper.ts";
|
||||
import { DEFAULT_THINKING_CLAUDE_SIGNATURE } from "../../config/defaultThinkingSignature.ts";
|
||||
|
||||
// Prefix for Claude OAuth tool names to avoid conflicts
|
||||
const CLAUDE_OAUTH_TOOL_PREFIX = "proxy_";
|
||||
@@ -11,7 +11,7 @@ const CLAUDE_OAUTH_TOOL_PREFIX = "proxy_";
|
||||
export function openaiToClaudeRequest(model, body, stream) {
|
||||
// Tool name mapping for Claude OAuth (capitalizedName → originalName)
|
||||
const toolNameMap = new Map();
|
||||
const result = {
|
||||
const result: Record<string, any> = {
|
||||
model: model,
|
||||
max_tokens: adjustMaxTokens(body),
|
||||
stream: stream,
|
||||
+4
-5
@@ -2,8 +2,8 @@
|
||||
* OpenAI to Cursor Request Translator
|
||||
* Converts OpenAI messages to Cursor simple format
|
||||
*/
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { register } from "../index.ts";
|
||||
import { FORMATS } from "../formats.ts";
|
||||
|
||||
/**
|
||||
* Convert OpenAI messages to Cursor format with native tool_results support
|
||||
@@ -66,7 +66,7 @@ function convertMessages(messages) {
|
||||
|
||||
// Keep tool_calls structure for assistant messages
|
||||
if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
|
||||
const assistantMsg = { role: "assistant" };
|
||||
const assistantMsg: Record<string, any> = { role: "assistant" };
|
||||
if (content) {
|
||||
assistantMsg.content = content;
|
||||
}
|
||||
@@ -80,8 +80,7 @@ function convertMessages(messages) {
|
||||
|
||||
result.push(assistantMsg);
|
||||
} else if (content || pendingToolResults.length > 0) {
|
||||
const msgObj = {
|
||||
role: msg.role,
|
||||
const msgObj: Record<string, any> = { role: msg.role,
|
||||
content: content || "",
|
||||
};
|
||||
|
||||
+10
-10
@@ -1,8 +1,8 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { DEFAULT_THINKING_GEMINI_SIGNATURE } from "../../config/defaultThinkingSignature.js";
|
||||
import { ANTIGRAVITY_DEFAULT_SYSTEM } from "../../config/constants.js";
|
||||
import { openaiToClaudeRequestForAntigravity } from "./openai-to-claude.js";
|
||||
import { register } from "../index.ts";
|
||||
import { FORMATS } from "../formats.ts";
|
||||
import { DEFAULT_THINKING_GEMINI_SIGNATURE } from "../../config/defaultThinkingSignature.ts";
|
||||
import { ANTIGRAVITY_DEFAULT_SYSTEM } from "../../config/constants.ts";
|
||||
import { openaiToClaudeRequestForAntigravity } from "./openai-to-claude.ts";
|
||||
|
||||
function generateUUID() {
|
||||
return crypto.randomUUID();
|
||||
@@ -17,11 +17,11 @@ import {
|
||||
generateSessionId,
|
||||
generateProjectId,
|
||||
cleanJSONSchemaForAntigravity,
|
||||
} from "../helpers/geminiHelper.js";
|
||||
} from "../helpers/geminiHelper.ts";
|
||||
|
||||
// Core: Convert OpenAI request to Gemini format (base for all variants)
|
||||
function openaiToGeminiBase(model, body, stream) {
|
||||
const result = {
|
||||
const result: Record<string, any> = {
|
||||
model: model,
|
||||
contents: [],
|
||||
generationConfig: {},
|
||||
@@ -253,7 +253,7 @@ export function openaiToGeminiCLIRequest(model, body, stream) {
|
||||
function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigravity = false) {
|
||||
const projectId = credentials?.projectId || generateProjectId();
|
||||
|
||||
const envelope = {
|
||||
const envelope: Record<string, any> = {
|
||||
project: projectId,
|
||||
model: model,
|
||||
userAgent: isAntigravity ? "antigravity" : "gemini-cli",
|
||||
@@ -272,7 +272,7 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra
|
||||
envelope.requestType = "agent";
|
||||
|
||||
// Inject required default system prompt for Antigravity
|
||||
const defaultPart = { text: ANTIGRAVITY_DEFAULT_SYSTEM };
|
||||
const defaultPart: Record<string, any> = { text: ANTIGRAVITY_DEFAULT_SYSTEM };
|
||||
if (envelope.request.systemInstruction?.parts) {
|
||||
envelope.request.systemInstruction.parts.unshift(defaultPart);
|
||||
} else {
|
||||
@@ -297,7 +297,7 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra
|
||||
function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = null) {
|
||||
const projectId = credentials?.projectId || generateProjectId();
|
||||
|
||||
const envelope = {
|
||||
const envelope: Record<string, any> = {
|
||||
project: projectId,
|
||||
model: model,
|
||||
userAgent: "antigravity",
|
||||
+4
-4
@@ -2,8 +2,8 @@
|
||||
* OpenAI to Kiro Request Translator
|
||||
* Converts OpenAI Chat Completions format to Kiro/AWS CodeWhisperer format
|
||||
*/
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { register } from "../index.ts";
|
||||
import { FORMATS } from "../formats.ts";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
/**
|
||||
@@ -22,7 +22,7 @@ function convertMessages(messages, tools, model) {
|
||||
const flushPending = () => {
|
||||
if (currentRole === "user") {
|
||||
const content = pendingUserContent.join("\n\n").trim() || "continue";
|
||||
const userMsg = {
|
||||
const userMsg: Record<string, any> = {
|
||||
userInputMessage: {
|
||||
content: content,
|
||||
modelId: "",
|
||||
@@ -255,7 +255,7 @@ export function buildKiroPayload(model, body, stream, credentials) {
|
||||
const timestamp = new Date().toISOString();
|
||||
finalContent = `[Context: Current time is ${timestamp}]\n\n${finalContent}`;
|
||||
|
||||
const payload = {
|
||||
const payload: Record<string, any> = {
|
||||
conversationState: {
|
||||
chatTriggerType: "MANUAL",
|
||||
conversationId: uuidv4(),
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { register } from "../index.ts";
|
||||
import { FORMATS } from "../formats.ts";
|
||||
|
||||
// Create OpenAI chunk helper
|
||||
function createChunk(state, delta, finishReason = null) {
|
||||
@@ -133,7 +133,7 @@ export function claudeToOpenAIResponse(chunk, state) {
|
||||
|
||||
if (chunk.delta?.stop_reason) {
|
||||
state.finishReason = convertStopReason(chunk.delta.stop_reason);
|
||||
const finalChunk = {
|
||||
const finalChunk: Record<string, any> = {
|
||||
id: `chatcmpl-${state.messageId}`,
|
||||
object: "chat.completion.chunk",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
+2
-2
@@ -2,8 +2,8 @@
|
||||
* Cursor to OpenAI Response Translator
|
||||
* CursorExecutor already emits OpenAI format - this is a passthrough
|
||||
*/
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { register } from "../index.ts";
|
||||
import { FORMATS } from "../formats.ts";
|
||||
|
||||
/**
|
||||
* Convert Cursor response to OpenAI format
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { register } from "../index.ts";
|
||||
import { FORMATS } from "../formats.ts";
|
||||
|
||||
/**
|
||||
* Direct Gemini → Claude response translator.
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { register } from "../index.ts";
|
||||
import { FORMATS } from "../formats.ts";
|
||||
|
||||
// Convert Gemini response chunk to OpenAI format
|
||||
export function geminiToOpenAIResponse(chunk, state) {
|
||||
@@ -226,7 +226,7 @@ export function geminiToOpenAIResponse(chunk, state) {
|
||||
finishReason = "tool_calls";
|
||||
}
|
||||
|
||||
const finalChunk = {
|
||||
const finalChunk: Record<string, any> = {
|
||||
id: `chatcmpl-${state.messageId}`,
|
||||
object: "chat.completion.chunk",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
+3
-3
@@ -2,8 +2,8 @@
|
||||
* Kiro to OpenAI Response Translator
|
||||
* Converts Kiro/AWS CodeWhisperer streaming events to OpenAI SSE format
|
||||
*/
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { register } from "../index.ts";
|
||||
import { FORMATS } from "../formats.ts";
|
||||
|
||||
/**
|
||||
* Parse Kiro SSE event and convert to OpenAI format
|
||||
@@ -155,7 +155,7 @@ export function convertKiroToOpenAI(chunk, state) {
|
||||
if (eventType === "messageStopEvent" || eventType === "done" || data.messageStopEvent) {
|
||||
state.finishReason = "stop"; // Mark for usage injection in stream.js
|
||||
|
||||
const openaiChunk = {
|
||||
const openaiChunk: Record<string, any> = {
|
||||
id: state.responseId,
|
||||
object: "chat.completion.chunk",
|
||||
created: state.created,
|
||||
+3
-3
@@ -2,8 +2,8 @@
|
||||
* Translator: OpenAI Chat Completions → OpenAI Responses API (response)
|
||||
* Converts streaming chunks from Chat Completions to Responses API events
|
||||
*/
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { register } from "../index.ts";
|
||||
import { FORMATS } from "../formats.ts";
|
||||
|
||||
/**
|
||||
* Translate OpenAI chunk to Responses API events
|
||||
@@ -519,7 +519,7 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
|
||||
state.finishReasonSent = true;
|
||||
state.finishReason = "stop"; // Mark for usage injection in stream.js
|
||||
|
||||
const finalChunk = {
|
||||
const finalChunk: Record<string, any> = {
|
||||
id: state.chatId,
|
||||
object: "chat.completion.chunk",
|
||||
created: state.created,
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { register } from "../index.ts";
|
||||
import { FORMATS } from "../formats.ts";
|
||||
|
||||
// Convert OpenAI SSE chunk to Antigravity SSE format
|
||||
// Real Antigravity format:
|
||||
@@ -81,7 +81,7 @@ export function openaiToAntigravityResponse(chunk, state) {
|
||||
}
|
||||
|
||||
// Build candidate
|
||||
const candidate = { content: { role: "model", parts } };
|
||||
const candidate: Record<string, any> = { content: { role: "model", parts } };
|
||||
|
||||
// Finish reason mapping
|
||||
if (finishReason) {
|
||||
@@ -95,7 +95,7 @@ export function openaiToAntigravityResponse(chunk, state) {
|
||||
}
|
||||
|
||||
// Build response
|
||||
const response = {
|
||||
const response: Record<string, any> = {
|
||||
candidates: [candidate],
|
||||
modelVersion: state._modelVersion,
|
||||
responseId: state._responseId,
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { register } from "../index.ts";
|
||||
import { FORMATS } from "../formats.ts";
|
||||
|
||||
// Prefix for Claude OAuth tool names (must match request translator)
|
||||
const CLAUDE_OAUTH_TOOL_PREFIX = "proxy_";
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"strict": false,
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["dom", "esnext"],
|
||||
"baseUrl": "..",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@omniroute/open-sse": ["./open-sse"],
|
||||
"@omniroute/open-sse/*": ["./open-sse/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.js"
|
||||
]
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { detectFormat } from "../services/provider.js";
|
||||
import { translateResponse, initState } from "../translator/index.js";
|
||||
import { FORMATS } from "../translator/formats.js";
|
||||
import { SKIP_PATTERNS } from "../config/constants.js";
|
||||
import { formatSSE } from "./stream.js";
|
||||
import { detectFormat } from "../services/provider.ts";
|
||||
import { translateResponse, initState } from "../translator/index.ts";
|
||||
import { FORMATS } from "../translator/formats.ts";
|
||||
import { SKIP_PATTERNS } from "../config/constants.ts";
|
||||
import { formatSSE } from "./stream.ts";
|
||||
|
||||
/**
|
||||
* Check for bypass patterns — return fake response without calling provider.
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ERROR_TYPES, DEFAULT_ERROR_MESSAGES } from "../config/constants.js";
|
||||
import { ERROR_TYPES, DEFAULT_ERROR_MESSAGES } from "../config/constants.ts";
|
||||
|
||||
/**
|
||||
* Build OpenAI-compatible error response body
|
||||
@@ -131,8 +131,8 @@ export async function parseUpstreamError(response, provider = null) {
|
||||
* @param {number|null} retryAfterMs - Optional retry-after time in milliseconds
|
||||
* @returns {{ success: false, status: number, error: string, response: Response, retryAfterMs?: number }}
|
||||
*/
|
||||
export function createErrorResult(statusCode, message, retryAfterMs = null) {
|
||||
const result = {
|
||||
export function createErrorResult(statusCode: number, message: string, retryAfterMs: number | null = null) {
|
||||
const result: Record<string, any> = {
|
||||
success: false,
|
||||
status: statusCode,
|
||||
error: message,
|
||||
@@ -155,7 +155,7 @@ export function createErrorResult(statusCode, message, retryAfterMs = null) {
|
||||
* @param {string} retryAfterHuman - Human-readable retry info e.g. "reset after 30s"
|
||||
* @returns {Response}
|
||||
*/
|
||||
export function unavailableResponse(statusCode, message, retryAfter, retryAfterHuman) {
|
||||
export function unavailableResponse(statusCode, message, retryAfter?: any, retryAfterHuman?: any) {
|
||||
const retryAfterSec = Math.max(
|
||||
Math.ceil((new Date(retryAfter).getTime() - Date.now()) / 1000),
|
||||
1
|
||||
@@ -7,7 +7,7 @@
|
||||
* - "json": Single-line JSON objects for log aggregators
|
||||
*
|
||||
* Usage:
|
||||
* import { logger, createLogger, generateRequestId } from "../utils/logger.js";
|
||||
* import { logger, createLogger, generateRequestId } from "../utils/logger.ts";
|
||||
*
|
||||
* // Tag-based (simple — for services/utilities):
|
||||
* const log = logger("CHAT");
|
||||
@@ -96,7 +96,7 @@ export function logger(tag) {
|
||||
const consoleFn = getConsoleFn(level);
|
||||
|
||||
if (jsonFormat) {
|
||||
const entry = {
|
||||
const entry: Record<string, any> = {
|
||||
ts: new Date().toISOString(),
|
||||
level,
|
||||
tag,
|
||||
@@ -132,7 +132,7 @@ export function createLogger(requestId = null) {
|
||||
const consoleFn = getConsoleFn(level);
|
||||
|
||||
if (jsonFormat) {
|
||||
const entry = {
|
||||
const entry: Record<string, any> = {
|
||||
ts: new Date().toISOString(),
|
||||
level,
|
||||
tag,
|
||||
@@ -20,7 +20,7 @@ async function getConfig() {
|
||||
if (_cachedConfig && now < _cacheExpiry) return _cachedConfig;
|
||||
|
||||
try {
|
||||
const { getProxyConfig } = await import("../../src/lib/localDb.js");
|
||||
const { getProxyConfig } = await import("../../src/lib/localDb");
|
||||
_cachedConfig = await getProxyConfig();
|
||||
_cacheExpiry = now + 30_000; // Cache for 30s
|
||||
return _cachedConfig;
|
||||
@@ -34,6 +34,7 @@ async function getConfig() {
|
||||
* @param {string} providerId - Provider ID (e.g., "openai", "anthropic")
|
||||
* @returns {string|null} Proxy URL or null if no proxy configured
|
||||
*/
|
||||
/** @returns {Promise<any>} */
|
||||
export async function resolveProxy(providerId) {
|
||||
const config = await getConfig();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user