Compare commits
175 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7664c55aff | |||
| faa0610efe | |||
| ebb0b80579 | |||
| 562ad901b1 | |||
| 0016c74920 | |||
| 05ea200170 | |||
| 050e289a8c | |||
| 426c5f5e1b | |||
| ba3e7c53ac | |||
| d7aa2801bb | |||
| defda32df6 | |||
| 6b1b16a80e | |||
| 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 | |||
| a1137c1fa1 | |||
| e962e4f558 | |||
| 6d3b4166df | |||
| 45eced0b3e | |||
| 1b00a6b70b | |||
| 03d05e0490 | |||
| 4cac582cb0 | |||
| eb0fbccd05 | |||
| 32c0f40021 | |||
| baae44f825 | |||
| 5fe53168b2 | |||
| ebdc1c214d | |||
| 467e998650 | |||
| 9a9f592f54 | |||
| c3fe96e221 | |||
| 1c6541f25d | |||
| 9f8dfa1398 | |||
| e4db9bff0e | |||
| 54aba4c087 | |||
| c2542b75f6 | |||
| 6572f2a1b6 | |||
| 2ad23f3b3c | |||
| fa9687ae0d | |||
| 0ed7738c7c | |||
| a43b1c9218 | |||
| 413a9f2a69 | |||
| 14714aa9f5 | |||
| ac4df197c3 | |||
| 06826d4c86 | |||
| 78b0782129 | |||
| 5c92f05079 | |||
| 16cdb345a0 | |||
| 0e238a61fb | |||
| 6964904bac | |||
| 597c590a1d | |||
| a94b6a8d8c | |||
| b08fb31a28 | |||
| 5bfd621438 | |||
| 034f1a7e1d | |||
| bc95095f82 | |||
| 7cf8ae8db6 | |||
| 05e93fa9b0 | |||
| 1acaf66dd0 | |||
| 1cee3ec8ff | |||
| 33c28f73db | |||
| 25f4f5987c | |||
| 4269c25a9f | |||
| 87b36dc197 | |||
| 6e29bd1197 | |||
| 5db6676319 | |||
| d08b3889d3 | |||
| 443e5b7b62 | |||
| b87e04db22 | |||
| 4341254b5d | |||
| 6718b23d57 | |||
| e3fc9387be | |||
| c094c9c678 | |||
| 388f06c74f | |||
| 91f6750d59 | |||
| 7e066df6cd | |||
| 33f8123835 | |||
| e87067f2fb | |||
| a9a85fdc1b | |||
| 31c09f08e1 | |||
| 3e89560a33 | |||
| 2dbb717377 | |||
| f44ec7e1f2 | |||
| ad003b6226 | |||
| c09978454b | |||
| daffd6c9ca | |||
| 492afc4ff1 | |||
| 967689d0a1 | |||
| f77dd89d53 | |||
| 0a5434cae5 | |||
| 2178e99da7 | |||
| 1cbbc33f20 | |||
| d408be489c | |||
| 81a4f2986c | |||
| c1f1069a06 | |||
| c4cdb52fe6 | |||
| 15ec100dcb | |||
| d401ef40f1 | |||
| 92b4c59fec | |||
| 4f0155eb4a | |||
| e544d81624 | |||
| b8c50cc62d | |||
| c6000ecc8e | |||
| 4e6ba7d9cc | |||
| c0c2816671 | |||
| 80cc76d531 | |||
| af1ffab63d | |||
| 7ff1ba978a | |||
| d43bbf77fe | |||
| 4e5dc6a80b | |||
| 0be2852af2 | |||
| b3c36ed2ca | |||
| 2b1b8e4539 | |||
| 8b7b50f6fb | |||
| 9c0ba39ed6 | |||
| 801a050995 | |||
| e7532d7189 | |||
| 7349052b33 | |||
| 0af22a558f | |||
| 3206583299 | |||
| 575bda7375 | |||
| b3cde151c3 | |||
| b7e757f3ba | |||
| 81a78e8733 | |||
| fbac38b2b5 | |||
| f5898d3898 | |||
| 75e9648d06 | |||
| 878277edc9 | |||
| 50fc931086 | |||
| 172461863e | |||
| 79e10c9a41 | |||
| 699329944e |
@@ -0,0 +1,54 @@
|
||||
---
|
||||
description: Git workflow — NEVER commit directly to main. Always use feature branches.
|
||||
---
|
||||
|
||||
# Git Workflow
|
||||
|
||||
## ⚠️ CRITICAL RULE: NEVER commit directly to `main`
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Before starting any work**, create a feature branch from `main`:
|
||||
|
||||
```bash
|
||||
git checkout main && git pull origin main
|
||||
git checkout -b feature/<feature-name>
|
||||
```
|
||||
|
||||
2. **During development**, commit to the feature branch:
|
||||
|
||||
```bash
|
||||
git add -A && git commit -m "<type>(<scope>): <description>"
|
||||
```
|
||||
|
||||
3. **Before pushing**, verify the build passes:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
4. **When the feature is complete and verified**, push the branch and STOP:
|
||||
|
||||
```bash
|
||||
git push origin feature/<feature-name>
|
||||
```
|
||||
|
||||
5. **DO NOT** create a PR, merge, or push to `main`. Let the user handle that.
|
||||
|
||||
## Branch naming convention
|
||||
|
||||
- `feature/<name>` — new features
|
||||
- `fix/<name>` — bugfixes
|
||||
- `refactor/<name>` — refactoring
|
||||
- `docker/<name>` — Docker / infrastructure changes
|
||||
- `style/<name>` — UI / CSS changes
|
||||
|
||||
## Commit types
|
||||
|
||||
- `feat` — new feature
|
||||
- `fix` — bugfix
|
||||
- `refactor` — code refactoring
|
||||
- `style` — UI / CSS changes
|
||||
- `docker` — Docker / infrastructure
|
||||
- `docs` — documentation
|
||||
- `chore` — maintenance
|
||||
@@ -1,131 +0,0 @@
|
||||
---
|
||||
description: Analyze open feature request issues, implement viable ones on dedicated branches, and respond to authors
|
||||
---
|
||||
|
||||
# /implement-features — Feature Request Implementation Workflow
|
||||
|
||||
## Overview
|
||||
|
||||
Fetches open feature request issues, analyzes each against the current codebase, implements viable ones on dedicated branches, and responds to authors with results. Does NOT merge to main — leaves branches for author validation.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Identify the Repository
|
||||
|
||||
// turbo
|
||||
|
||||
- Run: `git -C <project_root> remote get-url origin` to extract owner/repo
|
||||
|
||||
### 2. Fetch Open Feature Request Issues
|
||||
|
||||
// turbo
|
||||
|
||||
- Run: `gh issue list --repo <owner>/<repo> --state open --limit 50 --json number,title,labels,body,comments,createdAt,author`
|
||||
- Filter for issues that are feature requests (label `enhancement`/`feature`, or body describes new functionality, or previously classified as feature request)
|
||||
- Sort by oldest first
|
||||
|
||||
### 3. Analyze Each Feature Request
|
||||
|
||||
For each feature request issue, perform a **two-level analysis**:
|
||||
|
||||
#### Level 1 — Viability Assessment
|
||||
|
||||
Ask yourself:
|
||||
|
||||
- Does this feature align with the project's goals and architecture?
|
||||
- Is the request technically feasible with the current codebase?
|
||||
- Does it duplicate existing functionality?
|
||||
- Would it introduce breaking changes or security risks?
|
||||
- Is there enough detail to implement it?
|
||||
|
||||
**Verdict options:**
|
||||
|
||||
1. ✅ **VIABLE** — Makes sense, enough detail to implement → Go to Level 2
|
||||
2. ❓ **NEEDS MORE INFO** — Good idea but insufficient detail → Post comment asking for specifics
|
||||
3. ❌ **NOT VIABLE** — Doesn't fit the project or is fundamentally flawed → Post comment explaining why, close issue
|
||||
|
||||
#### Level 2 — Implementation (only for VIABLE features)
|
||||
|
||||
1. **Research** — Read all related source files to understand the current architecture
|
||||
2. **Design** — Plan the implementation, filling gaps in the original request
|
||||
3. **Create branch** — Name format: `feat/issue-<NUMBER>-<short-slug>`
|
||||
```bash
|
||||
git checkout main
|
||||
git pull origin main
|
||||
git checkout -b feat/issue-<NUMBER>-<short-slug>
|
||||
```
|
||||
4. **Implement** — Build the complete solution following project patterns
|
||||
5. **Build** — Run `npm run build` to verify compilation
|
||||
6. **Commit** — Commit with: `feat: <description> (#<NUMBER>)`
|
||||
7. **Push** — Push the branch: `git push -u origin feat/issue-<NUMBER>-<short-slug>`
|
||||
8. **Return to main** — `git checkout main`
|
||||
|
||||
### 4. Respond to Authors
|
||||
|
||||
#### For VIABLE (implemented) features:
|
||||
|
||||
// turbo
|
||||
Post a comment on the issue:
|
||||
|
||||
````markdown
|
||||
## ✅ Feature Implemented!
|
||||
|
||||
Hi @<author>! We've analyzed your request and implemented it on a dedicated branch.
|
||||
|
||||
**Branch:** `feat/issue-<NUMBER>-<short-slug>`
|
||||
|
||||
### What was implemented:
|
||||
|
||||
- <bullet list of what was done>
|
||||
|
||||
### How to try it:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git checkout feat/issue-<NUMBER>-<short-slug>
|
||||
npm install && npm run dev
|
||||
```
|
||||
````
|
||||
|
||||
### Next steps:
|
||||
|
||||
1. **Test it** — Please verify it works as you expected
|
||||
2. **Want to improve it?** — You're welcome to contribute! Just:
|
||||
```bash
|
||||
git checkout feat/issue-<NUMBER>-<short-slug>
|
||||
# Make your improvements
|
||||
git add -A && git commit -m "improve: <your changes>"
|
||||
git push origin feat/issue-<NUMBER>-<short-slug>
|
||||
```
|
||||
Then open a Pull Request from your branch to `main` 🎉
|
||||
3. **Not quite right?** — Let us know in this issue what needs to change
|
||||
|
||||
Looking forward to your feedback! 🚀
|
||||
|
||||
```
|
||||
|
||||
#### For NEEDS MORE INFO:
|
||||
// turbo
|
||||
Post a comment asking for specific missing details needed to implement, e.g.:
|
||||
- "Could you describe the exact behavior when X happens?"
|
||||
- "Which API endpoints should be affected?"
|
||||
- "Should this apply to all providers or only specific ones?"
|
||||
|
||||
Add the context of WHY you need each piece of information.
|
||||
|
||||
#### For NOT VIABLE:
|
||||
// turbo
|
||||
Post a polite comment explaining why the feature doesn't fit at this time:
|
||||
- If the idea is decent but timing is wrong: "This is an interesting idea, but it doesn't align with our current priorities. Feel free to open a new issue with more details if you'd like us to reconsider."
|
||||
- If fundamentally flawed: Explain the technical or architectural reasons why it won't work, suggest alternatives if possible.
|
||||
- Close the issue after posting the comment.
|
||||
|
||||
### 5. Summary Report
|
||||
Present a summary report to the user via `notify_user`:
|
||||
|
||||
| Issue | Title | Verdict | Branch / Action |
|
||||
|---|---|---|---|
|
||||
| #N | Title | ✅ Implemented | `feat/issue-N-slug` |
|
||||
| #N | Title | ❓ Needs Info | Comment posted |
|
||||
| #N | Title | ❌ Not Viable | Closed with explanation |
|
||||
```
|
||||
@@ -1,100 +0,0 @@
|
||||
---
|
||||
description: Fetch all open GitHub issues, analyze bugs, resolve what's possible, triage the rest, then commit and release
|
||||
---
|
||||
|
||||
# /resolve-issues — Automated Issue Resolution Workflow
|
||||
|
||||
## Overview
|
||||
|
||||
This workflow fetches all open issues from the project's GitHub repository, classifies them, analyzes bugs, resolves what can be fixed, triages issues with insufficient information, and generates a release with all fixes.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Identify the GitHub Repository
|
||||
|
||||
// turbo
|
||||
|
||||
- Run: `git -C <project_root> remote get-url origin` to extract the owner/repo
|
||||
- Parse the owner and repo name from the URL
|
||||
|
||||
### 2. Fetch All Open Issues
|
||||
|
||||
// turbo
|
||||
|
||||
- Run: `gh issue list --repo <owner>/<repo> --state open --limit 100 --json number,title,labels,body,comments,createdAt,author`
|
||||
- Parse the JSON output to get a list of all open issues
|
||||
- Sort by oldest first (FIFO)
|
||||
|
||||
### 3. Classify Each Issue
|
||||
|
||||
For each issue, determine its type:
|
||||
|
||||
- **Bug** — Has `bug` label, or body contains error messages, stack traces, "doesn't work", "broken", "crash", "error"
|
||||
- **Feature Request** — Has `enhancement`/`feature` label, or body describes new functionality
|
||||
- **Question** — Has `question` label, or is asking "how to" something
|
||||
- **Other** — Anything else
|
||||
|
||||
Focus ONLY on **Bugs** for resolution. Feature requests and questions should be skipped with a note in the final report.
|
||||
|
||||
### 4. Analyze Each Bug — For each bug issue:
|
||||
|
||||
#### 4a. Check Information Sufficiency
|
||||
|
||||
Verify the issue contains enough information to reproduce and fix:
|
||||
|
||||
- [ ] Clear description of the problem
|
||||
- [ ] Steps to reproduce
|
||||
- [ ] Error messages or logs
|
||||
- [ ] Expected vs actual behavior
|
||||
|
||||
#### 4b. If Information Is INSUFFICIENT
|
||||
|
||||
Call the `/issue-triage` workflow (located at `~/.gemini/antigravity/global_workflows/issue-triage.md`):
|
||||
// turbo
|
||||
|
||||
- Post a comment asking for more details using `gh issue comment`
|
||||
- Add `needs-info` label using `gh issue edit`
|
||||
- Mark this issue as **DEFERRED** and move to the next one
|
||||
|
||||
#### 4c. If Information Is SUFFICIENT
|
||||
|
||||
Proceed with resolution:
|
||||
|
||||
1. **Research** — Search the codebase for files related to the issue
|
||||
2. **Root Cause** — Identify the root cause by reading the relevant source files
|
||||
3. **Implement Fix** — Apply the fix following existing code patterns and conventions
|
||||
4. **Test** — Build the project and run tests to verify the fix
|
||||
5. **Commit** — Commit with message format: `fix: <description> (#<issue_number>)`
|
||||
|
||||
### 5. Commit All Fixes
|
||||
|
||||
After processing all issues:
|
||||
|
||||
- Ensure all fixes are committed with proper issue references
|
||||
- Each fix should be its own commit for clean git history
|
||||
|
||||
### 6. Close Resolved Issues
|
||||
|
||||
For each successfully fixed issue:
|
||||
// turbo
|
||||
|
||||
- Close with a comment: `gh issue close <NUMBER> --repo <owner>/<repo> --comment "Fixed in <commit_hash>. The fix will be included in the next release."`
|
||||
|
||||
### 7. Generate Report
|
||||
|
||||
Present a summary report to the user via `notify_user`:
|
||||
|
||||
| Issue | Title | Status | Action |
|
||||
| ----- | ----- | ------------- | --------------------------- |
|
||||
| #N | Title | ✅ Fixed | Commit hash |
|
||||
| #N | Title | ❓ Needs Info | Triage comment posted |
|
||||
| #N | Title | ⏭️ Skipped | Feature request / not a bug |
|
||||
|
||||
### 8. Update Docs & Release
|
||||
|
||||
If any fixes were committed:
|
||||
|
||||
1. Run the `/update-docs` workflow (at `~/.gemini/antigravity/global_workflows/update-docs.md`) to update CHANGELOG and README
|
||||
2. Run the `/generate-release` workflow (at `.agents/workflows/generate-release.md`) to bump version, tag, and publish
|
||||
|
||||
If NO fixes were committed, skip this step and just present the report.
|
||||
@@ -1,96 +0,0 @@
|
||||
---
|
||||
description: Analyze open Pull Requests from the project's GitHub repository, generate a critical report, and optionally implement approved changes
|
||||
---
|
||||
|
||||
# /review-prs — PR Review & Analysis Workflow
|
||||
|
||||
## Overview
|
||||
|
||||
This workflow fetches all open PRs from the project's GitHub repository, performs a critical analysis of each one, generates a detailed report, and waits for user approval before proceeding with implementation.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Identify the GitHub Repository
|
||||
|
||||
- Read `package.json` to get the repository URL, or use the git remote origin URL
|
||||
// turbo
|
||||
- Run: `git -C <project_root> remote get-url origin` to extract the owner/repo
|
||||
|
||||
### 2. Fetch Open Pull Requests
|
||||
|
||||
- Navigate to `https://github.com/<owner>/<repo>/pulls` and scrape all open PRs
|
||||
- For each open PR, collect:
|
||||
- PR number, title, author, branch, number of commits, date
|
||||
- PR description/body
|
||||
- Files changed (diff)
|
||||
- Existing review comments (from bots or humans)
|
||||
|
||||
### 3. Analyze Each PR — For each open PR, perform the following analysis:
|
||||
|
||||
#### 3a. Feature Assessment
|
||||
|
||||
- **Does it make sense?** Evaluate if the feature fills a real gap or solves a valid problem
|
||||
- **Alignment** — Check if it aligns with the project's architecture and roadmap
|
||||
- **Complexity** — Assess if the scope is reasonable or if it should be split
|
||||
|
||||
#### 3b. Code Quality Review
|
||||
|
||||
- Check for code duplication
|
||||
- Evaluate error handling patterns (consistent with existing codebase?)
|
||||
- Check naming conventions and code style
|
||||
- Verify TypeScript types (any `any` usage, missing types?)
|
||||
|
||||
#### 3c. Security Review
|
||||
|
||||
- Check for missing authentication/authorization on new endpoints
|
||||
- Check for injection vulnerabilities (URL params, SQL, XSS)
|
||||
- Verify input validation on all user-controlled data
|
||||
- Check for hardcoded secrets or credentials
|
||||
|
||||
#### 3d. Architecture Review
|
||||
|
||||
- Does the change follow existing patterns?
|
||||
- Are there any breaking changes to public APIs?
|
||||
- Is the database schema affected? Migration needed?
|
||||
- Impact on performance (N+1 queries, missing indexes?)
|
||||
|
||||
#### 3e. Test Coverage
|
||||
|
||||
- Does the PR include tests?
|
||||
- Are edge cases covered?
|
||||
- Would existing tests break?
|
||||
|
||||
### 4. Generate Report — Create a markdown report for each PR including:
|
||||
|
||||
- **PR Summary** — What it does, files affected, commit count
|
||||
- **Improvements/Benefits** — Numbered list with impact level (HIGH/MEDIUM/LOW)
|
||||
- **Risks & Issues** — Categorized as CRITICAL / IMPORTANT / MINOR
|
||||
- **Scoring Table** — Rate across: Feature Relevance, Code Quality, Security, Robustness, Tests
|
||||
- **Verdict** — Ready to merge? With mandatory vs optional fixes
|
||||
- **Next Steps** — What will happen if approved
|
||||
|
||||
### 5. Present to User
|
||||
|
||||
- Show the report via `notify_user` with `BlockedOnUser: true`
|
||||
- Wait for user decision:
|
||||
- **Approved** → Proceed to step 6
|
||||
- **Approved with changes** → Implement the fixes and corrections before merging
|
||||
- **Rejected** → Close the PR or leave a review comment
|
||||
|
||||
### 6. Implementation (if approved)
|
||||
|
||||
- Checkout the PR branch or apply changes locally
|
||||
- Implement any required fixes identified in the analysis
|
||||
- Run the project's test suite to verify nothing breaks
|
||||
// turbo
|
||||
- Run: `npm test` or equivalent test command
|
||||
- Build the project to verify compilation
|
||||
// turbo
|
||||
- Run: `npm run build` or equivalent build command
|
||||
- If all checks pass, prepare the merge
|
||||
|
||||
### 7. Post-Merge (if applicable)
|
||||
|
||||
- Update CHANGELOG.md with the new feature
|
||||
- Consider version bump if warranted
|
||||
- Follow the `/generate-release` workflow if a release is needed
|
||||
@@ -44,9 +44,6 @@ REQUIRE_API_KEY=false
|
||||
BASE_URL=http://localhost:20128
|
||||
CLOUD_URL=
|
||||
# Backward-compatible/public variables:
|
||||
# NEXT_PUBLIC_BASE_URL is also used as the OAuth redirect_uri origin when running behind a
|
||||
# reverse proxy (e.g., nginx). Set this to your public-facing URL so OAuth callbacks work.
|
||||
# Example: NEXT_PUBLIC_BASE_URL=https://omniroute.example.com
|
||||
NEXT_PUBLIC_BASE_URL=http://localhost:20128
|
||||
NEXT_PUBLIC_CLOUD_URL=
|
||||
|
||||
@@ -82,45 +79,17 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
|
||||
# Provider OAuth Credentials (optional — override hardcoded defaults)
|
||||
# These can also be set via data/provider-credentials.json
|
||||
# CLAUDE_OAUTH_CLIENT_ID=
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# ⚠️ GOOGLE OAUTH (Antigravity, Gemini CLI) — IMPORTANT FOR REMOTE SERVERS
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# The built-in Google OAuth credentials ONLY work when OmniRoute runs on
|
||||
# localhost (127.0.0.1 / local network). They are registered with
|
||||
# redirect_uri = http://localhost:PORT/callback and Google will reject any
|
||||
# other redirect URI with: redirect_uri_mismatch.
|
||||
#
|
||||
# If you are hosting OmniRoute on a remote server (VPS, Docker, cloud), you
|
||||
# MUST register your own Google Cloud OAuth 2.0 credentials:
|
||||
#
|
||||
# 1. Go to https://console.cloud.google.com/apis/credentials
|
||||
# 2. Create an OAuth 2.0 Client ID (type: "Web application")
|
||||
# 3. Add your server URL as Authorized redirect URI:
|
||||
# https://your-server.com/callback
|
||||
# 4. Copy the Client ID and Client Secret below.
|
||||
#
|
||||
# See the full tutorial in README.md → "OAuth em Servidor Remoto" section.
|
||||
#
|
||||
# Antigravity (Google Gemini Code Assist):
|
||||
# ANTIGRAVITY_OAUTH_CLIENT_ID=your-client-id.apps.googleusercontent.com
|
||||
# ANTIGRAVITY_OAUTH_CLIENT_SECRET=GOCSPX-your-secret
|
||||
ANTIGRAVITY_OAUTH_CLIENT_SECRET=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
|
||||
|
||||
# Gemini CLI (Google AI):
|
||||
# GEMINI_OAUTH_CLIENT_ID=your-client-id.apps.googleusercontent.com
|
||||
# GEMINI_OAUTH_CLIENT_SECRET=GOCSPX-your-secret
|
||||
# GEMINI_OAUTH_CLIENT_ID=
|
||||
# GEMINI_OAUTH_CLIENT_SECRET=
|
||||
# GEMINI_CLI_OAUTH_CLIENT_ID=
|
||||
GEMINI_OAUTH_CLIENT_SECRET=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
|
||||
GEMINI_CLI_OAUTH_CLIENT_SECRET=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
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# CLAUDE_OAUTH_CLIENT_ID=
|
||||
# GEMINI_CLI_OAUTH_CLIENT_SECRET=
|
||||
# CODEX_OAUTH_CLIENT_ID=
|
||||
# CODEX_OAUTH_CLIENT_SECRET=
|
||||
# QWEN_OAUTH_CLIENT_ID=
|
||||
# IFLOW_OAUTH_CLIENT_ID=
|
||||
IFLOW_OAUTH_CLIENT_SECRET=4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW
|
||||
# IFLOW_OAUTH_CLIENT_SECRET=
|
||||
# ANTIGRAVITY_OAUTH_CLIENT_ID=
|
||||
# ANTIGRAVITY_OAUTH_CLIENT_SECRET=
|
||||
|
||||
# API Key Providers (Phase 1 + Phase 4)
|
||||
# Add via Dashboard → Providers → Add API Key, or set here
|
||||
@@ -149,7 +118,3 @@ IFLOW_OAUTH_CLIENT_SECRET=4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW
|
||||
# Logging
|
||||
# LOG_LEVEL=info
|
||||
# LOG_FORMAT=text
|
||||
LOG_TO_FILE=true
|
||||
# LOG_FILE_PATH=logs/application/app.log
|
||||
# LOG_MAX_FILE_SIZE=50M
|
||||
# LOG_RETENTION_DAYS=7
|
||||
|
||||
@@ -36,8 +36,7 @@ jobs:
|
||||
- name: Dependency audit
|
||||
run: npm audit --audit-level=high --omit=dev
|
||||
- name: Check for known vulnerabilities
|
||||
run: npx is-my-node-vulnerable
|
||||
continue-on-error: true
|
||||
run: npx is-my-node-vulnerable || true
|
||||
|
||||
build:
|
||||
name: Build
|
||||
@@ -109,39 +108,3 @@ jobs:
|
||||
- run: npx playwright install --with-deps chromium
|
||||
- run: npm run build
|
||||
- run: npm run test:e2e
|
||||
|
||||
test-integration:
|
||||
name: Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
env:
|
||||
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||
INITIAL_PASSWORD: ci-test-password-for-integration
|
||||
DATA_DIR: /tmp/omniroute-ci
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run test:integration
|
||||
continue-on-error: true
|
||||
|
||||
test-security:
|
||||
name: Security Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
env:
|
||||
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run test:security
|
||||
continue-on-error: true
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
# name: Codex PR Review
|
||||
name: Codex PR Review
|
||||
|
||||
# on:
|
||||
# pull_request:
|
||||
# types: [opened, synchronize]
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
# jobs:
|
||||
# request-codex-review:
|
||||
# runs-on: ubuntu-latest
|
||||
# permissions:
|
||||
# pull-requests: write
|
||||
# steps:
|
||||
# - name: Request Codex Review
|
||||
# uses: actions/github-script@v8
|
||||
# with:
|
||||
# script: |
|
||||
# await github.rest.issues.createComment({
|
||||
# owner: context.repo.owner,
|
||||
# repo: context.repo.repo,
|
||||
# issue_number: context.payload.pull_request.number,
|
||||
# body: '@codex review'
|
||||
# });
|
||||
jobs:
|
||||
request-codex-review:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Request Codex Review
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
body: '@codex review'
|
||||
});
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
name: Deploy to VPS
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Publish to Docker Hub"]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
|
||||
name: Deploy OmniRoute to VPS
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Tailscale
|
||||
uses: tailscale/github-action@v3
|
||||
with:
|
||||
oauth-client-id: ""
|
||||
oauth-secret: ""
|
||||
tags: tag:ci-deploy
|
||||
continue-on-error: true
|
||||
|
||||
- name: Deploy via SSH
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.VPS_HOST }}
|
||||
username: ${{ secrets.VPS_USER }}
|
||||
key: ${{ secrets.VPS_SSH_KEY }}
|
||||
port: 22
|
||||
timeout: 30s
|
||||
script: |
|
||||
echo "=== Updating OmniRoute ==="
|
||||
npm install -g omniroute@latest 2>&1
|
||||
INSTALLED_VERSION=$(omniroute --version 2>/dev/null || echo "unknown")
|
||||
echo "Installed version: $INSTALLED_VERSION"
|
||||
|
||||
echo "=== Restarting PM2 ==="
|
||||
pm2 restart omniroute || pm2 start omniroute --name omniroute -- --port 20128
|
||||
pm2 save
|
||||
|
||||
echo "=== Health Check ==="
|
||||
sleep 3
|
||||
curl -sf http://localhost:20128/api/settings > /dev/null && echo "✅ OmniRoute is healthy" || echo "❌ Health check failed"
|
||||
echo "=== Deploy complete ==="
|
||||
@@ -8,65 +8,9 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build (${{ matrix.platform }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
platform_pair: linux-amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
platform_pair: linux-arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
env:
|
||||
IMAGE_NAME: diegosouzapw/omniroute
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
target: runner-base
|
||||
platforms: ${{ matrix.platform }}
|
||||
outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=gha,scope=omniroute-runner-base-${{ matrix.platform_pair }}
|
||||
cache-to: type=gha,mode=max,scope=omniroute-runner-base-${{ matrix.platform_pair }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p "${{ runner.temp }}/digests"
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ matrix.platform_pair }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
name: Merge manifest and publish tags
|
||||
docker:
|
||||
name: Build & Push Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
env:
|
||||
IMAGE_NAME: diegosouzapw/omniroute
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -79,12 +23,8 @@ jobs:
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Publishing Docker image version: $VERSION"
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
@@ -92,20 +32,18 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: ${{ runner.temp }}/digests
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
-t "${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}" \
|
||||
-t "${{ env.IMAGE_NAME }}:latest" \
|
||||
$(printf '${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect "${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}"
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
target: runner-base
|
||||
push: true
|
||||
tags: |
|
||||
diegosouzapw/omniroute:${{ steps.version.outputs.version }}
|
||||
diegosouzapw/omniroute:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Update Docker Hub description
|
||||
uses: peter-evans/dockerhub-description@v5
|
||||
|
||||
@@ -71,9 +71,6 @@ docs/*
|
||||
!docs/planning/
|
||||
!docs/improvement-plans/
|
||||
!docs/api/
|
||||
!docs/VM_DEPLOYMENT_GUIDE.md
|
||||
!docs/FEATURES.md
|
||||
!docs/screenshots/
|
||||
|
||||
# open-sse tests
|
||||
open-sse/test/*
|
||||
@@ -91,9 +88,3 @@ omnirouteSite/
|
||||
|
||||
# Security Analysis (standalone project with own git)
|
||||
security-analysis/
|
||||
|
||||
# Deploy workflow (contains sensitive VPS credentials)
|
||||
.agent/workflows/deploy.md
|
||||
clipr/
|
||||
app.log
|
||||
*.tgz
|
||||
|
||||
@@ -7,673 +7,242 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
---
|
||||
|
||||
## [1.4.9] — 2026-02-25
|
||||
## [0.9.0] — 2026-02-17
|
||||
|
||||
> ### 🌐 Full Internationalization (i18n) + Multi-Account Fix
|
||||
### Added
|
||||
|
||||
- 🌐 **Full multilingual README support** — Complete translations of README.md into 7 languages: Portuguese (pt-BR), Spanish (es), Russian (ru), Chinese Simplified (zh-CN), German (de), French (fr), Italian (it)
|
||||
- 🤖 **Agent showcase grid** — Modernized header with visual grid showcasing 10 AI coding agents (OpenClaw, NanoBot, PicoClaw, ZeroClaw, IronClaw, OpenCode, Codex CLI, Claude Code, Gemini CLI, Kilo Code)
|
||||
- 🖼️ **Provider logos** — Added logo assets for OpenClaw, NanoBot, PicoClaw, ZeroClaw, IronClaw, OpenCode
|
||||
- 🔌 **OpenAI-compatible provider validation fix** — Fallback validation via chat completions for providers that don't expose `/models` endpoint
|
||||
- 🛡️ **Red shield badges** — Replaced plain ❌ emoji with styled badge icons across all README files
|
||||
|
||||
---
|
||||
|
||||
## [0.8.8] — 2026-02-17
|
||||
|
||||
### Added
|
||||
|
||||
- 📊 **Analytics page redesign** — Rebuilt analytics dashboard with Recharts (SVG-based) charts replacing the previous implementation. New layout: stat cards → model usage bar chart → provider breakdown table with success rates and avg latency
|
||||
- 🎯 **6 global routing strategies** — Expanded from 3 (Fill-First, Round-Robin, P2C) to 6, adding Random, Least-Used, and Cost-Optimized. All strategies now have descriptions and icons in the Settings → Routing tab
|
||||
- 🔧 **Editable rate limits** — Rate limit defaults (RPM, Min Gap, Max Concurrent) are now editable in Settings → Resilience with save/cancel functionality. Values persist via the resilience API
|
||||
- 📋 **Policies in Resilience tab** — Moved PoliciesPanel (circuit breaker status + locked identifiers) from Security to Resilience tab for better logical grouping
|
||||
- 🧠 **Prompt Cache in AI tab** — Relocated CacheStatsCard from Advanced to AI tab alongside Thinking Budget and System Prompt
|
||||
|
||||
### Changed
|
||||
|
||||
- ♻️ **Settings page restructure** — Reorganized all settings tabs for better UX:
|
||||
- **Security**: Simplified to Login/Password and IP Access Control only
|
||||
- **Routing**: Expanded strategy grid with all 6 routing strategies
|
||||
- **Resilience**: Reordered cards (Provider Profiles → Rate Limiting → Circuit Breakers → Policies & Locked Identifiers)
|
||||
- **AI**: Now includes Thinking Budget, System Prompt, and Prompt Cache
|
||||
- **Advanced**: Simplified to only Global Proxy configuration
|
||||
- 🔄 **Backend routing strategies** — Implemented `random` (Fisher-Yates shuffle), `least-used` (sorted by `lastUsedAt`), `cost-optimized` (sorted by priority ascending), and fixed `p2c` (power-of-two-choices with health scoring) in `auth.ts`
|
||||
- 🔌 **Resilience API updates** — GET endpoint now merges saved rate limit defaults with constants; PATCH endpoint accepts both `profiles` and `defaults`
|
||||
- 📊 **Usage page split** — Refactored Usage page into "Request Logs" (with updated icon) and a new dedicated "Limits & Quotas" page
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛 **Provider add error** — Improved error handling for API responses when adding new provider connections, with clear validation feedback
|
||||
|
||||
---
|
||||
|
||||
## [0.8.5] — 2026-02-17
|
||||
|
||||
> ### 🔷 MILESTONE: 100% TypeScript Migration
|
||||
>
|
||||
> Complete dashboard i18n migration with next-intl, supporting English and Portuguese (Brazil). Fixes production build issues and enables multiple Codex accounts from the same workspace.
|
||||
> **OmniRoute is now fully TypeScript.** The entire `src/` directory (API routes, components, services, lib, domain layer) and all 94 files in `open-sse/` have been migrated from JavaScript/JSX to TypeScript/TSX — with zero `@ts-ignore` annotations and zero TypeScript errors. This is a complete rewrite of the type layer across 200+ files.
|
||||
|
||||
### ✨ New Features
|
||||
### Added
|
||||
|
||||
- **Full Dashboard Internationalization** — Complete i18n migration of 21+ pages and components using `next-intl`. Every dashboard string is now translatable with full EN and PT-BR support. Includes language selector (globe icon) in the header for real-time language switching
|
||||
- **Portuguese (Brazil) Translation** — Complete `pt-BR.json` translation file with 500+ keys covering all pages: Home, Providers, Settings, Combos, Analytics, Costs, Logs, Health, CLI Tools, Endpoint, API Manager, and Onboarding
|
||||
- **Language Selector Component** — New `LanguageSelector` component in the header with flag icons and dropdown for switching between 🇺🇸 English and 🇧🇷 Português
|
||||
- 🔒 **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)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
### Refactored
|
||||
|
||||
- **Multiple Codex Accounts from Same Workspace** — Fixed deduplication logic in `createProviderConnection` that prevented adding multiple OpenAI Pro Business accounts from the same Team workspace. Now uses compound check (workspaceId + email) instead of workspaceId-only, allowing separate connections per user
|
||||
- **Production Build — Crypto Import** — Fixed `instrumentation.ts` using `eval('require')('crypto')` to bypass webpack's static analysis that blocked the Node.js crypto module in the bundled instrumentation file
|
||||
- **Production Build — Translation Scope** — Fixed sub-components `ProviderOverviewCard` and `ProviderModelsModal` in `HomePageClient.tsx` that referenced parent-scope translation hooks. Each sub-component now has its own `useTranslations()` call
|
||||
- **Production Build — app/ Directory Conflict** — Resolved Next.js 16 confusing the production `app/` directory (server build output) with the `src/app/` app router directory, which caused "missing root layout" build failures
|
||||
- 🔷 **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
|
||||
|
||||
### 📄 i18n Pages Migrated
|
||||
### Fixed
|
||||
|
||||
Home, Endpoint, API Manager, Providers (list + detail + new), Combos, Logs, Costs, Analytics, Health, CLI Tools, Settings (General, Security, Routing, Session, IP Filter, Compliance, Fallback Chains, Thinking Budget, Policies, Pricing, Resilience, Advanced), Onboarding Wizard, Audit Log, Usage
|
||||
- 🐛 **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`
|
||||
|
||||
---
|
||||
|
||||
## [1.4.7] — 2026-02-25
|
||||
## [0.8.0] — 2026-02-16
|
||||
|
||||
> ### 🐛 Bugfix — Antigravity Model Prefix & Version Sync
|
||||
>
|
||||
> Fixes model name sent to Antigravity upstream API containing `antigravity/` prefix, causing 400 errors for non-opus models. Also syncs package-lock.json version.
|
||||
### Added
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
- 🌐 **Official website** — [omniroute.online](https://omniroute.online) live with static site on Akamai VM + Cloudflare proxy
|
||||
- 🛡️ **Comprehensive SECURITY.md** — Full codebase audit documenting 10+ security features (AES-256-GCM, prompt injection guard, PII redaction, circuit breaker, etc.)
|
||||
- 📖 **Documentation tracking** — `USER_GUIDE.md`, `API_REFERENCE.md`, `TROUBLESHOOTING.md` now tracked in git
|
||||
- 🏷️ **Website badge** — Official website badge and links in README, npm, and Docker Hub
|
||||
- 🔗 **36+ providers** — Updated provider count across documentation
|
||||
|
||||
- **Antigravity Model Prefix Stripping** — Model names sent to the Antigravity upstream API (Google Cloud Code) now have any `provider/` prefix defensively stripped. Previously, models like `antigravity/gemini-3-flash` were sent with the prefix intact, causing 400 errors from the upstream API. Only `claude-opus-4-6-thinking` worked because its routing path differed. Fix applied in 3 locations: `antigravity.ts` executor, and both `wrapInCloudCodeEnvelope` and `wrapInCloudCodeEnvelopeForClaude` in the translator
|
||||
- **Package-lock.json Version Sync** — Fixed `package-lock.json` being stuck at `1.4.3` while `package.json` was at `1.4.6`, which prevented npm from publishing the correct version and caused the VPS deploy to stay on the old version
|
||||
### Changed
|
||||
|
||||
- 📦 **npm homepage** — Points to `omniroute.online` instead of GitHub
|
||||
- 🐳 **Docker OCI labels** — Added `org.opencontainers.image.url` for Docker Hub
|
||||
- 🔒 **Security policy** — Updated supported versions, replaced email with GitHub Security Advisories
|
||||
|
||||
---
|
||||
|
||||
## [1.4.6] — 2026-02-25
|
||||
## [0.7.0] — 2026-02-16
|
||||
|
||||
> ### ✨ Community Release — Security Fix, Multi-Platform Docker, Model Updates & Plus Tier
|
||||
>
|
||||
> Enforces API key model restrictions across all endpoints, adds ARM64 Docker support, updates model registry for latest AI models, and introduces Plus tier in ProviderLimits.
|
||||
### Added
|
||||
|
||||
### 🔒 Security
|
||||
|
||||
- **API Key Model Restrictions Enforced** — `isModelAllowedForKey()` was never called, allowing API keys with `allowedModels` restrictions to access any model. Created centralized `enforceApiKeyPolicy()` middleware and wired it into all `/v1/*` endpoints (chat, embeddings, images, audio, moderations, rerank). Supports exact match, prefix match (`openai/*`), and wildcard patterns ([#130](https://github.com/diegosouzapw/OmniRoute/issues/130), [PR #131](https://github.com/diegosouzapw/OmniRoute/pull/131) by [@ersintarhan](https://github.com/ersintarhan))
|
||||
- **ApiKeyMetadata Type Safety** — Replaced `any` types with proper `ApiKeyMetadata` interface in the policy middleware. Added error logging in catch blocks for metadata fetch and budget check failures
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **Docker Multi-Platform Builds** — Restructured Docker CI workflow to support both `linux/amd64` and `linux/arm64` using native runners and digest-based manifest merging. ARM64 users (Apple Silicon, AWS Graviton, Raspberry Pi) can now run OmniRoute natively ([PR #127](https://github.com/diegosouzapw/OmniRoute/pull/127) by [@npmSteven](https://github.com/npmSteven))
|
||||
- **Plus Tier in ProviderLimits** — Added "Plus" as a separate category in the ProviderLimits dashboard, distinguishing Plus/Paid plans from Pro plans with proper ranking and filtering ([PR #126](https://github.com/diegosouzapw/OmniRoute/pull/126) by [@nyatoru](https://github.com/nyatoru))
|
||||
|
||||
### 🔧 Improvements
|
||||
|
||||
- **Model Registry Updates** — Updated provider registry, usage tracking, CLI tools config, and pricing for latest AI models: added Claude Sonnet 4.6, Gemini 3.1 Pro (High/Low), GPT OSS 120B Medium; removed deprecated Claude 4.5 variants and Gemini 2.5 Flash ([PR #128](https://github.com/diegosouzapw/OmniRoute/pull/128) by [@nyatoru](https://github.com/nyatoru))
|
||||
- **Model ID Consistency** — Fixed `claude-sonnet-4-6-thinking` → `claude-sonnet-4-6` mismatch in `importantModels` to match the provider registry
|
||||
- 🐳 **Docker Hub public image** — `diegosouzapw/omniroute` available on [Docker Hub](https://hub.docker.com/r/diegosouzapw/omniroute) with `latest` and versioned tags
|
||||
- 🔄 **Docker CI/CD** — GitHub Actions workflow (`docker-publish.yml`) auto-builds and pushes Docker image on every release
|
||||
- ☁️ **Akamai VM deployment** — Nanode 1GB instance created for remote hosting
|
||||
- 🎯 **Provider model filtering** — Filter model suggestions by selected provider in Translator and Chat Tester
|
||||
- 🔌 **CLI status badges** — Extract `CliStatusBadge` component; status visible on collapsed tool cards
|
||||
- ☁️ **Cloud connection UX** — GET status endpoint, toast feedback, and sidebar indicator for cloud sync
|
||||
- 🔐 **OAuth provider secrets** — Default cloud URL and OAuth provider secrets set via environment variables
|
||||
- ⚡ **Edge compatibility** — Replace `uuid` package with native `crypto.randomUUID()` for Cloudflare Workers compatibility
|
||||
|
||||
---
|
||||
|
||||
## [1.4.5] — 2026-02-24
|
||||
## [0.6.0] — 2026-02-16
|
||||
|
||||
> ### 🐛 Bugfix Release — Claude Code OAuth & OAuth Proxy Routing
|
||||
>
|
||||
> Fixes Claude Code OAuth failures on remote deployments and routes all OAuth token exchanges through configured proxy.
|
||||
### Added
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **Claude Code OAuth** — Fixed `400 Bad Request` on remote deployments by using Anthropic's registered `redirect_uri` (`https://platform.claude.com/oauth/code/callback`) instead of the dynamic server URL. Added missing OAuth scopes (`user:sessions:claude_code`, `user:mcp_servers`) to match the official Claude CLI. Configurable via `CLAUDE_CODE_REDIRECT_URI` env var ([#124](https://github.com/diegosouzapw/OmniRoute/issues/124))
|
||||
- **OAuth Token Exchange Through Proxy** — OAuth token exchange during new connection setup now routes through the configured proxy (provider-level → global → direct), fixing `unsupported_country_region_territory` errors for region-restricted providers like OpenAI Codex ([#119](https://github.com/diegosouzapw/OmniRoute/issues/119))
|
||||
- 💰 **Costs & Budget page** — Dedicated dashboard page for cost tracking and budget management
|
||||
- 📊 **Provider metrics display** — Show per-provider usage metrics and statistics
|
||||
- 📥 **Model import for passthrough providers** — Import models from API-compatible providers (Deepgram, AssemblyAI, NanoBanana)
|
||||
- 🎨 **App icon redesign** — New network node graph icon with updated color scheme
|
||||
|
||||
---
|
||||
|
||||
## [1.4.4] — 2026-02-24
|
||||
## [0.5.0] — 2026-02-15
|
||||
|
||||
> ### ✨ Feature Release — Custom Provider Models in /v1/models
|
||||
>
|
||||
> Compatible provider models are now saved to the customModels database, making them visible via `/v1/models` for all OpenAI-compatible clients.
|
||||
### Added
|
||||
|
||||
### ✨ New Features
|
||||
- 🧪 **LLM Evaluations (Evals)** — Golden set testing framework with 4 match strategies (`exact`, `contains`, `regex`, `custom`)
|
||||
- 🎲 **Advanced combo strategies** — `random`, `least-used`, and `cost-optimized` balancing strategies for combos
|
||||
- 📊 **API key usage in Evals** — Evals tab uses API key auth for real LLM calls through the proxy
|
||||
- 🏷️ **Model availability badge** — Visual indicator for model availability per provider
|
||||
- 🎨 **Landing page retheme** — Updated landing page design with new aesthetic
|
||||
- 🧩 **Shared UI component library** — Refactored dashboard with reusable component library
|
||||
|
||||
- **Custom Provider Model Persistence** — Compatible provider models (manual or imported) are now saved to the `customModels` database so they appear in `/v1/models` listing for clients like Cursor, Cline, Antigravity, and Claude Code ([PR #122](https://github.com/diegosouzapw/OmniRoute/pull/122) by [@nyatoru](https://github.com/nyatoru))
|
||||
- **Provider Models API** — New `/api/provider-models` endpoint (GET/POST/DELETE) for managing custom model entries with full authentication via `isAuthenticated`
|
||||
- **Unified Model Deletion** — New `handleDeleteModel` removes models from both alias configuration and `customModels` database, preventing orphaned entries
|
||||
- **Provider Node Prefix Resolution** — `getModelInfo` refactored to use provider node prefixes for accurate custom provider model resolution
|
||||
### Fixed
|
||||
|
||||
### 🔒 Security
|
||||
|
||||
- **Authentication on Provider Models API** — All `/api/provider-models` endpoints require API key or JWT session authentication via shared `isAuthenticated` utility
|
||||
- **URL Parameter Injection Fix** — Applied `encodeURIComponent` to all user-controlled URL parameters (`providerStorageAlias`, `providerId`) to prevent query string injection attacks
|
||||
- **Shared Auth Utility** — Authentication logic extracted to `@/shared/utils/apiAuth.ts`, eliminating code duplication across `/api/models/alias` and `/api/provider-models`
|
||||
|
||||
### 🔧 Improvements
|
||||
|
||||
- **Toast Notifications** — Replaced blocking `alert()` calls with non-blocking `notify.error`/`notify.success` toast notifications matching the project's notification system
|
||||
- **Transactional Save** — Model persistence is now transactional: database save must succeed before alias creation, preventing inconsistent state
|
||||
- **Consistent Error Handling** — All model operations (add, import, delete) now provide user-facing error/success feedback via toast notifications
|
||||
- **ComboFormModal Matching** — Improved provider node matching by ID or prefix for combo model selection
|
||||
- 🐛 Fix `TypeError` in `chat/completions` `ensureInitialized` call
|
||||
|
||||
---
|
||||
|
||||
## [1.4.3] — 2026-02-23
|
||||
## [0.4.0] — 2026-02-15
|
||||
|
||||
### 🐛 Bug Fix
|
||||
### Added
|
||||
|
||||
- **OAuth LAN Access** — Fixed OAuth flow for remote/LAN IP access (`192.168.x.x`). Previously, LAN IPs incorrectly used popup mode, leading to a broken redirect loop. Now defaults to manual callback URL input mode for non-localhost access
|
||||
- 🧠 **LLM Gateway Intelligence** (Phase 9) — Smart routing, semantic caching, request idempotency, progress tracking
|
||||
- 📄 **Missing flows & pages** (Phase 8) — Error pages, UX components, telemetry dashboards
|
||||
- 🔧 **API & code quality** (Phase 7) — API restructuring, JSDoc documentation, code quality improvements
|
||||
- 📚 **Documentation restructuring** (Phase 10) — Component decomposition, docs cleanup
|
||||
- ✅ **26 action items** from critical analysis resolved
|
||||
|
||||
### Changed
|
||||
|
||||
- ♻️ **Architecture refactor** (Phase 5-6) — Domain persistence, policy engine, OAuth extraction, proxy decoupling
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛 Fix CI build and lint failures
|
||||
- 🐛 Fix ghost import in `chatHelpers.js` SSE handling
|
||||
|
||||
---
|
||||
|
||||
## [1.4.2] — 2026-02-23
|
||||
## [0.3.0] — 2026-02-15
|
||||
|
||||
### 🐛 Bug Fix
|
||||
### Added
|
||||
|
||||
- **OAuth Token Refresh** — Fixed `client_secret is missing` error for Google-based OAuth providers (Antigravity, Gemini, Gemini CLI, iFlow). Desktop/CLI OAuth secrets are now hardcoded as defaults since Next.js inlined empty strings at build time.
|
||||
- ⚡ **Resilience system** — Exponential backoff, circuit breaker, anti-thundering herd mutex, Resilience UI settings page
|
||||
- 🖥️ **100% frontend API coverage** — 7 implementation batches covering all backend routes
|
||||
- 📊 **9 new API routes** — Budget, telemetry, compliance, tags, storage health, and more
|
||||
- 🧪 **Eval framework & compliance** — ADRs, accessibility, CLI specs, Playwright test specs (46 tasks)
|
||||
- 🏗️ **Pipeline integration** — 7 backend modules wired into request processing pipeline
|
||||
- 🔐 **Security hardening** — Phases 01–06 (input validation, CSRF, rate limiting, auth hardening)
|
||||
- 🤖 **Advanced features** — Phases 07–09 (domain extraction, error codes, request ID, fetch timeout)
|
||||
- 🔄 **Unrecoverable token handling** — Detect and mark connections as expired on fatal refresh errors
|
||||
|
||||
### Changed
|
||||
|
||||
- ♻️ Decompose `usageDb`, `handleSingleModelChat`, and UI components for maintainability
|
||||
- ⬇️ Downgrade ESLint v10 → v9 for `eslint-config-next` compatibility
|
||||
|
||||
---
|
||||
|
||||
## [1.4.1] — 2026-02-23
|
||||
## [0.2.0] — 2026-02-14
|
||||
|
||||
### 🔧 Improvements
|
||||
### Added
|
||||
|
||||
- **Endpoint Page Cleanup** — Removed redundant API Key Management section from Endpoint page (now fully managed in the dedicated API Manager page)
|
||||
- **CI/CD** — Added `deploy-vps.yml` workflow for automatic VPS deployment on new releases
|
||||
- 🛣️ **Advanced routing services** — Priority-based routing, global strategy configuration
|
||||
- 💰 **Cost analytics dashboard** — Token cost tracking and analytics visualization
|
||||
- 💎 **Pricing overhaul** — Comprehensive pricing data for all supported providers and models
|
||||
- 📦 **npm badge & CLI options** — npm version badge in README, CLI options table, automated release docs
|
||||
|
||||
---
|
||||
|
||||
## [1.4.0] — 2026-02-23
|
||||
## [0.1.0] — 2026-02-14
|
||||
|
||||
> ### ✨ Feature Release — Dedicated API Key Manager with Model Permissions
|
||||
>
|
||||
> Community-contributed API Key Manager page with model-level access control, enhanced with usage statistics, key status indicators, and improved UX.
|
||||
### Added
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **Dedicated API Key Manager** — New `/dashboard/api-manager` page for managing API keys, extracted from the Endpoint page. Includes create, delete, and permissions management with a clean table UI ([PR #118](https://github.com/diegosouzapw/OmniRoute/pull/118) by [@nyatoru](https://github.com/nyatoru))
|
||||
- **Model-Level API Key Permissions** — Restrict API keys to specific models using `allowed_models` with wildcard pattern support (e.g., `openai/*`). Toggle between "Allow All" and "Restrict" modes with an intuitive provider-grouped model selector
|
||||
- **API Key Validation Cache** — 3-tier caching layer (validation, metadata, permission) reduces database hits on every request, with automatic cache invalidation on key changes
|
||||
- **Usage Statistics Per Key** — Each API key shows total request count and last used timestamp, with a stats summary dashboard (total keys, restricted keys, total requests, models available)
|
||||
- **Key Status Indicators** — Color-coded lock/unlock icons and copy buttons on each key row for quick identification of restricted vs unrestricted keys
|
||||
|
||||
### 🔧 Improvements
|
||||
|
||||
- **Endpoint Page Simplified** — API key management removed from Endpoint page and replaced with a prominent link to the API Manager
|
||||
- **Sidebar Navigation** — New "API Manager" entry with `vpn_key` icon in the sidebar
|
||||
- **Prepared Statements** — API key database operations now use cached prepared statements for better performance
|
||||
- **Input Validation** — XSS-safe sanitization and regex validation for key names; ID format validation for API calls
|
||||
- 🎉 **Initial OmniRoute release** — Rebranded from 9router with full feature set
|
||||
- 🔄 **28 AI providers** — OpenAI, Claude, Gemini, Copilot, DeepSeek, Groq, xAI, Mistral, Qwen, iFlow, and more
|
||||
- 🎯 **Smart fallback** — 3-tier auto-routing (Subscription → Cheap → Free)
|
||||
- 🔀 **Format translation** — Seamless OpenAI ↔ Claude ↔ Gemini format conversion
|
||||
- 👥 **Multi-account support** — Multiple accounts per provider with round-robin
|
||||
- 🔐 **OAuth 2.0 (PKCE)** — Automatic token management and refresh
|
||||
- 📊 **Usage tracking** — Real-time quota monitoring with reset countdown
|
||||
- 🎨 **Custom combos** — Create model combinations with fallback chains
|
||||
- ☁️ **Cloud sync** — Sync configuration across devices via Cloudflare Worker
|
||||
- 📖 **OpenAPI specification** — Full API documentation
|
||||
- 🛡️ **SOCKS5 proxy support** — Outbound proxy for upstream provider calls
|
||||
- 🔌 **New endpoints** — `/v1/rerank`, `/v1/audio/*`, `/v1/moderations`
|
||||
- 📦 **npm CLI package** — `npm install -g omniroute` with auto-launch
|
||||
- 🐳 **Docker support** — Multi-stage Dockerfile with `base` and `cli` profiles
|
||||
- 🔒 **Security policy** — `SECURITY.md` with vulnerability reporting guidelines
|
||||
- 🧪 **CI/CD pipeline** — GitHub Actions for lint, build, test, and npm publish
|
||||
|
||||
---
|
||||
|
||||
## [1.3.1] — 2026-02-23
|
||||
|
||||
> ### 🐛 Bugfix Release — Proxy Connection Tests & Compatible Provider Display
|
||||
>
|
||||
> Fixes provider connection tests bypassing configured proxy and improves compatible provider display in the request logger.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **Connection Tests Now Use Proxy** — Provider connection tests (`Test Connection` button) now route through the configured proxy (key → combo → provider → global → direct), matching the behavior of real API calls. Previously, `fetch()` was called directly, bypassing the proxy entirely ([#119](https://github.com/diegosouzapw/OmniRoute/issues/119))
|
||||
- **Compatible Provider Display in Logs** — OpenAI/Anthropic compatible providers now show friendly labels (`OAI-COMPAT`, `ANT-COMPAT`) instead of raw UUID-based IDs in the request logger's provider column, dropdown, and quick filters ([#113](https://github.com/diegosouzapw/OmniRoute/issues/113))
|
||||
|
||||
### 🧪 Tests
|
||||
|
||||
- **Connection Test Unit Tests** — 26 new test cases covering error classification logic, token expiry detection, and provider display label resolution
|
||||
|
||||
---
|
||||
|
||||
## [1.3.0] — 2026-02-23
|
||||
|
||||
> ### ✨ Feature Release — iFlow Fix, Health Check Logs Toggle, Kilocode Models & Model Deduplication
|
||||
>
|
||||
> Community-driven release with iFlow HMAC-SHA256 signature support, health check log management, expanded Kilocode model list, and model deduplication on the dashboard.
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **Hide Health Check Logs** — New toggle in Settings → Appearance to suppress verbose `[HealthCheck]` messages from the server console. Uses a 30-second cache to minimize database reads with request coalescing for concurrent calls ([PR #111](https://github.com/diegosouzapw/OmniRoute/pull/111) by [@nyatoru](https://github.com/nyatoru))
|
||||
- **Kilocode Custom Models Endpoint** — Added `modelsUrl` support in `RegistryEntry` for providers with non-standard model endpoints. Expanded Kilocode model list from 8 to 26 models including Qwen3, GPT-5, Claude 3 Haiku, Gemini 2.5, DeepSeek V3, Llama 4, and more ([PR #115](https://github.com/diegosouzapw/OmniRoute/pull/115) by [@benzntech](https://github.com/benzntech))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **iFlow 406 Error** — Created dedicated `IFlowExecutor` with HMAC-SHA256 signature support (`session-id`, `x-iflow-timestamp`, `x-iflow-signature` headers). The iFlow provider was previously using the default executor which lacked the required signature headers, causing 406 errors ([#114](https://github.com/diegosouzapw/OmniRoute/issues/114))
|
||||
- **Duplicate Models in Endpoint Lists** — Filtered out parent models (`!m.parent`) from all model categorization and count logic on the Endpoint page. Provider modal lists also exclude duplicates ([PR #112](https://github.com/diegosouzapw/OmniRoute/pull/112) by [@nyatoru](https://github.com/nyatoru))
|
||||
|
||||
### 🧪 Tests
|
||||
|
||||
- **IFlowExecutor Unit Tests** — 11 new test cases covering HMAC-SHA256 signature generation, header building, URL construction, body passthrough, and executor registry integration
|
||||
|
||||
---
|
||||
|
||||
## [1.2.0] — 2026-02-22
|
||||
|
||||
> ### ✨ Feature Release — Dashboard Session Auth for Models Endpoint
|
||||
>
|
||||
> Dashboard users can now access `/v1/models` via their existing session when API key auth is required.
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **JWT Session Auth Fallback** — When `requireAuthForModels` is enabled, the `/v1/models` endpoint now accepts both API key (Bearer token) for external clients **and** the dashboard JWT session cookie (`auth_token`), allowing logged-in dashboard users to view models without needing an explicit API key ([PR #110](https://github.com/diegosouzapw/OmniRoute/pull/110) by [@nyatoru](https://github.com/nyatoru))
|
||||
|
||||
### 🔧 Improvements
|
||||
|
||||
- **401 instead of 404** — Authentication failures on `/v1/models` now return `401 Unauthorized` with a structured JSON error body (OpenAI-compatible format) instead of a generic `404 Not Found`, improving debuggability for API clients
|
||||
- **Simplified auth logic** — Refactored the JWT cookie verification to reuse the same pattern as `apiAuth.ts`, removing redundant same-origin detection (~60 lines) since the `sameSite:lax` + `httpOnly` cookie flags already provide equivalent CSRF protection
|
||||
|
||||
---
|
||||
|
||||
## [1.1.1] — 2026-02-22
|
||||
|
||||
> ### 🐛 Bugfix Release — API Key Creation & Codex Team Plan Quotas
|
||||
>
|
||||
> Fixes API key creation crash when `API_KEY_SECRET` is not set and adds Code Review rate limit window to Codex quota display.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **API Key Creation** — Added deterministic fallback for `API_KEY_SECRET` to prevent `crypto.createHmac` crash when the environment variable is not configured. Keys created without the secret are insecure (warned at startup) but the application no longer crashes ([#108](https://github.com/diegosouzapw/OmniRoute/issues/108))
|
||||
- **Codex Code Review Quota** — Added parsing of the third rate limit window (`code_review_rate_limit`) from the ChatGPT usage API, supporting Plus/Pro/Team plan differences. The dashboard now displays all three quota bars: Session (5h), Weekly, and Code Review ([#106](https://github.com/diegosouzapw/OmniRoute/issues/106))
|
||||
|
||||
---
|
||||
|
||||
## [1.1.0] — 2026-02-21
|
||||
|
||||
> ### 🐛 Bugfix Release — OAuth Client Secret and Codex Business Quotas
|
||||
>
|
||||
> Fixes missing remote-server OAuth configurations and adds ChatGPT Business account quota monitoring.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **OAuth Client Secret** — Omitted explicitly empty `client_secret` parameters to resolve token exchange connection rejection on remote servers missing environment variables for Antigravity, Gemini and iFlow ([#103](https://github.com/diegosouzapw/OmniRoute/issues/103))
|
||||
- **Codex Business Quotas** — Automatically fetches the appropriate ChatGPT workspace to unlock the 5-hour Business usage limits directly inside the Quota tab and mapped `BIZ` string variant perfectly ([#101](https://github.com/diegosouzapw/OmniRoute/issues/101))
|
||||
|
||||
---
|
||||
|
||||
## [1.0.10] — 2026-02-21
|
||||
|
||||
> ### 🐛 Bugfix — Multi-Account Support for Qwen
|
||||
>
|
||||
> Solves the issue where adding a second Qwen account would overwrite the first one.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **OAuth Accounts** — Extracted user email from the `id_token` using JWT decoding for Qwen and similar providers, allowing multiple accounts of the same provider to be authenticated simultaneously instead of triggering the fallback overwrite logic ([#99](https://github.com/diegosouzapw/OmniRoute/issues/99))
|
||||
|
||||
---
|
||||
|
||||
## [1.0.9] — 2026-02-21
|
||||
|
||||
> ### 🐛 Hotfix — Settings Persistence
|
||||
>
|
||||
> Fixes blocked providers and API auth toggle not being saved after page reload.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **Settings Persistence** — Added `requireAuthForModels` (boolean) and `blockedProviders` (string array) to the Zod validation schema, which was silently stripping these fields during PATCH requests, preventing them from being saved to the database
|
||||
|
||||
---
|
||||
|
||||
## [1.0.8] — 2026-02-21
|
||||
|
||||
> ### 🔒 API Security & Windows Support
|
||||
>
|
||||
> Adds API Endpoint Protection for `/models`, Windows server startup fixes, and UI improvements.
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **API Endpoint Protection (`/models`)** — New Security Tab settings to optionally require an API key for the `/v1/models` endpoint (returns 404 when unauthorized) and to selectively block specific providers from appearing in the models list ([#100](https://github.com/diegosouzapw/OmniRoute/issues/100), [#96](https://github.com/diegosouzapw/OmniRoute/issues/96))
|
||||
- **Interactive Provider UI** — Blocked Providers setting features an interactive chip selector with visual badges for all available AI providers
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **Windows Server Startup** — Fixed `ERR_INVALID_FILE_URL_PATH` crash on Windows by safely wrapping `import.meta.url` resolution with a fallback to `process.cwd()` for globally installed npm packages ([#98](https://github.com/diegosouzapw/OmniRoute/issues/98))
|
||||
- **Combo buttons visibility** — Fixed layout overlap and tight spacing for the Quick Action buttons (Clone / Delete / Test) on the Combos page on narrower screens ([#95](https://github.com/diegosouzapw/OmniRoute/issues/95))
|
||||
|
||||
---
|
||||
|
||||
## [1.0.7] — 2026-02-20
|
||||
|
||||
> ### 🐛 Bugfix Release — OpenAI Compatibility, Custom Models & OAuth UX
|
||||
>
|
||||
> Fixes three community-reported issues: stream default now follows OpenAI spec, custom OpenAI-compatible providers appear in `/v1/models`, and Google OAuth shows a clear error + tutorial for remote deployments.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **`stream` defaults to `false`** — Aligns with the OpenAI specification which explicitly states `stream` defaults to `false`. Previously OmniRoute defaulted to `true`, causing SSE data to be returned instead of a JSON object, breaking clients like Spacebot, OpenCode, and standard Python/Rust/Go OpenAI SDKs that don't explicitly set `stream: true` ([#89](https://github.com/diegosouzapw/OmniRoute/issues/89))
|
||||
- **Custom AI providers now appear in `/v1/models`** — OpenAI-compatible custom providers (e.g. FriendLI) whose provider ID wasn't in the built-in alias map were silently excluded from the models list even when active. Fixed by also checking the raw provider ID from the database against active connections ([#90](https://github.com/diegosouzapw/OmniRoute/issues/90))
|
||||
- **OAuth `redirect_uri_mismatch` — improved UX for remote deployments** — Google OAuth providers (Antigravity, Gemini CLI) now always use `localhost` as redirect URI matching the registered credentials. Remote-access users see a targeted amber warning with a link to the new setup guide. The token exchange error message explains the root cause and guides users to configure their own credentials ([#91](https://github.com/diegosouzapw/OmniRoute/issues/91))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- **OAuth em Servidor Remoto tutorial** — New README section with step-by-step guide to configure custom Google Cloud OAuth 2.0 credentials for remote/VPS/Docker deployments
|
||||
- **`.env.example` Google OAuth block** — Added prominent warning block explaining remote credential requirements with direct links to Google Cloud Console
|
||||
|
||||
### 📁 Files Modified
|
||||
|
||||
| File | Change |
|
||||
| -------------------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| `open-sse/handlers/chatCore.ts` | `stream` defaults to `false` (was `true`) per OpenAI spec |
|
||||
| `src/app/api/v1/models/route.ts` | Added raw `providerId` check for custom models active-provider filter |
|
||||
| `src/shared/components/OAuthModal.tsx` | Force `localhost` redirect for Google OAuth; improved `redirect_uri_mismatch` error message |
|
||||
| `.env.example` | Added ⚠️ Google OAuth remote credentials block with step-by-step instructions |
|
||||
| `README.md` | New "🔐 OAuth em Servidor Remoto" tutorial section |
|
||||
|
||||
---
|
||||
|
||||
## [1.0.6] — 2026-02-20
|
||||
|
||||
> ### ✨ Provider & Combo Toggles — Strict Model Filtering
|
||||
>
|
||||
> `/v1/models` now shows only models from providers with active connections. Combos and providers can be toggled on/off directly from the dashboard.
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **Provider toggle on Providers page** — Enable/disable all connections for a provider directly from the main Providers list. Toggle is always visible, no hover needed
|
||||
- **Combo enable/disable toggle** — Each combo on the Combos page now has a toggle. Disabled combos are excluded from `/v1/models`
|
||||
- **OAuth private IP support** — Expanded localhost detection to include private/LAN IPs (`192.168.x.x`, `10.x.x.x`, `172.16-31.x.x`) for correct OAuth redirect URIs
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **`/v1/models` strict filtering** — Models are now shown only from providers with active, enabled connections. Previously, if no connections existed or all were disabled, all 378+ models were shown as a fallback
|
||||
- **Disabled provider models hidden** — Toggling off a provider immediately removes its models from `/v1/models`
|
||||
|
||||
---
|
||||
|
||||
## [1.0.5] — 2026-02-20
|
||||
|
||||
> ### 🐛 Hotfix — Model Filtering & Docker DATA_DIR
|
||||
>
|
||||
> Filters all model types in `/v1/models` by active providers and fixes Docker data directory mismatch.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **`/v1/models` full filtering** — Embedding, image, rerank, audio, and moderation models are now filtered by active provider connections, matching chat model behavior. Providers like Together AI no longer appear without a configured API key (#88)
|
||||
- **Docker `DATA_DIR`** — Added `ENV DATA_DIR=/app/data` to Dockerfile and `docker-compose.yml` ensuring the volume mount always matches the app data directory — prevents empty database on container recreation
|
||||
|
||||
---
|
||||
|
||||
## [1.0.4] — 2026-02-19
|
||||
|
||||
> ### 🔧 Provider Filtering, OAuth Proxy Fix & Documentation
|
||||
>
|
||||
> Dashboard model filtering by active providers, provider enable/disable visual indicators, OAuth login fix for nginx reverse proxy, and LLM onboarding documentation.
|
||||
|
||||
### ✨ Features
|
||||
|
||||
- **API Models filtering** — `GET /api/models` now returns only models from active providers; use `?all=true` for all models (#85)
|
||||
- **Provider disabled indicator** — Provider cards show ⏸ "Disabled" badge with reduced opacity when all connections are inactive (#85)
|
||||
- **`llm.txt`** — Comprehensive LLM onboarding file with project overview, architecture, flows, and conventions (#84)
|
||||
- **WhatsApp Community** — Added WhatsApp group link to README badges and Support section
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **OAuth behind nginx** — Fixed OAuth login failing when behind a reverse proxy by using `window.location.origin` for redirect URI instead of hardcoded `localhost` (#86)
|
||||
- **`NEXT_PUBLIC_BASE_URL` for OAuth** — Documented env var usage as redirect URI override for proxy deployments (#86)
|
||||
|
||||
### 📁 Files Added
|
||||
|
||||
| File | Purpose |
|
||||
| --------- | -------------------------------------------------- |
|
||||
| `llm.txt` | LLM and contributor onboarding (llms.txt standard) |
|
||||
|
||||
### 📁 Files Modified
|
||||
|
||||
| File | Change |
|
||||
| -------------------------------------------------- | ---------------------------------------------------------------- |
|
||||
| `src/app/api/models/route.ts` | Filter by active providers, `?all=true` param, `available` field |
|
||||
| `src/app/(dashboard)/dashboard/providers/page.tsx` | `allDisabled` detection + ⏸ badge + opacity-50 on provider cards |
|
||||
| `src/shared/components/OAuthModal.tsx` | Proxy-aware redirect URI using `window.location.origin` |
|
||||
| `.env.example` | Documented `NEXT_PUBLIC_BASE_URL` for OAuth behind proxy |
|
||||
|
||||
---
|
||||
|
||||
## [1.0.3] — 2026-02-19
|
||||
|
||||
> ### 📊 Logs Dashboard & Real-Time Console Viewer
|
||||
>
|
||||
> Unified logs interface with real-time console log viewer, file-based logging via console interception, and server initialization improvements.
|
||||
|
||||
### ✨ Features
|
||||
|
||||
- **Logs Dashboard** — Consolidated 4-tab page at `/dashboard/logs` with Request Logs, Proxy Logs, Audit Logs, and Console tabs
|
||||
- **Console Log Viewer** — Terminal-style real-time log viewer with color-coded log levels, auto-scroll, search/filtering, level filter, and 5-second polling
|
||||
- **Console Interceptor** — Monkey-patches `console.log/info/warn/error/debug` at server start to capture all application output as JSON lines to `logs/application/app.log`
|
||||
- **Log Rotation** — Size-based rotation and retention-based cleanup for log files
|
||||
|
||||
### 🔧 Improvements
|
||||
|
||||
- **Instrumentation consolidation** — Moved `initAuditLog()`, `cleanupExpiredLogs()`, and console interceptor initialization to Next.js `instrumentation.ts` (runs on both dev and prod server start)
|
||||
- **Structured Logger file output** — `structuredLogger.ts` now also appends JSON log entries to the log file
|
||||
- **Pino Logger fix** — Fixed broken mix of pino `transport` targets + manual `createWriteStream`; now uses `pino/file` transport targets exclusively with absolute paths
|
||||
|
||||
### 🗂️ Files Added
|
||||
|
||||
| File | Purpose |
|
||||
| ---------------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| `src/app/(dashboard)/dashboard/logs/page.tsx` | Tabbed Logs Dashboard page |
|
||||
| `src/app/(dashboard)/dashboard/logs/AuditLogTab.tsx` | Audit log tab component extracted from standalone page |
|
||||
| `src/shared/components/ConsoleLogViewer.tsx` | Terminal-style real-time log viewer |
|
||||
| `src/app/api/logs/console/route.ts` | API endpoint to read log file (filters last 1h, level, component) |
|
||||
| `src/lib/consoleInterceptor.ts` | Console method monkey-patching for file capture |
|
||||
| `src/lib/logRotation.ts` | Log rotation by size and cleanup by retention days |
|
||||
|
||||
### 🗂️ Files Modified
|
||||
|
||||
| File | Change |
|
||||
| --------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| `src/shared/components/Sidebar.tsx` | Nav: "Request Logs" → "Logs" with `description` icon |
|
||||
| `src/shared/components/Breadcrumbs.tsx` | Added breadcrumb labels for `logs`, `audit-log`, `console` |
|
||||
| `src/instrumentation.ts` | Added console interceptor + audit log init + expired log cleanup |
|
||||
| `src/server-init.ts` | Added console interceptor import (backup init) |
|
||||
| `src/shared/utils/logger.ts` | Fixed pino file transport using `pino/file` targets |
|
||||
| `src/shared/utils/structuredLogger.ts` | Added `appendFileSync` file writing + log file config |
|
||||
| `.env.example` | Added `LOG_TO_FILE`, `LOG_FILE_PATH`, `LOG_MAX_FILE_SIZE`, `LOG_RETENTION_DAYS` |
|
||||
|
||||
### ⚙️ Configuration
|
||||
|
||||
New environment variables:
|
||||
|
||||
| Variable | Default | Description |
|
||||
| -------------------- | -------------------------- | ----------------------------- |
|
||||
| `LOG_TO_FILE` | `true` | Enable/disable file logging |
|
||||
| `LOG_FILE_PATH` | `logs/application/app.log` | Log file path |
|
||||
| `LOG_MAX_FILE_SIZE` | `50M` | Max file size before rotation |
|
||||
| `LOG_RETENTION_DAYS` | `7` | Days to retain old log files |
|
||||
|
||||
---
|
||||
|
||||
## [1.0.2] — 2026-02-18
|
||||
|
||||
> ### 🔒 Security Hardening, Architecture Improvements & UX Polish
|
||||
>
|
||||
> Comprehensive audit-driven improvements across security, architecture, testing, and user experience.
|
||||
|
||||
### 🛡️ Security (Phase 0)
|
||||
|
||||
- **Auth guard** — API route protection via `withAuth` middleware for all dashboard routes
|
||||
- **CSRF protection** — Token-based CSRF guard for all state-changing API routes
|
||||
- **Request payload validation** — Zod schemas for provider, combo, key, and settings endpoints
|
||||
- **Prompt injection guard** — Input sanitization against malicious prompt patterns
|
||||
- **Body size guard** — Route-specific body size limits with dedicated audio upload threshold
|
||||
- **Rate limiter** — Per-IP rate limiting with configurable windows and thresholds
|
||||
|
||||
### 🏗️ Architecture (Phase 1–2)
|
||||
|
||||
- **DI container** — Simple dependency injection container for service registration
|
||||
- **Policy engine** — Consolidated `PolicyEngine` for routing, security, and rate limiting
|
||||
- **SQLite migration** — Database migration system with versioned migration runner
|
||||
- **Graceful shutdown** — Clean server shutdown with connection draining
|
||||
- **TypeScript fixes** — Resolved all `tsc` errors; removed redundant `@ts-check` directives
|
||||
- **Pipeline decomposition** — `handleSingleModelChat` decomposed into composable pipeline stages
|
||||
- **Prompt template versioning** — Version-tracked prompt templates with rollback support
|
||||
- **Eval scheduling** — Automated evaluation suite scheduling with cron-based runner
|
||||
- **Plugin architecture** — Extensible plugin system for custom middleware and handlers
|
||||
|
||||
### 🧪 Testing & CI (Phase 2)
|
||||
|
||||
- **Coverage thresholds** — Jest coverage thresholds enforced in CI (368 tests passing)
|
||||
- **Proxy pipeline integration tests** — End-to-end tests for the proxy request pipeline
|
||||
- **CI audit workflow** — npm audit and security scanning in GitHub Actions
|
||||
- **k6 load tests** — Performance testing with ramping VUs and custom metrics
|
||||
|
||||
### ✨ UX & Polish (Phase 3–4)
|
||||
|
||||
- **Session management** — Session info card with login time, age, user agent, and logout
|
||||
- **Focus indicators** — Global `:focus-visible` styles and `--focus-ring` CSS utility
|
||||
- **Audit log viewer** — Security event audit log with structured data display
|
||||
- **Dashboard cleanup** — Removed unused files, fixed Quick Start links to Endpoint page
|
||||
- **Documentation** — Troubleshooting guide, deployment improvements
|
||||
|
||||
---
|
||||
|
||||
## [1.0.1] — 2026-02-18
|
||||
|
||||
> ### 🔧 API Compatibility & SDK Hardening
|
||||
>
|
||||
> Response sanitization, role normalization, and structured output improvements for strict OpenAI SDK compatibility and cross-provider robustness.
|
||||
|
||||
### 🛡️ Response Sanitization (NEW)
|
||||
|
||||
- **Response sanitizer module** — New `responseSanitizer.ts` strips non-standard fields (`x_groq`, `usage_breakdown`, `service_tier`, etc.) from all OpenAI-format provider responses, fixing OpenAI Python SDK v1.83+ Pydantic validation failures that returned raw strings instead of parsed `ChatCompletion` objects
|
||||
- **Streaming chunk sanitization** — Passthrough streaming mode now sanitizes each SSE chunk in real-time via `sanitizeStreamingChunk()`, ensuring strict `chat.completion.chunk` schema compliance
|
||||
- **ID/Object/Usage normalization** — Ensures `id`, `object`, `created`, `model`, `choices`, and `usage` fields always exist with correct types
|
||||
- **Usage field cleanup** — Strips non-standard usage sub-fields, keeps only `prompt_tokens`, `completion_tokens`, `total_tokens`, and OpenAI detail fields
|
||||
|
||||
### 🧠 Think Tag Extraction (NEW)
|
||||
|
||||
- **`<think>` tag extraction** — Automatically extracts `<think>...</think>` blocks from thinking model responses (DeepSeek R1, Kimi K2 Thinking, etc.) into OpenAI's standard `reasoning_content` field
|
||||
- **Streaming think-tag stripping** — Real-time `<think>` extraction in passthrough SSE stream, preventing JSON parsing errors in downstream tools
|
||||
- **Preserves native reasoning** — Providers that already send `reasoning_content` natively (e.g., OpenAI o1) are not overwritten
|
||||
|
||||
### 🔄 Role Normalization (NEW)
|
||||
|
||||
- **`developer` → `system` conversion** — OpenAI's new `developer` role is automatically converted to `system` for all non-OpenAI providers (Claude, Gemini, Kiro, etc.)
|
||||
- **`system` → `user` merging** — For models that reject the `system` role (GLM, ERNIE), system messages are intelligently merged into the first user message with clear delimiters
|
||||
- **Model-aware normalization** — Uses model name prefix matching (`glm-*`, `ernie-*`) for compatibility decisions, avoiding hardcoded provider-level flags
|
||||
|
||||
### 📐 Structured Output for Gemini (NEW)
|
||||
|
||||
- **`response_format` → Gemini conversion** — OpenAI's `json_schema` structured output is now translated to Gemini's `responseMimeType` + `responseSchema` in the translator pipeline
|
||||
- **`json_object` support** — `response_format: { type: "json_object" }` maps to Gemini's `application/json` MIME type
|
||||
- **Schema cleanup** — Automatically removes unsupported JSON Schema keywords (`$schema`, `additionalProperties`) for Gemini compatibility
|
||||
|
||||
### 📁 Files Added
|
||||
|
||||
| File | Purpose |
|
||||
| ---------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| `open-sse/handlers/responseSanitizer.ts` | Response field stripping, think-tag extraction, ID/usage normalization |
|
||||
| `open-sse/services/roleNormalizer.ts` | Developer→system, system→user role conversion pipeline |
|
||||
|
||||
### 📁 Files Modified
|
||||
|
||||
| File | Change |
|
||||
| ------------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| `open-sse/handlers/chatCore.ts` | Integrated response sanitizer for non-streaming OpenAI responses |
|
||||
| `open-sse/utils/stream.ts` | Integrated streaming chunk sanitizer + think-tag extraction in passthrough mode |
|
||||
| `open-sse/translator/index.ts` | Integrated role normalizer into the request translation pipeline |
|
||||
| `open-sse/translator/request/openai-to-gemini.ts` | Added `response_format` → `responseMimeType`/`responseSchema` conversion |
|
||||
|
||||
---
|
||||
|
||||
## [1.0.0] — 2026-02-18
|
||||
|
||||
> ### 🎉 First Major Release — OmniRoute 1.0
|
||||
>
|
||||
> OmniRoute is an intelligent API gateway that unifies 20+ AI providers behind a single OpenAI-compatible endpoint. This release represents the culmination of the entire development effort — from initial prototype to production-ready platform.
|
||||
|
||||
### 🧠 Core Routing & Intelligence
|
||||
|
||||
- **Smart 4-tier fallback** — Auto-routing: Subscription → Cheap → Free → Emergency
|
||||
- **6 routing strategies** — Fill First, Round Robin, Power-of-Two-Choices, Random, Least Used, Cost Optimized
|
||||
- **Semantic caching** — Auto-cache responses for deduplication with configurable TTL
|
||||
- **Request idempotency** — Prevent duplicate processing of identical requests
|
||||
- **Thinking budget validation** — Control reasoning token allocation per request
|
||||
- **System prompt injection** — Configurable global system prompts for all requests
|
||||
|
||||
### 🔌 Providers & Models
|
||||
|
||||
- **20+ AI providers** — OpenAI, Claude (Anthropic), Gemini, GitHub Copilot, DeepSeek, Groq, xAI, Mistral, Qwen, iFlow, Kiro, OpenRouter, GLM, MiniMax, Kimi, NVIDIA NIM, and more
|
||||
- **Multi-account support** — Multiple accounts per provider with automatic rotation
|
||||
- **OAuth 2.0 (PKCE)** — Automatic token management and refresh for Claude Code, Codex, Gemini CLI, Copilot, Kiro
|
||||
- **Auto token refresh** — Background refresh with expiry detection and unrecoverable error handling
|
||||
- **Model import** — Import models from API-compatible passthrough providers
|
||||
- **OpenAI-compatible validation** — Fallback validation via chat completions for providers without `/models` endpoint
|
||||
- **TLS fingerprint spoofing** — Browser-like TLS fingerprinting via `wreq-js` to bypass bot detection
|
||||
|
||||
### 🔄 Format Translation
|
||||
|
||||
- **Multi-format translation** — Seamless OpenAI ↔ Claude ↔ Gemini ↔ OpenAI Responses API conversion
|
||||
- **Translator Playground** — 4 interactive modes:
|
||||
- **Playground** — Test format translations between any provider formats
|
||||
- **Chat Tester** — Send real requests through the proxy with visual response rendering
|
||||
- **Test Bench** — Automated batch testing across multiple providers
|
||||
- **Live Monitor** — Real-time stream of active proxy requests and translations
|
||||
|
||||
### 🎯 Combos & Fallback Chains
|
||||
|
||||
- **Custom combos** — Create model combinations with multi-provider fallback chains
|
||||
- **6 combo balancing strategies** — Fill First, Round Robin, Random, Least Used, P2C, Cost Optimized
|
||||
- **Combo circuit breaker** — Auto-disable failing providers within a combo chain
|
||||
|
||||
### 🛡️ Resilience & Security
|
||||
|
||||
- **Circuit breakers** — Auto-recovery with configurable thresholds and cooldown periods
|
||||
- **Exponential backoff** — Progressive retry delays for failed requests
|
||||
- **Anti-thundering herd** — Mutex-based protection against concurrent retry storms
|
||||
- **Rate limit detection** — Per-provider RPM, min gap, and max concurrent request tracking
|
||||
- **Editable rate limits** — Configurable defaults via Settings → Resilience with persistence
|
||||
- **Prompt injection guard** — Input sanitization for malicious prompt patterns
|
||||
- **PII redaction** — Automatic detection and masking of personally identifiable information
|
||||
- **AES-256-GCM encryption** — Credential encryption at rest
|
||||
- **IP access control** — Whitelist/blacklist IP filtering
|
||||
- **SOCKS5 proxy support** — Outbound proxy for upstream provider calls
|
||||
|
||||
### 📊 Observability & Analytics
|
||||
|
||||
- **Analytics dashboard** — Recharts-based SVG charts: stat cards, model usage bar chart, provider breakdown table with success rates and latency
|
||||
- **Real-time health monitoring** — Provider health, rate limits, latency telemetry
|
||||
- **Request logs** — Dedicated page with SQLite-persisted proxy request/response logs
|
||||
- **Limits & Quotas** — Separate dashboard for quota monitoring with reset countdowns
|
||||
- **Cost analytics** — Token cost tracking and budget management per provider
|
||||
- **Request telemetry** — Correlation IDs, structured logging, request timing
|
||||
|
||||
### 💾 Database & Backup
|
||||
|
||||
- **Dual database** — LowDB (JSON) for config + SQLite for domain state and proxy logs
|
||||
- **Export database** — `GET /api/db-backups/export` — Download SQLite database file
|
||||
- **Export all** — `GET /api/db-backups/exportAll` — Full backup as `.tar.gz` archive (DB + settings + combos + providers + masked API keys)
|
||||
- **Import database** — `POST /api/db-backups/import` — Upload and restore with validation, integrity check, and pre-import backup
|
||||
- **Automatic backups** — Configurable backup schedule with retention
|
||||
- **Storage health** — Dashboard widget with database size, path, and backup status
|
||||
|
||||
### 🖥️ Dashboard & UI
|
||||
|
||||
- **Full dashboard** — Provider management, analytics, health monitoring, settings, CLI tools
|
||||
- **9 dashboard sections** — Providers, Combos, Analytics, Health, Translator, Settings, CLI Tools, Usage, Endpoint
|
||||
- **Settings restructure** — 6 tabs: Security, Routing, Resilience, AI, System/Storage, Advanced
|
||||
- **Shared UI component library** — Reusable components (Avatar, Badge, Button, Card, DataTable, Modal, etc.)
|
||||
- **Dark/Light/System theme** — Persistent theme selection with system preference detection
|
||||
- **Agent showcase grid** — Visual grid of 10 AI coding agents in README header
|
||||
- **Provider logos** — Logo assets for all supported agents and providers
|
||||
- **Red shield badges** — Styled badge icons across all documentation
|
||||
|
||||
### ☁️ Deployment & Infrastructure
|
||||
|
||||
- **Docker support** — Multi-stage Dockerfile with `base` and `cli` profiles
|
||||
- **Docker Hub** — `diegosouzapw/omniroute` with `latest` and versioned tags
|
||||
- **Docker CI/CD** — GitHub Actions auto-build and push on release
|
||||
- **npm CLI package** — `npx omniroute` with auto-launch
|
||||
- **npm CI/CD** — GitHub Actions auto-publish to npm on release
|
||||
- **Akamai VM deployment** — Production deployment on Nanode 1GB with nginx reverse proxy
|
||||
- **Cloud sync** — Sync configuration across devices via Cloudflare Worker
|
||||
- **Edge compatibility** — Native `crypto.randomUUID()` for Cloudflare Workers
|
||||
|
||||
### 🧪 Testing & Quality
|
||||
|
||||
- **100% TypeScript** — Full migration of `src/` (200+ files) and `open-sse/` (94 files) — zero `@ts-ignore`, zero TypeScript errors
|
||||
- **CI/CD pipeline** — GitHub Actions for lint, build, test, npm publish, Docker publish
|
||||
- **Unit tests** — 20+ test suites covering domain logic, security, caching, routing
|
||||
- **E2E tests** — Playwright specs for API, navigation, and responsive behavior
|
||||
- **LLM evaluations** — Golden set testing framework with 4 match strategies (`exact`, `contains`, `regex`, `custom`)
|
||||
- **Security tests** — CLI runtime, Docker hardening, cloud sync, and OpenAI compatibility
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- **8 language READMEs** — English, Portuguese (pt-BR), Spanish, Russian, Chinese (zh-CN), German, French, Italian
|
||||
- **VM Deployment Guide** — Complete guide (VM + Docker + nginx + Cloudflare + security)
|
||||
- **Features Gallery** — 9 dashboard screenshots with descriptions
|
||||
- **API Reference** — Full endpoint documentation including backup/export/import
|
||||
- **User Guide** — Step-by-step setup, configuration, and usage instructions
|
||||
- **Architecture docs** — System design, component decomposition, ADRs
|
||||
- **OpenAPI specification** — Machine-readable API documentation
|
||||
- **Troubleshooting guide** — Common issues and solutions
|
||||
- **Security policy** — `SECURITY.md` with vulnerability reporting via GitHub Security Advisories
|
||||
- **Roadmap** — 150+ planned features across 6 categories
|
||||
|
||||
### 🔌 API Endpoints
|
||||
|
||||
- `/v1/chat/completions` — OpenAI-compatible chat endpoint with format translation
|
||||
- `/v1/embeddings` — Embedding generation
|
||||
- `/v1/images/generations` — Image generation
|
||||
- `/v1/models` — Model listing with provider filtering
|
||||
- `/v1/rerank` — Re-ranking endpoint
|
||||
- `/v1/audio/*` — Audio transcription and translation
|
||||
- `/v1/moderations` — Content moderation
|
||||
- `/api/db-backups/export` — Database export
|
||||
- `/api/db-backups/exportAll` — Full archive export
|
||||
- `/api/db-backups/import` — Database import with validation
|
||||
- 30+ dashboard API routes for providers, combos, settings, analytics, health, CLI tools
|
||||
|
||||
---
|
||||
|
||||
[1.4.7]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.4.7
|
||||
[1.4.6]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.4.6
|
||||
[1.4.5]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.4.5
|
||||
[1.4.4]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.4.4
|
||||
[1.4.3]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.4.3
|
||||
[1.4.2]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.4.2
|
||||
[1.4.1]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.4.1
|
||||
[1.4.0]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.4.0
|
||||
[1.3.1]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.3.1
|
||||
[1.3.0]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.3.0
|
||||
[1.2.0]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.2.0
|
||||
[1.1.1]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.1.1
|
||||
[1.0.7]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.0.7
|
||||
[1.0.6]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.0.6
|
||||
[1.0.5]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.0.5
|
||||
[1.0.4]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.0.4
|
||||
[1.1.0]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.1.0
|
||||
[1.0.1]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.0.1
|
||||
[1.0.3]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.0.3
|
||||
[1.0.2]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.0.2
|
||||
[1.0.0]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v1.0.0
|
||||
[0.9.0]: https://github.com/diegosouzapw/OmniRoute/compare/v0.8.8...v0.9.0
|
||||
[0.8.8]: https://github.com/diegosouzapw/OmniRoute/compare/v0.8.5...v0.8.8
|
||||
[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
|
||||
[0.5.0]: https://github.com/diegosouzapw/OmniRoute/compare/v0.4.0...v0.5.0
|
||||
[0.4.0]: https://github.com/diegosouzapw/OmniRoute/compare/v0.3.0...v0.4.0
|
||||
[0.3.0]: https://github.com/diegosouzapw/OmniRoute/compare/v0.2.0...v0.3.0
|
||||
[0.2.0]: https://github.com/diegosouzapw/OmniRoute/compare/v0.1.0...v0.2.0
|
||||
[0.1.0]: https://github.com/diegosouzapw/OmniRoute/releases/tag/v0.1.0
|
||||
|
||||
@@ -159,7 +159,7 @@ src/ # TypeScript (.ts / .tsx)
|
||||
│ ├── cacheLayer.ts # LRU cache
|
||||
│ ├── semanticCache.ts # Semantic response cache
|
||||
│ ├── idempotencyLayer.ts # Request deduplication
|
||||
│ └── localDb.ts # Settings facade (LowDB for config, SQLite for domain data)
|
||||
│ └── localDb.ts # LowDB (JSON) storage
|
||||
├── shared/
|
||||
│ ├── components/ # React components (.tsx)
|
||||
│ ├── middleware/ # Correlation IDs, etc.
|
||||
|
||||
@@ -20,8 +20,7 @@ ENV NODE_ENV=production
|
||||
ENV PORT=20128
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
|
||||
# Data directory inside Docker — must match the volume mount in docker-compose.yml
|
||||
ENV DATA_DIR=/app/data
|
||||
# Runtime writable location for localDb when DATA_DIR is configured to /app/data
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<img src="./docs/screenshots/MainOmniRoute.png" alt="OmniRoute Dashboard" width="800"/>
|
||||
<img src="./images/omniroute.png" alt="OmniRoute Dashboard" width="800"/>
|
||||
|
||||
# 🚀 OmniRoute — Das kostenlose AI-Gateway
|
||||
|
||||
@@ -100,7 +100,6 @@ _Verbinde jedes KI-gesteuerte IDE- oder CLI-Tool über OmniRoute — kostenloses
|
||||
[](https://hub.docker.com/r/diegosouzapw/omniroute)
|
||||
[](https://github.com/diegosouzapw/OmniRoute/blob/main/LICENSE)
|
||||
[](https://omniroute.online)
|
||||
[](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
|
||||
[🌐 Website](https://omniroute.online) • [🚀 Schnellstart](#-schnellstart) • [💡 Funktionen](#-hauptfunktionen) • [📖 Doku](#-dokumentation) • [💰 Preise](#-preisübersicht)
|
||||
|
||||
@@ -243,7 +242,7 @@ docker compose --profile cli up -d
|
||||
| Image | Tag | Größe | Beschreibung |
|
||||
| ------------------------ | -------- | ------ | ------------------------ |
|
||||
| `diegosouzapw/omniroute` | `latest` | ~250MB | Letztes stabiles Release |
|
||||
| `diegosouzapw/omniroute` | `1.0.6` | ~250MB | Aktuelle Version |
|
||||
| `diegosouzapw/omniroute` | `0.9.0` | ~250MB | Aktuelle Version |
|
||||
|
||||
---
|
||||
|
||||
@@ -893,7 +892,7 @@ Das vorgeladene „OmniRoute Golden Set" enthält 10 Testfälle:
|
||||
**Verbindungstest zeigt „Invalid" für OpenAI-kompatible Anbieter**
|
||||
|
||||
- Viele Anbieter stellen den `/models` Endpoint nicht bereit
|
||||
- OmniRoute v1.0.6+ enthält Fallback-Validierung via Chat Completions
|
||||
- OmniRoute v0.9.0+ enthält Fallback-Validierung via Chat Completions
|
||||
- Stelle sicher, dass die Base URL den `/v1` Suffix enthält
|
||||
|
||||
</details>
|
||||
@@ -903,7 +902,7 @@ Das vorgeladene „OmniRoute Golden Set" enthält 10 Testfälle:
|
||||
## 🛠️ Technologie-Stack
|
||||
|
||||
- **Runtime**: Node.js 20+
|
||||
- **Sprache**: TypeScript 5.9 — **100% TypeScript** in `src/` und `open-sse/` (v1.0.6)
|
||||
- **Sprache**: TypeScript 5.9 — **100% TypeScript** in `src/` und `open-sse/` (v0.9.0)
|
||||
- **Framework**: Next.js 16 + React 19 + Tailwind CSS 4
|
||||
- **Datenbank**: LowDB (JSON) + SQLite (Domain-Status + Proxy-Logs)
|
||||
- **Streaming**: Server-Sent Events (SSE)
|
||||
@@ -933,13 +932,9 @@ Das vorgeladene „OmniRoute Golden Set" enthält 10 Testfälle:
|
||||
|
||||
## 📧 Support
|
||||
|
||||
> 💬 **Treten Sie unserer Community bei!** [WhatsApp-Gruppe](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Hilfe bekommen, Tipps teilen und auf dem Laufenden bleiben.
|
||||
|
||||
- **Website**: [omniroute.online](https://omniroute.online)
|
||||
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
|
||||
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
|
||||
- **WhatsApp**: [Community-Gruppe](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
- **WhatsApp**: [Community-Gruppe](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
- **Originalprojekt**: [9router von decolua](https://github.com/decolua/9router)
|
||||
|
||||
---
|
||||
@@ -962,7 +957,7 @@ Siehe [CONTRIBUTING.md](CONTRIBUTING.md) für detaillierte Richtlinien.
|
||||
|
||||
```bash
|
||||
# Release erstellen — npm-Veröffentlichung erfolgt automatisch
|
||||
gh release create v1.0.6 --title "v1.0.6" --generate-notes
|
||||
gh release create v0.9.0 --title "v0.9.0" --generate-notes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<img src="./docs/screenshots/MainOmniRoute.png" alt="OmniRoute Dashboard" width="800"/>
|
||||
<img src="./images/omniroute.png" alt="OmniRoute Dashboard" width="800"/>
|
||||
|
||||
# 🚀 OmniRoute — El Gateway de IA Gratuito
|
||||
|
||||
@@ -100,7 +100,6 @@ _Conecta cualquier IDE o herramienta CLI con IA a través de OmniRoute — gatew
|
||||
[](https://hub.docker.com/r/diegosouzapw/omniroute)
|
||||
[](https://github.com/diegosouzapw/OmniRoute/blob/main/LICENSE)
|
||||
[](https://omniroute.online)
|
||||
[](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
|
||||
[🌐 Website](https://omniroute.online) • [🚀 Inicio Rápido](#-inicio-rápido) • [💡 Características](#-características-principales) • [📖 Docs](#-documentación) • [💰 Precios](#-precios-resumidos)
|
||||
|
||||
@@ -243,7 +242,7 @@ docker compose --profile cli up -d
|
||||
| Imagen | Tag | Tamaño | Descripción |
|
||||
| ------------------------ | -------- | ------ | ---------------------- |
|
||||
| `diegosouzapw/omniroute` | `latest` | ~250MB | Última versión estable |
|
||||
| `diegosouzapw/omniroute` | `1.0.6` | ~250MB | Versión actual |
|
||||
| `diegosouzapw/omniroute` | `0.9.0` | ~250MB | Versión actual |
|
||||
|
||||
---
|
||||
|
||||
@@ -893,7 +892,7 @@ El "OmniRoute Golden Set" precargado contiene 10 casos de prueba que cubren:
|
||||
**Prueba de conexión muestra "Invalid" para proveedores compatibles con OpenAI**
|
||||
|
||||
- Muchos proveedores no exponen el endpoint `/models`
|
||||
- OmniRoute v1.0.6+ incluye validación vía chat completions como fallback
|
||||
- OmniRoute v0.9.0+ incluye validación vía chat completions como fallback
|
||||
- Asegúrate de que la URL base incluya el sufijo `/v1`
|
||||
|
||||
</details>
|
||||
@@ -903,7 +902,7 @@ El "OmniRoute Golden Set" precargado contiene 10 casos de prueba que cubren:
|
||||
## 🛠️ Stack Tecnológico
|
||||
|
||||
- **Runtime**: Node.js 20+
|
||||
- **Lenguaje**: TypeScript 5.9 — **100% TypeScript** en `src/` y `open-sse/` (v1.0.6)
|
||||
- **Lenguaje**: TypeScript 5.9 — **100% TypeScript** en `src/` y `open-sse/` (v0.9.0)
|
||||
- **Framework**: Next.js 16 + React 19 + Tailwind CSS 4
|
||||
- **Base de Datos**: LowDB (JSON) + SQLite (estado del dominio + logs de proxy)
|
||||
- **Streaming**: Server-Sent Events (SSE)
|
||||
@@ -933,12 +932,9 @@ El "OmniRoute Golden Set" precargado contiene 10 casos de prueba que cubren:
|
||||
|
||||
## 📧 Soporte
|
||||
|
||||
> 💬 **¡Únete a la comunidad!** [Grupo WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Obtén ayuda, comparte consejos y mantente al día.
|
||||
|
||||
- **Website**: [omniroute.online](https://omniroute.online)
|
||||
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
|
||||
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
|
||||
- **WhatsApp**: [Grupo de la Comunidad](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
- **Proyecto Original**: [9router por decolua](https://github.com/decolua/9router)
|
||||
|
||||
---
|
||||
@@ -961,7 +957,7 @@ Consulta [CONTRIBUTING.md](CONTRIBUTING.md) para directrices detalladas.
|
||||
|
||||
```bash
|
||||
# Crea un release — la publicación en npm ocurre automáticamente
|
||||
gh release create v1.0.6 --title "v1.0.6" --generate-notes
|
||||
gh release create v0.9.0 --title "v0.9.0" --generate-notes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<img src="./docs/screenshots/MainOmniRoute.png" alt="OmniRoute Dashboard" width="800"/>
|
||||
<img src="./images/omniroute.png" alt="OmniRoute Dashboard" width="800"/>
|
||||
|
||||
# 🚀 OmniRoute — La Passerelle IA Gratuite
|
||||
|
||||
@@ -100,7 +100,6 @@ _Connectez n'importe quel IDE ou outil CLI alimenté par l'IA via OmniRoute —
|
||||
[](https://hub.docker.com/r/diegosouzapw/omniroute)
|
||||
[](https://github.com/diegosouzapw/OmniRoute/blob/main/LICENSE)
|
||||
[](https://omniroute.online)
|
||||
[](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
|
||||
[🌐 Site web](https://omniroute.online) • [🚀 Démarrage rapide](#-démarrage-rapide) • [💡 Fonctionnalités](#-fonctionnalités-principales) • [📖 Docs](#-documentation) • [💰 Tarifs](#-aperçu-des-tarifs)
|
||||
|
||||
@@ -243,7 +242,7 @@ docker compose --profile cli up -d
|
||||
| Image | Tag | Taille | Description |
|
||||
| ------------------------ | -------- | ------ | ----------------------- |
|
||||
| `diegosouzapw/omniroute` | `latest` | ~250MB | Dernière version stable |
|
||||
| `diegosouzapw/omniroute` | `1.0.6` | ~250MB | Version actuelle |
|
||||
| `diegosouzapw/omniroute` | `0.9.0` | ~250MB | Version actuelle |
|
||||
|
||||
---
|
||||
|
||||
@@ -893,7 +892,7 @@ Le « OmniRoute Golden Set » préchargé contient 10 cas de test :
|
||||
**Le test de connexion affiche « Invalid » pour les fournisseurs compatibles OpenAI**
|
||||
|
||||
- Beaucoup de fournisseurs n'exposent pas le point de terminaison `/models`
|
||||
- OmniRoute v1.0.6+ inclut une validation de secours via chat completions
|
||||
- OmniRoute v0.9.0+ inclut une validation de secours via chat completions
|
||||
- Assurez-vous que l'URL de base inclut le suffixe `/v1`
|
||||
|
||||
</details>
|
||||
@@ -903,7 +902,7 @@ Le « OmniRoute Golden Set » préchargé contient 10 cas de test :
|
||||
## 🛠️ Stack technologique
|
||||
|
||||
- **Runtime** : Node.js 20+
|
||||
- **Langage** : TypeScript 5.9 — **100% TypeScript** dans `src/` et `open-sse/` (v1.0.6)
|
||||
- **Langage** : TypeScript 5.9 — **100% TypeScript** dans `src/` et `open-sse/` (v0.9.0)
|
||||
- **Framework** : Next.js 16 + React 19 + Tailwind CSS 4
|
||||
- **Base de données** : LowDB (JSON) + SQLite (état du domaine + logs proxy)
|
||||
- **Streaming** : Server-Sent Events (SSE)
|
||||
@@ -933,12 +932,9 @@ Le « OmniRoute Golden Set » préchargé contient 10 cas de test :
|
||||
|
||||
## 📧 Support
|
||||
|
||||
> 💬 **Rejoignez notre communauté !** [Groupe WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Obtenez de l'aide, partagez des astuces et restez informé.
|
||||
|
||||
- **Site web** : [omniroute.online](https://omniroute.online)
|
||||
- **GitHub** : [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
|
||||
- **Issues** : [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
|
||||
- **WhatsApp** : [Groupe communautaire](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
- **Projet original** : [9router par decolua](https://github.com/decolua/9router)
|
||||
|
||||
---
|
||||
@@ -961,7 +957,7 @@ Consultez [CONTRIBUTING.md](CONTRIBUTING.md) pour les directives détaillées.
|
||||
|
||||
```bash
|
||||
# Créer un release — la publication npm est automatique
|
||||
gh release create v1.0.6 --title "v1.0.6" --generate-notes
|
||||
gh release create v0.9.0 --title "v0.9.0" --generate-notes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<img src="./docs/screenshots/MainOmniRoute.png" alt="OmniRoute Dashboard" width="800"/>
|
||||
<img src="./images/omniroute.png" alt="OmniRoute Dashboard" width="800"/>
|
||||
|
||||
# 🚀 OmniRoute — Il Gateway IA Gratuito
|
||||
|
||||
@@ -100,7 +100,6 @@ _Connetti qualsiasi IDE o strumento CLI con IA tramite OmniRoute — gateway API
|
||||
[](https://hub.docker.com/r/diegosouzapw/omniroute)
|
||||
[](https://github.com/diegosouzapw/OmniRoute/blob/main/LICENSE)
|
||||
[](https://omniroute.online)
|
||||
[](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
|
||||
[🌐 Sito Web](https://omniroute.online) • [🚀 Avvio Rapido](#-avvio-rapido) • [💡 Funzionalità](#-funzionalità-principali) • [📖 Docs](#-documentazione) • [💰 Prezzi](#-panoramica-prezzi)
|
||||
|
||||
@@ -243,7 +242,7 @@ docker compose --profile cli up -d
|
||||
| Immagine | Tag | Dimensione | Descrizione |
|
||||
| ------------------------ | -------- | ---------- | ----------------------- |
|
||||
| `diegosouzapw/omniroute` | `latest` | ~250MB | Ultima versione stabile |
|
||||
| `diegosouzapw/omniroute` | `1.0.6` | ~250MB | Versione attuale |
|
||||
| `diegosouzapw/omniroute` | `0.9.0` | ~250MB | Versione attuale |
|
||||
|
||||
---
|
||||
|
||||
@@ -893,7 +892,7 @@ Il "OmniRoute Golden Set" precaricato contiene 10 casi di test:
|
||||
**Il test di connessione mostra "Invalid" per provider compatibili OpenAI**
|
||||
|
||||
- Molti provider non espongono l'endpoint `/models`
|
||||
- OmniRoute v1.0.6+ include validazione fallback tramite chat completions
|
||||
- OmniRoute v0.9.0+ include validazione fallback tramite chat completions
|
||||
- Assicurati che la URL base includa il suffisso `/v1`
|
||||
|
||||
</details>
|
||||
@@ -903,7 +902,7 @@ Il "OmniRoute Golden Set" precaricato contiene 10 casi di test:
|
||||
## 🛠️ Stack Tecnologico
|
||||
|
||||
- **Runtime**: Node.js 20+
|
||||
- **Linguaggio**: TypeScript 5.9 — **100% TypeScript** in `src/` e `open-sse/` (v1.0.6)
|
||||
- **Linguaggio**: TypeScript 5.9 — **100% TypeScript** in `src/` e `open-sse/` (v0.9.0)
|
||||
- **Framework**: Next.js 16 + React 19 + Tailwind CSS 4
|
||||
- **Database**: LowDB (JSON) + SQLite (stato dominio + log proxy)
|
||||
- **Streaming**: Server-Sent Events (SSE)
|
||||
@@ -933,13 +932,9 @@ Il "OmniRoute Golden Set" precaricato contiene 10 casi di test:
|
||||
|
||||
## 📧 Supporto
|
||||
|
||||
> 💬 **Unisciti alla nostra community!** [Gruppo WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Ottieni aiuto, condividi consigli e rimani aggiornato.
|
||||
|
||||
- **Sito Web**: [omniroute.online](https://omniroute.online)
|
||||
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
|
||||
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
|
||||
- **WhatsApp**: [Gruppo della comunità](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
- **WhatsApp**: [Gruppo della comunità](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
- **Progetto Originale**: [9router di decolua](https://github.com/decolua/9router)
|
||||
|
||||
---
|
||||
@@ -962,7 +957,7 @@ Consulta [CONTRIBUTING.md](CONTRIBUTING.md) per le linee guida dettagliate.
|
||||
|
||||
```bash
|
||||
# Crea un rilascio — la pubblicazione npm avviene automaticamente
|
||||
gh release create v1.0.6 --title "v1.0.6" --generate-notes
|
||||
gh release create v0.9.0 --title "v0.9.0" --generate-notes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<div align="center">
|
||||
<img src="./docs/screenshots/MainOmniRoute.png" alt="OmniRoute Dashboard" width="800"/>
|
||||
<img src="./images/omniroute.png" alt="OmniRoute Dashboard" width="800"/>
|
||||
|
||||
# 🚀 OmniRoute — The Free AI Gateway
|
||||
|
||||
🌐 **[English](#-omniroute--the-free-ai-gateway)** | **[Português (BR)](#-omniroute--gateway-de-ia-gratuito)**
|
||||
|
||||
### Never stop coding. Smart routing to **FREE & low-cost AI models** with automatic fallback.
|
||||
|
||||
_Your universal API proxy — one endpoint, 36+ providers, zero downtime._
|
||||
@@ -102,9 +100,8 @@ _Connect any AI-powered IDE or CLI tool through OmniRoute — free API gateway f
|
||||
[](https://hub.docker.com/r/diegosouzapw/omniroute)
|
||||
[](https://github.com/diegosouzapw/OmniRoute/blob/main/LICENSE)
|
||||
[](https://omniroute.online)
|
||||
[](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
|
||||
[🌐 Website](https://omniroute.online) • [🚀 Quick Start](#-quick-start) • [💡 Features](#-key-features) • [📖 Docs](#-documentation) • [💰 Pricing](#-pricing-at-a-glance) • [💬 WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
[🌐 Website](https://omniroute.online) • [🚀 Quick Start](#-quick-start) • [💡 Features](#-key-features) • [📖 Docs](#-documentation) • [💰 Pricing](#-pricing-at-a-glance)
|
||||
|
||||
🌐 **Available in:** [English](README.md) | [Português](README.pt-BR.md) | [Español](README.es.md) | [Русский](README.ru.md) | [中文](README.zh-CN.md) | [Deutsch](README.de.md) | [Français](README.fr.md) | [Italiano](README.it.md)
|
||||
|
||||
@@ -245,7 +242,7 @@ docker compose --profile cli up -d
|
||||
| Image | Tag | Size | Description |
|
||||
| ------------------------ | -------- | ------ | --------------------- |
|
||||
| `diegosouzapw/omniroute` | `latest` | ~250MB | Latest stable release |
|
||||
| `diegosouzapw/omniroute` | `1.0.3` | ~250MB | Current version |
|
||||
| `diegosouzapw/omniroute` | `0.9.0` | ~250MB | Current version |
|
||||
|
||||
---
|
||||
|
||||
@@ -343,7 +340,7 @@ Access via: WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
|
||||
| ------------------------------- | ------------------------------------------------------------------------------ |
|
||||
| 🎯 **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 + response sanitization |
|
||||
| 🔄 **Format Translation** | OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro seamless |
|
||||
| 👥 **Multi-Account Support** | Multiple accounts per provider with intelligent selection |
|
||||
| 🔄 **Auto Token Refresh** | OAuth tokens refresh automatically with retry |
|
||||
| 🎨 **Custom Combos** | 6 strategies: fill-first, round-robin, p2c, random, least-used, cost-optimized |
|
||||
@@ -375,35 +372,31 @@ Access via: WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
|
||||
| 🔒 **TLS Fingerprint Spoofing** | Bypass TLS-based bot detection via wreq-js |
|
||||
| 🌐 **IP Filtering** | Allowlist/blocklist for API access control |
|
||||
| 📊 **Editable Rate Limits** | Configurable RPM, min gap, and max concurrent at system level |
|
||||
| 🛡 **API Endpoint Protection** | Auth gating + provider blocking for the `/models` endpoint |
|
||||
|
||||
### 📊 Observability & Analytics
|
||||
|
||||
| Feature | What It Does |
|
||||
| -------------------------- | ---------------------------------------------------------------------- |
|
||||
| 📝 **Request Logging** | Debug mode with full request/response logs |
|
||||
| 💾 **SQLite Proxy Logs** | Persistent proxy logs survive server restarts |
|
||||
| 📊 **Analytics Dashboard** | Recharts-powered: stat cards, model usage chart, provider table |
|
||||
| 📈 **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 |
|
||||
| 📋 **Logs Dashboard** | Unified 4-tab page: Request Logs, Proxy Logs, Audit Logs, Console |
|
||||
| 🖥️ **Console Log Viewer** | Real-time terminal-style viewer with level filter, search, auto-scroll |
|
||||
| 📑 **File-Based Logging** | Console interceptor captures all output to JSON log file with rotation |
|
||||
| 🏥 **Health Dashboard** | System uptime, circuit breaker states, lockouts, cache stats |
|
||||
| 💰 **Cost Tracking** | Budget management + per-model pricing configuration |
|
||||
| Feature | What It Does |
|
||||
| ---------------------------- | --------------------------------------------------------------- |
|
||||
| 📝 **Request Logging** | Debug mode with full request/response logs |
|
||||
| 💾 **SQLite Proxy Logs** | Persistent proxy logs survive server restarts |
|
||||
| 📊 **Analytics Dashboard** | Recharts-powered: stat cards, model usage chart, provider table |
|
||||
| 📈 **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 |
|
||||
| 📋 **Request Logs + Quotas** | Dedicated pages for log browsing and limits/quotas tracking |
|
||||
| 🏥 **Health Dashboard** | System uptime, circuit breaker states, lockouts, cache stats |
|
||||
| 💰 **Cost Tracking** | Budget management + per-model pricing configuration |
|
||||
|
||||
### ☁️ 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 |
|
||||
| 🧙 **Onboarding Wizard** | 4-step guided setup for first-time users |
|
||||
| 🔧 **CLI Tools Dashboard** | One-click configure Claude, Codex, Cline, OpenClaw, Kilo, Antigravity |
|
||||
| 🔄 **DB Backups** | Automatic backup, restore, export & import for all settings |
|
||||
| 🌐 **Internationalization** | Full i18n with next-intl — English + Portuguese (Brazil) support |
|
||||
| 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 |
|
||||
| 🧙 **Onboarding Wizard** | 4-step guided setup for first-time users |
|
||||
| 🔧 **CLI Tools Dashboard** | One-click configure Claude, Codex, Cline, OpenClaw, Kilo, Antigravity |
|
||||
| 🔄 **DB Backups** | Automatic backup and restore for all settings |
|
||||
|
||||
<details>
|
||||
<summary><b>📖 Feature Details</b></summary>
|
||||
@@ -436,10 +429,6 @@ Seamless translation between formats:
|
||||
- **OpenAI** ↔ **Claude** ↔ **Gemini** ↔ **OpenAI Responses**
|
||||
- Your CLI tool sends OpenAI format → OmniRoute translates → Provider receives native format
|
||||
- Works with any tool that supports custom OpenAI endpoints
|
||||
- **Response sanitization** — Strips non-standard fields for strict OpenAI SDK compatibility
|
||||
- **Role normalization** — `developer` → `system` for non-OpenAI; `system` → `user` for GLM/ERNIE models
|
||||
- **Think tag extraction** — `<think>` blocks → `reasoning_content` for thinking models
|
||||
- **Structured output** — `json_schema` → Gemini's `responseMimeType`/`responseSchema`
|
||||
|
||||
### 👥 Multi-Account Support
|
||||
|
||||
@@ -470,16 +459,9 @@ Seamless translation between formats:
|
||||
|
||||
### 🔧 Translator Playground
|
||||
|
||||
OmniRoute includes a powerful built-in Translator Playground with **4 modes** for debugging, testing, and monitoring API translations:
|
||||
|
||||
| Mode | Description |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **💻 Playground** | Direct format translation — paste any API request body and instantly see how OmniRoute translates it between provider formats (OpenAI ↔ Claude ↔ Gemini ↔ Responses API). Includes example templates and format auto-detection. |
|
||||
| **💬 Chat Tester** | Send real chat requests through OmniRoute and see the full round-trip: your input, the translated request, the provider response, and the translated response back. Invaluable for validating combo routing. |
|
||||
| **🧪 Test Bench** | Batch testing mode — define multiple test cases with different inputs and expected outputs, run them all at once, and compare results across providers and models. |
|
||||
| **📱 Live Monitor** | Real-time request monitoring — watch incoming requests as they flow through OmniRoute, see format translations happening live, and identify issues instantly. |
|
||||
|
||||
**Access:** Dashboard → Translator (sidebar)
|
||||
- Debug, test, and visualize API format translations
|
||||
- Send requests and see how OmniRoute translates between provider formats
|
||||
- Invaluable for troubleshooting integration issues
|
||||
|
||||
### 💾 Cloud Sync
|
||||
|
||||
@@ -862,99 +844,6 @@ The pre-loaded "OmniRoute Golden Set" contains 10 test cases covering:
|
||||
|
||||
---
|
||||
|
||||
## 🔐 OAuth em Servidor Remoto (Remote OAuth Setup)
|
||||
|
||||
<a name="oauth-em-servidor-remoto"></a>
|
||||
|
||||
> **⚠️ IMPORTANTE para usuários com OmniRoute em VPS/Docker/servidor remoto**
|
||||
|
||||
### Por que o OAuth do Antigravity / Gemini CLI falha em servidores remotos?
|
||||
|
||||
Os provedores **Antigravity** e **Gemini CLI** usam **Google OAuth 2.0** para autenticação. O Google exige que a `redirect_uri` usada no fluxo OAuth seja **exatamente** uma das URIs pré-cadastradas no Google Cloud Console do aplicativo.
|
||||
|
||||
As credenciais OAuth embutidas no OmniRoute estão cadastradas **apenas para `localhost`**. Quando você acessa o OmniRoute em um servidor remoto (ex: `https://omniroute.meuservidor.com`), o Google rejeita a autenticação com:
|
||||
|
||||
```
|
||||
Error 400: redirect_uri_mismatch
|
||||
```
|
||||
|
||||
### Solução: Configure suas próprias credenciais OAuth
|
||||
|
||||
Você precisa criar um **OAuth 2.0 Client ID** no Google Cloud Console com a URI do seu servidor.
|
||||
|
||||
#### Passo a passo
|
||||
|
||||
**1. Acesse o Google Cloud Console**
|
||||
|
||||
Abra: [https://console.cloud.google.com/apis/credentials](https://console.cloud.google.com/apis/credentials)
|
||||
|
||||
**2. Crie um novo OAuth 2.0 Client ID**
|
||||
|
||||
- Clique em **"+ Create Credentials"** → **"OAuth client ID"**
|
||||
- Tipo de aplicativo: **"Web application"**
|
||||
- Nome: escolha qualquer nome (ex: `OmniRoute Remote`)
|
||||
|
||||
**3. Adicione as Authorized Redirect URIs**
|
||||
|
||||
No campo **"Authorized redirect URIs"**, adicione:
|
||||
|
||||
```
|
||||
https://seu-servidor.com/callback
|
||||
```
|
||||
|
||||
> Substitua `seu-servidor.com` pelo domínio ou IP do seu servidor (inclua a porta se necessário, ex: `http://45.33.32.156:20128/callback`).
|
||||
|
||||
**4. Salve e copie as credenciais**
|
||||
|
||||
Após criar, o Google mostrará o **Client ID** e o **Client Secret**.
|
||||
|
||||
**5. Configure as variáveis de ambiente**
|
||||
|
||||
No seu `.env` (ou nas variáveis de ambiente do Docker):
|
||||
|
||||
```bash
|
||||
# Para Antigravity:
|
||||
ANTIGRAVITY_OAUTH_CLIENT_ID=seu-client-id.apps.googleusercontent.com
|
||||
ANTIGRAVITY_OAUTH_CLIENT_SECRET=GOCSPX-seu-secret
|
||||
|
||||
# Para Gemini CLI:
|
||||
GEMINI_OAUTH_CLIENT_ID=seu-client-id.apps.googleusercontent.com
|
||||
GEMINI_OAUTH_CLIENT_SECRET=GOCSPX-seu-secret
|
||||
GEMINI_CLI_OAUTH_CLIENT_SECRET=GOCSPX-seu-secret
|
||||
```
|
||||
|
||||
**6. Reinicie o OmniRoute**
|
||||
|
||||
```bash
|
||||
# Se usando npm:
|
||||
npm run dev
|
||||
|
||||
# Se usando Docker:
|
||||
docker restart omniroute
|
||||
```
|
||||
|
||||
**7. Tente conectar novamente**
|
||||
|
||||
Dashboard → Providers → Antigravity (ou Gemini CLI) → OAuth
|
||||
|
||||
Agora o Google redirecionará corretamente para `https://seu-servidor.com/callback` e a autenticação funcionará.
|
||||
|
||||
---
|
||||
|
||||
### Workaround temporário (sem configurar credenciais próprias)
|
||||
|
||||
Se não quiser criar credenciais próprias agora, ainda é possível usar o fluxo **manual de URL**:
|
||||
|
||||
1. O OmniRoute abrirá a URL de autorização do Google
|
||||
2. Após você autorizar, o Google tentará redirecionar para `localhost` (que falha no servidor remoto)
|
||||
3. **Copie a URL completa** da barra de endereço do seu browser (mesmo que a página não carregue)
|
||||
4. Cole essa URL no campo que aparece no modal de conexão do OmniRoute
|
||||
5. Clique em **"Connect"**
|
||||
|
||||
> Este workaround funciona porque o código de autorização na URL é válido independente do redirect ter carregado ou não.
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
<details>
|
||||
@@ -1003,7 +892,7 @@ Se não quiser criar credenciais próprias agora, ainda é possível usar o flux
|
||||
**Connection test shows "Invalid" for OpenAI-compatible providers**
|
||||
|
||||
- Many providers don't expose a `/models` endpoint
|
||||
- OmniRoute v1.0.6+ includes fallback validation via chat completions
|
||||
- OmniRoute v0.9.0+ includes fallback validation via chat completions
|
||||
- Ensure base URL includes `/v1` suffix
|
||||
|
||||
</details>
|
||||
@@ -1012,8 +901,8 @@ Se não quiser criar credenciais próprias agora, ainda é possível usar o flux
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
- **Runtime**: Node.js 18–22 LTS (⚠️ Node.js 24+ is **not supported** — `better-sqlite3` native binaries are incompatible)
|
||||
- **Language**: TypeScript 5.9 — **100% TypeScript** across `src/` and `open-sse/` (v1.0.6)
|
||||
- **Runtime**: Node.js 20+
|
||||
- **Language**: TypeScript 5.9 — **100% TypeScript** across `src/` and `open-sse/` (v0.9.0)
|
||||
- **Framework**: Next.js 16 + React 19 + Tailwind CSS 4
|
||||
- **Database**: LowDB (JSON) + SQLite (domain state + proxy logs)
|
||||
- **Streaming**: Server-Sent Events (SSE)
|
||||
@@ -1029,72 +918,23 @@ Se não quiser criar credenciais próprias agora, ainda é possível usar o flux
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
| Document | Description |
|
||||
| -------------------------------------------- | ---------------------------------------------- |
|
||||
| [User Guide](docs/USER_GUIDE.md) | Providers, combos, CLI integration, deployment |
|
||||
| [API Reference](docs/API_REFERENCE.md) | All endpoints with examples |
|
||||
| [Troubleshooting](docs/TROUBLESHOOTING.md) | Common problems and solutions |
|
||||
| [Architecture](docs/ARCHITECTURE.md) | System architecture and internals |
|
||||
| [Contributing](CONTRIBUTING.md) | Development setup and guidelines |
|
||||
| [OpenAPI Spec](docs/openapi.yaml) | OpenAPI 3.0 specification |
|
||||
| [Security Policy](SECURITY.md) | Vulnerability reporting and security practices |
|
||||
| [VM Deployment](docs/VM_DEPLOYMENT_GUIDE.md) | Complete guide: VM + nginx + Cloudflare setup |
|
||||
| [Features Gallery](docs/FEATURES.md) | Visual dashboard tour with screenshots |
|
||||
|
||||
### 📸 Dashboard Preview
|
||||
|
||||
<details>
|
||||
<summary><b>Click to see dashboard screenshots</b></summary>
|
||||
|
||||
| Page | Screenshot |
|
||||
| -------------- | ------------------------------------------------- |
|
||||
| **Providers** |  |
|
||||
| **Combos** |  |
|
||||
| **Analytics** |  |
|
||||
| **Health** |  |
|
||||
| **Translator** |  |
|
||||
| **Settings** |  |
|
||||
| **CLI Tools** |  |
|
||||
| **Usage Logs** |  |
|
||||
| **Endpoint** |  |
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ Roadmap
|
||||
|
||||
OmniRoute has **210+ features planned** across multiple development phases. Here are the key areas:
|
||||
|
||||
| Category | Planned Features | Highlights |
|
||||
| ----------------------------- | ---------------- | -------------------------------------------------------------------------------------- |
|
||||
| 🧠 **Routing & Intelligence** | 25+ | Lowest-latency routing, tag-based routing, quota preflight, P2C account selection |
|
||||
| 🔒 **Security & Compliance** | 20+ | SSRF hardening, credential cloaking, rate-limit per endpoint, management key scoping |
|
||||
| 📊 **Observability** | 15+ | OpenTelemetry integration, real-time quota monitoring, cost tracking per model |
|
||||
| 🔄 **Provider Integrations** | 20+ | Dynamic model registry, provider cooldowns, multi-account Codex, Copilot quota parsing |
|
||||
| ⚡ **Performance** | 15+ | Dual cache layer, prompt cache, response cache, streaming keepalive, batch API |
|
||||
| 🌐 **Ecosystem** | 10+ | WebSocket API, config hot-reload, distributed config store, commercial mode |
|
||||
|
||||
### 🔜 Coming Soon
|
||||
|
||||
- 🔗 **OpenCode Integration** — Native provider support for the OpenCode AI coding IDE
|
||||
- 🔗 **TRAE Integration** — Full support for the TRAE AI development framework
|
||||
- 📦 **Batch API** — Asynchronous batch processing for bulk requests
|
||||
- 🎯 **Tag-Based Routing** — Route requests based on custom tags and metadata
|
||||
- 💰 **Lowest-Cost Strategy** — Automatically select the cheapest available provider
|
||||
|
||||
> 📝 Full feature specifications available in [`docs/new-features/`](docs/new-features/) (217 detailed specs)
|
||||
| Document | Description |
|
||||
| ------------------------------------------ | ---------------------------------------------- |
|
||||
| [User Guide](docs/USER_GUIDE.md) | Providers, combos, CLI integration, deployment |
|
||||
| [API Reference](docs/API_REFERENCE.md) | All endpoints with examples |
|
||||
| [Troubleshooting](docs/TROUBLESHOOTING.md) | Common problems and solutions |
|
||||
| [Architecture](docs/ARCHITECTURE.md) | System architecture and internals |
|
||||
| [Contributing](CONTRIBUTING.md) | Development setup and guidelines |
|
||||
| [OpenAPI Spec](docs/openapi.yaml) | OpenAPI 3.0 specification |
|
||||
| [Security Policy](SECURITY.md) | Vulnerability reporting and security practices |
|
||||
|
||||
---
|
||||
|
||||
## 📧 Support
|
||||
|
||||
> 💬 **Join our community!** [WhatsApp Group](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Get help, share tips, and stay updated.
|
||||
|
||||
- **Website**: [omniroute.online](https://omniroute.online)
|
||||
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
|
||||
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
|
||||
- **WhatsApp**: [Community Group](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
- **Original Project**: [9router by decolua](https://github.com/decolua/9router)
|
||||
|
||||
---
|
||||
@@ -1117,7 +957,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
|
||||
|
||||
```bash
|
||||
# Create a release — npm publish happens automatically
|
||||
gh release create v1.0.6 --title "v1.0.6" --generate-notes
|
||||
gh release create v0.9.0 --title "v0.9.0" --generate-notes
|
||||
```
|
||||
|
||||
---
|
||||
@@ -1148,85 +988,6 @@ MIT License - see [LICENSE](LICENSE) for details.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 🇧🇷 OmniRoute — Gateway de IA Gratuito
|
||||
|
||||
<a name="-omniroute--gateway-de-ia-gratuito"></a>
|
||||
|
||||
### Nunca pare de codar. Roteamento inteligente para **modelos de IA GRATUITOS e de baixo custo** com fallback automático.
|
||||
|
||||
_Seu proxy universal de API — um endpoint, 36+ provedores, zero downtime._
|
||||
|
||||
### 🌐 Internacionalização (i18n)
|
||||
|
||||
O dashboard do OmniRoute suporta **múltiplos idiomas**. Atualmente disponível em:
|
||||
|
||||
| Idioma | Código | Status |
|
||||
| --------------------- | ------- | ----------- |
|
||||
| 🇺🇸 English | `en` | ✅ Completo |
|
||||
| 🇧🇷 Português (Brasil) | `pt-BR` | ✅ Completo |
|
||||
|
||||
**Para trocar o idioma:** Clique no seletor de idioma (🇺🇸 EN) no header do dashboard → selecione o idioma desejado.
|
||||
|
||||
**Para adicionar um novo idioma:**
|
||||
|
||||
1. Crie `src/i18n/messages/{codigo}.json` baseado em `en.json`
|
||||
2. Adicione o código em `src/i18n/config.ts` → `LOCALES` e `LANGUAGES`
|
||||
3. Reinicie o servidor
|
||||
|
||||
### ⚡ Início Rápido
|
||||
|
||||
```bash
|
||||
# Instalar via npm
|
||||
npx omniroute@latest
|
||||
|
||||
# Ou rodar do código-fonte
|
||||
cp .env.example .env
|
||||
npm install
|
||||
PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev
|
||||
```
|
||||
|
||||
### 🐳 Docker
|
||||
|
||||
```bash
|
||||
docker run -d --name omniroute -p 20128:20128 diegosouzapw/omniroute:latest
|
||||
```
|
||||
|
||||
### 🔑 Funcionalidades Principais
|
||||
|
||||
- **36+ provedores de IA** — Claude, GPT, Gemini, Llama, Qwen, DeepSeek, e mais
|
||||
- **Roteamento inteligente** — Fallback automático entre provedores
|
||||
- **Tradução de formato** — OpenAI ↔ Claude ↔ Gemini automaticamente
|
||||
- **Multi-conta** — Múltiplas contas por provedor com seleção inteligente
|
||||
- **Cache semântico** — Reduz custos e latência
|
||||
- **OAuth automático** — Tokens renovam automaticamente
|
||||
- **Combos personalizados** — 6 estratégias de roteamento
|
||||
- **Dashboard completo** — Monitoramento, logs, análises, configurações
|
||||
- **CLI Tools** — Configure Claude Code, Codex, Cursor, Cline com um clique
|
||||
- **100% TypeScript** — Código limpo e tipado
|
||||
|
||||
### 📖 Documentação
|
||||
|
||||
| Documento | Descrição |
|
||||
| ----------------------------------------------- | -------------------------------------- |
|
||||
| [Guia do Usuário](docs/USER_GUIDE.md) | Provedores, combos, CLI, deploy |
|
||||
| [Referência da API](docs/API_REFERENCE.md) | Todos os endpoints com exemplos |
|
||||
| [Solução de Problemas](docs/TROUBLESHOOTING.md) | Problemas comuns e soluções |
|
||||
| [Arquitetura](docs/ARCHITECTURE.md) | Arquitetura e internos do sistema |
|
||||
| [Contribuição](CONTRIBUTING.md) | Setup de desenvolvimento e guidelines |
|
||||
| [Deploy em VM](docs/VM_DEPLOYMENT_GUIDE.md) | Guia completo: VM + nginx + Cloudflare |
|
||||
|
||||
### 📧 Suporte
|
||||
|
||||
> 💬 **Entre para a comunidade!** [Grupo WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Tire dúvidas, compartilhe dicas e fique atualizado.
|
||||
|
||||
- **Website**: [omniroute.online](https://omniroute.online)
|
||||
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
|
||||
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<sub>Built with ❤️ for developers who code 24/7</sub>
|
||||
<br/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<img src="./docs/screenshots/MainOmniRoute.png" alt="OmniRoute Dashboard" width="800"/>
|
||||
<img src="./images/omniroute.png" alt="OmniRoute Dashboard" width="800"/>
|
||||
|
||||
# 🚀 OmniRoute — O Gateway de IA Gratuito
|
||||
|
||||
@@ -100,7 +100,6 @@ _Conecte qualquer IDE ou ferramenta CLI com IA através do OmniRoute — gateway
|
||||
[](https://hub.docker.com/r/diegosouzapw/omniroute)
|
||||
[](https://github.com/diegosouzapw/OmniRoute/blob/main/LICENSE)
|
||||
[](https://omniroute.online)
|
||||
[](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
|
||||
[🌐 Website](https://omniroute.online) • [🚀 Início Rápido](#-início-rápido) • [💡 Funcionalidades](#-funcionalidades-principais) • [📖 Docs](#-documentação) • [💰 Preços](#-preços-resumidos)
|
||||
|
||||
@@ -243,7 +242,7 @@ docker compose --profile cli up -d
|
||||
| Imagem | Tag | Tamanho | Descrição |
|
||||
| ------------------------ | -------- | ------- | --------------------- |
|
||||
| `diegosouzapw/omniroute` | `latest` | ~250MB | Última versão estável |
|
||||
| `diegosouzapw/omniroute` | `1.0.6` | ~250MB | Versão atual |
|
||||
| `diegosouzapw/omniroute` | `0.9.0` | ~250MB | Versão atual |
|
||||
|
||||
---
|
||||
|
||||
@@ -373,7 +372,6 @@ Acesso via: WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
|
||||
| 🔒 **Spoofing de Fingerprint TLS** | Bypass de detecção de bot via TLS com wreq-js |
|
||||
| 🌐 **Filtragem de IP** | Allowlist/blocklist para controle de acesso à API |
|
||||
| 📊 **Rate Limits Editáveis** | RPM, gap mínimo e concorrência máxima configuráveis |
|
||||
| 🛡 **Proteção de Endpoint API** | Gateway de Auth + bloqueio de provedores para o endpoint `/models` |
|
||||
|
||||
### 📊 Observabilidade e Analytics
|
||||
|
||||
@@ -391,15 +389,14 @@ Acesso via: WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
|
||||
|
||||
### ☁️ Deploy e Sincronização
|
||||
|
||||
| Funcionalidade | O que Faz |
|
||||
| --------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| 💾 **Cloud Sync** | Sincronize configurações entre dispositivos via Cloudflare Workers |
|
||||
| 🌐 **Deploy em Qualquer Lugar** | Localhost, VPS, Docker, Cloudflare Workers |
|
||||
| 🔑 **Gestão de API Keys** | Gere, rotacione e defina escopo de API keys por provedor |
|
||||
| 🧙 **Assistente de Configuração** | Setup guiado em 4 etapas para novos usuários |
|
||||
| 🔧 **Dashboard CLI Tools** | Configuração em um clique para Claude, Codex, Cline, OpenClaw, Kilo, Antigravity |
|
||||
| 🔄 **Backups de DB** | Backup, restauração, exportação e importação automática de todas as configurações |
|
||||
| 🌐 **Internacionalização** | i18n completo com next-intl — suporte English + Português (Brasil) |
|
||||
| Funcionalidade | O que Faz |
|
||||
| --------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| 💾 **Cloud Sync** | Sincronize configurações entre dispositivos via Cloudflare Workers |
|
||||
| 🌐 **Deploy em Qualquer Lugar** | Localhost, VPS, Docker, Cloudflare Workers |
|
||||
| 🔑 **Gestão de API Keys** | Gere, rotacione e defina escopo de API keys por provedor |
|
||||
| 🧙 **Assistente de Configuração** | Setup guiado em 4 etapas para novos usuários |
|
||||
| 🔧 **Dashboard CLI Tools** | Configuração em um clique para Claude, Codex, Cline, OpenClaw, Kilo, Antigravity |
|
||||
| 🔄 **Backups de DB** | Backup e restauração automáticos de todas as configurações |
|
||||
|
||||
<details>
|
||||
<summary><b>📖 Detalhes das Funcionalidades</b></summary>
|
||||
@@ -462,16 +459,9 @@ Tradução transparente entre formatos:
|
||||
|
||||
### 🔧 Playground do Tradutor
|
||||
|
||||
O OmniRoute inclui um poderoso Playground de Tradução integrado com **4 modos** para debug, teste e monitoramento de traduções de API:
|
||||
|
||||
| Modo | Descrição |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **💻 Playground** | Tradução direta entre formatos — cole qualquer corpo de requisição e veja instantaneamente como o OmniRoute traduz entre formatos (OpenAI ↔ Claude ↔ Gemini ↔ Responses API). Inclui templates de exemplo e auto-detecção de formato. |
|
||||
| **💬 Chat Tester** | Envie requisições reais pelo OmniRoute e veja a viagem completa: sua entrada, a requisição traduzida, a resposta do provedor, e a resposta traduzida de volta. Inestimável para validar roteamento de combos. |
|
||||
| **🧪 Test Bench** | Modo de teste em lote — defina múltiplos casos de teste com diferentes entradas e saídas esperadas, execute todos de uma vez, e compare resultados entre provedores e modelos. |
|
||||
| **📱 Live Monitor** | Monitoramento de requisições em tempo real — acompanhe requisições entrando conforme fluem pelo OmniRoute, veja traduções de formato acontecendo ao vivo, e identifique problemas instantaneamente. |
|
||||
|
||||
**Acesso:** Dashboard → Translator (barra lateral)
|
||||
- Debug, teste e visualize traduções de formato de API
|
||||
- Envie requisições e veja como o OmniRoute traduz entre formatos de provedores
|
||||
- Inestimável para troubleshooting de problemas de integração
|
||||
|
||||
### 💾 Cloud Sync
|
||||
|
||||
@@ -902,7 +892,7 @@ O "OmniRoute Golden Set" pré-carregado contém 10 casos de teste cobrindo:
|
||||
**Teste de conexão mostra "Invalid" para provedores compatíveis com OpenAI**
|
||||
|
||||
- Muitos provedores não expõem endpoint `/models`
|
||||
- OmniRoute v1.0.6+ inclui validação via chat completions como fallback
|
||||
- OmniRoute v0.9.0+ inclui validação via chat completions como fallback
|
||||
- Certifique-se de que a base URL inclui sufixo `/v1`
|
||||
|
||||
</details>
|
||||
@@ -912,7 +902,7 @@ O "OmniRoute Golden Set" pré-carregado contém 10 casos de teste cobrindo:
|
||||
## 🛠️ Stack Tecnológico
|
||||
|
||||
- **Runtime**: Node.js 20+
|
||||
- **Linguagem**: TypeScript 5.9 — **100% TypeScript** em `src/` e `open-sse/` (v1.0.6)
|
||||
- **Linguagem**: TypeScript 5.9 — **100% TypeScript** em `src/` e `open-sse/` (v0.9.0)
|
||||
- **Framework**: Next.js 16 + React 19 + Tailwind CSS 4
|
||||
- **Banco de Dados**: LowDB (JSON) + SQLite (estado do domínio + logs de proxy)
|
||||
- **Streaming**: Server-Sent Events (SSE)
|
||||
@@ -937,63 +927,14 @@ O "OmniRoute Golden Set" pré-carregado contém 10 casos de teste cobrindo:
|
||||
| [Contribuindo](CONTRIBUTING.md) | Setup de desenvolvimento e diretrizes |
|
||||
| [Spec OpenAPI](docs/openapi.yaml) | Especificação OpenAPI 3.0 |
|
||||
| [Política de Segurança](SECURITY.md) | Reportar vulnerabilidades e práticas de segurança |
|
||||
| [Deploy em VM](docs/VM_DEPLOYMENT_GUIDE.md) | Guia completo: VM + nginx + Cloudflare |
|
||||
| [Galeria de Features](docs/FEATURES.md) | Tour visual do dashboard com screenshots |
|
||||
|
||||
### 📸 Preview do Dashboard
|
||||
|
||||
<details>
|
||||
<summary><b>Clique para ver screenshots do dashboard</b></summary>
|
||||
|
||||
| Página | Screenshot |
|
||||
| ----------------- | ------------------------------------------------- |
|
||||
| **Provedores** |  |
|
||||
| **Combos** |  |
|
||||
| **Analytics** |  |
|
||||
| **Saúde** |  |
|
||||
| **Tradutor** |  |
|
||||
| **Configurações** |  |
|
||||
| **CLI Tools** |  |
|
||||
| **Logs de Uso** |  |
|
||||
| **Endpoint** |  |
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ Roadmap
|
||||
|
||||
O OmniRoute tem **210+ funcionalidades planejadas** em múltiplas fases de desenvolvimento. Áreas principais:
|
||||
|
||||
| Categoria | Features Planejadas | Destaques |
|
||||
| -------------------------------- | ------------------- | --------------------------------------------------------------------------- |
|
||||
| 🧠 **Roteamento e Inteligência** | 25+ | Roteamento por menor latência, roteamento por tags, pré-verificação de cota |
|
||||
| 🔒 **Segurança e Compliance** | 20+ | Hardening SSRF, cloaking de credenciais, rate-limit por endpoint |
|
||||
| 📊 **Observabilidade** | 15+ | Integração OpenTelemetry, monitoramento de cota em tempo real |
|
||||
| 🔄 **Integrações** | 20+ | Registro dinâmico de modelos, cooldowns de provedor |
|
||||
| ⚡ **Performance** | 15+ | Cache dupla camada, prompt cache, response cache, batch API |
|
||||
| 🌐 **Ecossistema** | 10+ | WebSocket API, config hot-reload, modo comercial |
|
||||
|
||||
### 🔜 Em Breve
|
||||
|
||||
- 🔗 **Integração com OpenCode** — Suporte nativo como provider para a IDE OpenCode
|
||||
- 🔗 **Integração com TRAE** — Suporte completo ao framework de desenvolvimento TRAE
|
||||
- 📦 **Batch API** — Processamento assíncrono em lote para requisições em massa
|
||||
- 🎯 **Roteamento por Tags** — Roteamento de requisições baseado em tags personalizadas
|
||||
- 💰 **Estratégia de Menor Custo** — Seleção automática do provedor mais barato
|
||||
|
||||
> 📝 Especificações completas de features disponíveis em [`docs/new-features/`](docs/new-features/) (217 specs detalhadas)
|
||||
|
||||
---
|
||||
|
||||
## 📧 Suporte
|
||||
|
||||
> 💬 **Participe da comunidade!** [Grupo WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Tire dúvidas, compartilhe dicas e fique atualizado.
|
||||
|
||||
- **Website**: [omniroute.online](https://omniroute.online)
|
||||
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
|
||||
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
|
||||
- **WhatsApp**: [Grupo da Comunidade](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
- **Projeto Original**: [9router por decolua](https://github.com/decolua/9router)
|
||||
|
||||
---
|
||||
@@ -1016,7 +957,7 @@ Veja [CONTRIBUTING.md](CONTRIBUTING.md) para diretrizes detalhadas.
|
||||
|
||||
```bash
|
||||
# Crie um release — publicação no npm acontece automaticamente
|
||||
gh release create v1.0.6 --title "v1.0.6" --generate-notes
|
||||
gh release create v0.9.0 --title "v0.9.0" --generate-notes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<img src="./docs/screenshots/MainOmniRoute.png" alt="OmniRoute Dashboard" width="800"/>
|
||||
<img src="./images/omniroute.png" alt="OmniRoute Dashboard" width="800"/>
|
||||
|
||||
# 🚀 OmniRoute — Бесплатный AI Gateway
|
||||
|
||||
@@ -100,7 +100,6 @@ _Подключайте любую IDE или CLI-инструмент с AI ч
|
||||
[](https://hub.docker.com/r/diegosouzapw/omniroute)
|
||||
[](https://github.com/diegosouzapw/OmniRoute/blob/main/LICENSE)
|
||||
[](https://omniroute.online)
|
||||
[](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
|
||||
[🌐 Сайт](https://omniroute.online) • [🚀 Быстрый старт](#-быстрый-старт) • [💡 Функции](#-основные-функции) • [📖 Документация](#-документация) • [💰 Цены](#-обзор-цен)
|
||||
|
||||
@@ -243,7 +242,7 @@ docker compose --profile cli up -d
|
||||
| Образ | Тег | Размер | Описание |
|
||||
| ------------------------ | -------- | ------ | -------------------------- |
|
||||
| `diegosouzapw/omniroute` | `latest` | ~250MB | Последний стабильный релиз |
|
||||
| `diegosouzapw/omniroute` | `1.0.6` | ~250MB | Текущая версия |
|
||||
| `diegosouzapw/omniroute` | `0.9.0` | ~250MB | Текущая версия |
|
||||
|
||||
---
|
||||
|
||||
@@ -893,7 +892,7 @@ OmniRoute включает встроенный фреймворк оценки
|
||||
**Тест подключения показывает «Invalid» для OpenAI-совместимых провайдеров**
|
||||
|
||||
- Многие провайдеры не предоставляют endpoint `/models`
|
||||
- OmniRoute v1.0.6+ включает fallback-валидацию через chat completions
|
||||
- OmniRoute v0.9.0+ включает fallback-валидацию через chat completions
|
||||
- Убедитесь что base URL содержит суффикс `/v1`
|
||||
|
||||
</details>
|
||||
@@ -903,7 +902,7 @@ OmniRoute включает встроенный фреймворк оценки
|
||||
## 🛠️ Технологический стек
|
||||
|
||||
- **Runtime**: Node.js 20+
|
||||
- **Язык**: TypeScript 5.9 — **100% TypeScript** в `src/` и `open-sse/` (v1.0.6)
|
||||
- **Язык**: TypeScript 5.9 — **100% TypeScript** в `src/` и `open-sse/` (v0.9.0)
|
||||
- **Framework**: Next.js 16 + React 19 + Tailwind CSS 4
|
||||
- **База данных**: LowDB (JSON) + SQLite (состояние домена + proxy-логи)
|
||||
- **Стриминг**: Server-Sent Events (SSE)
|
||||
@@ -933,12 +932,9 @@ OmniRoute включает встроенный фреймворк оценки
|
||||
|
||||
## 📧 Поддержка
|
||||
|
||||
> 💬 **Присоединяйтесь к сообществу!** [Группа WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — Получайте помощь, делитесь советами и оставайтесь в курсе.
|
||||
|
||||
- **Сайт**: [omniroute.online](https://omniroute.online)
|
||||
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
|
||||
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
|
||||
- **WhatsApp**: [Группа сообщества](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
- **Оригинальный проект**: [9router от decolua](https://github.com/decolua/9router)
|
||||
|
||||
---
|
||||
@@ -961,7 +957,7 @@ OmniRoute включает встроенный фреймворк оценки
|
||||
|
||||
```bash
|
||||
# Создайте релиз — публикация в npm происходит автоматически
|
||||
gh release create v1.0.6 --title "v1.0.6" --generate-notes
|
||||
gh release create v0.9.0 --title "v0.9.0" --generate-notes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<img src="./docs/screenshots/MainOmniRoute.png" alt="OmniRoute Dashboard" width="800"/>
|
||||
<img src="./images/omniroute.png" alt="OmniRoute Dashboard" width="800"/>
|
||||
|
||||
# 🚀 OmniRoute — 免费 AI 网关
|
||||
|
||||
@@ -100,7 +100,6 @@ _通过 OmniRoute 连接任何 AI 驱动的 IDE 或 CLI 工具 — 免费 API
|
||||
[](https://hub.docker.com/r/diegosouzapw/omniroute)
|
||||
[](https://github.com/diegosouzapw/OmniRoute/blob/main/LICENSE)
|
||||
[](https://omniroute.online)
|
||||
[](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
|
||||
[🌐 网站](https://omniroute.online) • [🚀 快速开始](#-快速开始) • [💡 功能特性](#-核心功能) • [📖 文档](#-文档) • [💰 定价](#-定价概览)
|
||||
|
||||
@@ -243,7 +242,7 @@ docker compose --profile cli up -d
|
||||
| 镜像 | 标签 | 大小 | 描述 |
|
||||
| ------------------------ | -------- | ------ | ---------- |
|
||||
| `diegosouzapw/omniroute` | `latest` | ~250MB | 最新稳定版 |
|
||||
| `diegosouzapw/omniroute` | `1.0.6` | ~250MB | 当前版本 |
|
||||
| `diegosouzapw/omniroute` | `0.9.0` | ~250MB | 当前版本 |
|
||||
|
||||
---
|
||||
|
||||
@@ -893,7 +892,7 @@ OmniRoute 包含内置评估框架,用于针对黄金集测试 LLM 响应质
|
||||
**兼容 OpenAI 的提供商连接测试显示 "Invalid"**
|
||||
|
||||
- 许多提供商不暴露 `/models` 端点
|
||||
- OmniRoute v1.0.6+ 包含通过 chat completions 的回退验证
|
||||
- OmniRoute v0.9.0+ 包含通过 chat completions 的回退验证
|
||||
- 确保 base URL 包含 `/v1` 后缀
|
||||
|
||||
</details>
|
||||
@@ -903,7 +902,7 @@ OmniRoute 包含内置评估框架,用于针对黄金集测试 LLM 响应质
|
||||
## 🛠️ 技术栈
|
||||
|
||||
- **运行时**: Node.js 20+
|
||||
- **语言**: TypeScript 5.9 — `src/` 和 `open-sse/` 中 **100% TypeScript**(v1.0.6)
|
||||
- **语言**: TypeScript 5.9 — `src/` 和 `open-sse/` 中 **100% TypeScript**(v0.9.0)
|
||||
- **框架**: Next.js 16 + React 19 + Tailwind CSS 4
|
||||
- **数据库**: LowDB (JSON) + SQLite(领域状态 + 代理日志)
|
||||
- **流式传输**: Server-Sent Events (SSE)
|
||||
@@ -933,12 +932,9 @@ OmniRoute 包含内置评估框架,用于针对黄金集测试 LLM 响应质
|
||||
|
||||
## 📧 支持
|
||||
|
||||
> 💬 **加入我们的社区!** [WhatsApp 群组](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) — 获取帮助、分享技巧、了解最新动态。
|
||||
|
||||
- **网站**: [omniroute.online](https://omniroute.online)
|
||||
- **GitHub**: [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
|
||||
- **Issues**: [github.com/diegosouzapw/OmniRoute/issues](https://github.com/diegosouzapw/OmniRoute/issues)
|
||||
- **WhatsApp**: [社区群组](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
- **原始项目**: [decolua 的 9router](https://github.com/decolua/9router)
|
||||
|
||||
---
|
||||
@@ -961,7 +957,7 @@ OmniRoute 包含内置评估框架,用于针对黄金集测试 LLM 响应质
|
||||
|
||||
```bash
|
||||
# 创建发布 — npm 发布自动完成
|
||||
gh release create v1.0.6 --title "v1.0.6" --generate-notes
|
||||
gh release create v0.9.0 --title "v0.9.0" --generate-notes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -20,9 +20,9 @@ If you discover a security vulnerability in OmniRoute, please report it responsi
|
||||
|
||||
| Version | Support Status |
|
||||
| ------- | -------------- |
|
||||
| 1.0.x | ✅ Active |
|
||||
| 0.8.x | ✅ Security |
|
||||
| < 0.8.0 | ❌ Unsupported |
|
||||
| 0.8.x | ✅ Active |
|
||||
| 0.7.x | ✅ Security |
|
||||
| < 0.7.0 | ❌ Unsupported |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -81,26 +81,17 @@ console.log(`
|
||||
\\____/|_| |_| |_|_| |_|_|_| \\_\\___/ \\__,_|\\__\\___|
|
||||
\x1b[0m`);
|
||||
|
||||
// ── Node.js version check ──────────────────────────────────
|
||||
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
||||
if (nodeMajor >= 24) {
|
||||
console.warn(`\x1b[33m ⚠ Warning: You are running Node.js ${process.versions.node}.
|
||||
OmniRoute uses better-sqlite3, a native addon that does not yet
|
||||
have compatible prebuilt binaries for Node.js 24+.
|
||||
You may experience errors like "is not a valid Win32 application"
|
||||
or "NODE_MODULE_VERSION mismatch".
|
||||
|
||||
Recommended: use Node.js 22 LTS (or 20 LTS).
|
||||
Workaround: npm rebuild better-sqlite3\x1b[0m
|
||||
`);
|
||||
}
|
||||
|
||||
// ── Resolve server entry ───────────────────────────────────
|
||||
const serverJs = join(APP_DIR, "server.js");
|
||||
|
||||
if (!existsSync(serverJs)) {
|
||||
console.error("\x1b[31m✖ Server not found at:\x1b[0m", serverJs);
|
||||
console.error(" This usually means the package was not built correctly.");
|
||||
console.error(
|
||||
"\x1b[31m✖ Server not found at:\x1b[0m",
|
||||
serverJs,
|
||||
);
|
||||
console.error(
|
||||
" This usually means the package was not built correctly.",
|
||||
);
|
||||
console.error(" Try reinstalling: npm install -g omniroute");
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -128,10 +119,7 @@ server.stdout.on("data", (data) => {
|
||||
process.stdout.write(text);
|
||||
|
||||
// Detect server ready
|
||||
if (
|
||||
!started &&
|
||||
(text.includes("Ready") || text.includes("started") || text.includes("listening"))
|
||||
) {
|
||||
if (!started && (text.includes("Ready") || text.includes("started") || text.includes("listening"))) {
|
||||
started = true;
|
||||
onReady();
|
||||
}
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
x-common: &common
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
- DATA_DIR=/app/data # Must match the volume mount below
|
||||
volumes:
|
||||
- omniroute-data:/app/data
|
||||
healthcheck:
|
||||
|
||||
@@ -227,17 +227,6 @@ Response example:
|
||||
| `/api/monitoring/health` | GET | Health check |
|
||||
| `/api/cache` | GET/DELETE | Cache stats / clear |
|
||||
|
||||
### Backup & Export/Import
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
| --------------------------- | ------ | --------------------------------------- |
|
||||
| `/api/db-backups` | GET | List available backups |
|
||||
| `/api/db-backups` | PUT | Create a manual backup |
|
||||
| `/api/db-backups` | POST | Restore from a specific backup |
|
||||
| `/api/db-backups/export` | GET | Download database as .sqlite file |
|
||||
| `/api/db-backups/import` | POST | Upload .sqlite file to replace database |
|
||||
| `/api/db-backups/exportAll` | GET | Download full backup as .tar.gz archive |
|
||||
|
||||
### Cloud Sync
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# OmniRoute Architecture
|
||||
|
||||
_Last updated: 2026-02-18_
|
||||
_Last updated: 2026-02-17_
|
||||
|
||||
## Executive Summary
|
||||
|
||||
@@ -17,9 +17,6 @@ Core capabilities:
|
||||
- Embedding generation via `/v1/embeddings` (6 providers, 9 models)
|
||||
- Image generation via `/v1/images/generations` (4 providers, 9 models)
|
||||
- Think tag parsing (`<think>...</think>`) for reasoning models
|
||||
- Response sanitization for strict OpenAI SDK compatibility
|
||||
- Role normalization (developer→system, system→user) for cross-provider compatibility
|
||||
- Structured output conversion (json_schema → Gemini responseSchema)
|
||||
- Local persistence for providers, keys, aliases, combos, settings, pricing
|
||||
- Usage/cost tracking and request logging
|
||||
- Optional cloud sync for multi-device/state sync
|
||||
@@ -183,8 +180,6 @@ Main flow modules:
|
||||
- Embedding provider registry: `open-sse/config/embeddingRegistry.ts`
|
||||
- Image generation handler: `open-sse/handlers/imageGeneration.ts`
|
||||
- Image provider registry: `open-sse/config/imageRegistry.ts`
|
||||
- Response sanitization: `open-sse/handlers/responseSanitizer.ts`
|
||||
- Role normalization: `open-sse/services/roleNormalizer.ts`
|
||||
|
||||
Services (business logic):
|
||||
|
||||
@@ -652,13 +647,6 @@ Source Format → OpenAI (hub) → Target Format
|
||||
|
||||
Translations are selected dynamically based on source payload shape and provider target format.
|
||||
|
||||
Additional processing layers in the translation pipeline:
|
||||
|
||||
- **Response sanitization** — Strips non-standard fields from OpenAI-format responses (both streaming and non-streaming) to ensure strict SDK compliance
|
||||
- **Role normalization** — Converts `developer` → `system` for non-OpenAI targets; merges `system` → `user` for models that reject the system role (GLM, ERNIE)
|
||||
- **Think tag extraction** — Parses `<think>...</think>` blocks from content into `reasoning_content` field
|
||||
- **Structured output** — Converts OpenAI `response_format.json_schema` to Gemini's `responseMimeType` + `responseSchema`
|
||||
|
||||
## Supported API Endpoints
|
||||
|
||||
| Endpoint | Format | Handler |
|
||||
@@ -771,8 +759,8 @@ Environment variables actively used by code:
|
||||
|
||||
## Operational Verification Checklist
|
||||
|
||||
- Build from source: `npm run build`
|
||||
- Build Docker image: `docker build -t omniroute .`
|
||||
- Build from source: `cd /root/dev/omniroute && npm run build`
|
||||
- Build Docker image: `cd /root/dev/omniroute && docker build -t omniroute .`
|
||||
- Start service and verify:
|
||||
- `GET /api/settings`
|
||||
- `GET /api/v1/models`
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
# OmniRoute — Dashboard Features Gallery
|
||||
|
||||
Visual guide to every section of the OmniRoute dashboard.
|
||||
|
||||
---
|
||||
|
||||
## 🔌 Providers
|
||||
|
||||
Manage AI provider connections: OAuth providers (Claude Code, Codex, Gemini CLI), API key providers (Groq, DeepSeek, OpenRouter), and free providers (iFlow, Qwen, Kiro).
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🎨 Combos
|
||||
|
||||
Create model routing combos with 6 strategies: fill-first, round-robin, power-of-two-choices, random, least-used, and cost-optimized. Each combo chains multiple models with automatic fallback.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 📊 Analytics
|
||||
|
||||
Comprehensive usage analytics with token consumption, cost estimates, activity heatmaps, weekly distribution charts, and per-provider breakdowns.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🏥 System Health
|
||||
|
||||
Real-time monitoring: uptime, memory, version, latency percentiles (p50/p95/p99), cache statistics, and provider circuit breaker states.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🔧 Translator Playground
|
||||
|
||||
Four modes for debugging API translations: **Playground** (format converter), **Chat Tester** (live requests), **Test Bench** (batch tests), and **Live Monitor** (real-time stream).
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Settings
|
||||
|
||||
General settings, system storage, backup management (export/import database), appearance (dark/light mode), security (includes API endpoint protection and custom provider blocking), routing, resilience, and advanced configuration.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🔧 CLI Tools
|
||||
|
||||
One-click configuration for AI coding tools: Claude Code, Codex CLI, Gemini CLI, OpenClaw, Kilo Code, and Antigravity.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 📝 Request Logs
|
||||
|
||||
Real-time request logging with filtering by provider, model, account, and API key. Shows status codes, token usage, latency, and response details.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🌐 API Endpoint
|
||||
|
||||
Your unified API endpoint with capability breakdown: Chat Completions, Embeddings, Image Generation, Reranking, Audio Transcription, and registered API keys.
|
||||
|
||||

|
||||
@@ -177,10 +177,6 @@ Use **Dashboard → Translator** to debug format translation issues:
|
||||
- **Thinking tags not appearing** — Check if the target provider supports thinking and the thinking budget setting
|
||||
- **Tool calls dropping** — Some format translations may strip unsupported fields; verify in Playground mode
|
||||
- **System prompt missing** — Claude and Gemini handle system prompts differently; check translation output
|
||||
- **SDK returns raw string instead of object** — Fixed in v1.1.0: response sanitizer now strips non-standard fields (`x_groq`, `usage_breakdown`, etc.) that cause OpenAI SDK Pydantic validation failures
|
||||
- **GLM/ERNIE rejects `system` role** — Fixed in v1.1.0: role normalizer automatically merges system messages into user messages for incompatible models
|
||||
- **`developer` role not recognized** — Fixed in v1.1.0: automatically converted to `system` for non-OpenAI providers
|
||||
- **`json_schema` not working with Gemini** — Fixed in v1.1.0: `response_format` is now converted to Gemini's `responseMimeType` + `responseSchema`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -572,45 +572,13 @@ OmniRoute implements provider-level resilience with four components:
|
||||
|
||||
---
|
||||
|
||||
### Database Export / Import
|
||||
|
||||
Manage database backups in **Dashboard → Settings → System & Storage**.
|
||||
|
||||
| Action | Description |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **Export Database** | Downloads the current SQLite database as a `.sqlite` file |
|
||||
| **Export All (.tar.gz)** | Downloads a full backup archive including: database, settings, combos, provider connections (no credentials), API key metadata |
|
||||
| **Import Database** | Upload a `.sqlite` file to replace the current database. A pre-import backup is automatically created |
|
||||
|
||||
```bash
|
||||
# API: Export database
|
||||
curl -o backup.sqlite http://localhost:20128/api/db-backups/export
|
||||
|
||||
# API: Export all (full archive)
|
||||
curl -o backup.tar.gz http://localhost:20128/api/db-backups/exportAll
|
||||
|
||||
# API: Import database
|
||||
curl -X POST http://localhost:20128/api/db-backups/import \
|
||||
-F "file=@backup.sqlite"
|
||||
```
|
||||
|
||||
**Import Validation:** The imported file is validated for integrity (SQLite pragma check), required tables (`provider_connections`, `provider_nodes`, `combos`, `api_keys`), and size (max 100MB).
|
||||
|
||||
**Use Cases:**
|
||||
|
||||
- Migrate OmniRoute between machines
|
||||
- Create external backups for disaster recovery
|
||||
- Share configurations between team members (export all → share archive)
|
||||
|
||||
---
|
||||
|
||||
### Settings Dashboard
|
||||
|
||||
The settings page is organized into 5 tabs for easy navigation:
|
||||
|
||||
| Tab | Contents |
|
||||
| -------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| **Security** | Login/Password settings, IP Access Control, API auth for `/models`, and Provider Blocking |
|
||||
| **Security** | Login/Password settings and IP Access Control (allowlist/blocklist) |
|
||||
| **Routing** | Global routing strategy (6 options), wildcard model aliases, fallback chains, combo defaults |
|
||||
| **Resilience** | Provider profiles, editable rate limits, circuit breaker status, policies & locked identifiers |
|
||||
| **AI** | Thinking budget configuration, global system prompt injection, prompt cache stats |
|
||||
|
||||
@@ -1,399 +0,0 @@
|
||||
# OmniRoute — Guia de Deploy em VM com Cloudflare
|
||||
|
||||
Guia completo para instalar e configurar o OmniRoute em uma VM (VPS) com domínio gerenciado via Cloudflare.
|
||||
|
||||
---
|
||||
|
||||
## Pré-Requisitos
|
||||
|
||||
| Item | Mínimo | Recomendado |
|
||||
| ----------- | ------------------------ | ---------------- |
|
||||
| **CPU** | 1 vCPU | 2 vCPU |
|
||||
| **RAM** | 1 GB | 2 GB |
|
||||
| **Disco** | 10 GB SSD | 25 GB SSD |
|
||||
| **SO** | Ubuntu 22.04 LTS | Ubuntu 24.04 LTS |
|
||||
| **Domínio** | Registrado no Cloudflare | — |
|
||||
| **Docker** | Docker Engine 24+ | Docker 27+ |
|
||||
|
||||
**Providers testados**: Akamai (Linode), DigitalOcean, Vultr, Hetzner, AWS Lightsail.
|
||||
|
||||
---
|
||||
|
||||
## 1. Configurar a VM
|
||||
|
||||
### 1.1 Criar a instância
|
||||
|
||||
No seu provider de VPS preferido:
|
||||
|
||||
- Escolha Ubuntu 24.04 LTS
|
||||
- Selecione o plano mínimo (1 vCPU / 1 GB RAM)
|
||||
- Defina uma senha forte para root ou configure SSH key
|
||||
- Anote o **IP público** (ex: `203.0.113.10`)
|
||||
|
||||
### 1.2 Conectar via SSH
|
||||
|
||||
```bash
|
||||
ssh root@203.0.113.10
|
||||
```
|
||||
|
||||
### 1.3 Atualizar o sistema
|
||||
|
||||
```bash
|
||||
apt update && apt upgrade -y
|
||||
```
|
||||
|
||||
### 1.4 Instalar Docker
|
||||
|
||||
```bash
|
||||
# Instalar dependências
|
||||
apt install -y ca-certificates curl gnupg
|
||||
|
||||
# Adicionar repositório oficial do Docker
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
chmod a+r /etc/apt/keyrings/docker.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
apt update
|
||||
apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||
```
|
||||
|
||||
### 1.5 Instalar nginx
|
||||
|
||||
```bash
|
||||
apt install -y nginx
|
||||
```
|
||||
|
||||
### 1.6 Configurar Firewall (UFW)
|
||||
|
||||
```bash
|
||||
ufw default deny incoming
|
||||
ufw default allow outgoing
|
||||
ufw allow 22/tcp # SSH
|
||||
ufw allow 80/tcp # HTTP (redirect)
|
||||
ufw allow 443/tcp # HTTPS
|
||||
ufw enable
|
||||
```
|
||||
|
||||
> **Dica**: Para segurança máxima, restrinja as portas 80 e 443 apenas para IPs da Cloudflare. Veja a seção [Segurança Avançada](#segurança-avançada).
|
||||
|
||||
---
|
||||
|
||||
## 2. Instalar o OmniRoute
|
||||
|
||||
### 2.1 Criar diretório de configuração
|
||||
|
||||
```bash
|
||||
mkdir -p /opt/omniroute
|
||||
```
|
||||
|
||||
### 2.2 Criar arquivo de variáveis de ambiente
|
||||
|
||||
```bash
|
||||
cat > /opt/omniroute/.env << 'EOF'
|
||||
# === Segurança ===
|
||||
JWT_SECRET=ALTERE-PARA-CHAVE-SECRETA-UNICA-64-CHARS
|
||||
INITIAL_PASSWORD=SuaSenhaSegura123!
|
||||
API_KEY_SECRET=ALTERE-PARA-OUTRA-CHAVE-SECRETA
|
||||
STORAGE_ENCRYPTION_KEY=ALTERE-PARA-TERCEIRA-CHAVE-SECRETA
|
||||
STORAGE_ENCRYPTION_KEY_VERSION=v1
|
||||
MACHINE_ID_SALT=ALTERE-PARA-SALT-UNICO
|
||||
|
||||
# === App ===
|
||||
PORT=20128
|
||||
NODE_ENV=production
|
||||
HOSTNAME=0.0.0.0
|
||||
DATA_DIR=/app/data
|
||||
STORAGE_DRIVER=sqlite
|
||||
ENABLE_REQUEST_LOGS=true
|
||||
AUTH_COOKIE_SECURE=false
|
||||
REQUIRE_API_KEY=false
|
||||
|
||||
# === Domain (altere para seu domínio) ===
|
||||
BASE_URL=https://llms.seudominio.com
|
||||
NEXT_PUBLIC_BASE_URL=https://llms.seudominio.com
|
||||
|
||||
# === Cloud Sync (opcional) ===
|
||||
# CLOUD_URL=https://cloud.omniroute.online
|
||||
# NEXT_PUBLIC_CLOUD_URL=https://cloud.omniroute.online
|
||||
EOF
|
||||
```
|
||||
|
||||
> ⚠️ **IMPORTANTE**: Gere chaves secretas únicas! Use `openssl rand -hex 32` para cada chave.
|
||||
|
||||
### 2.3 Iniciar o container
|
||||
|
||||
```bash
|
||||
docker pull diegosouzapw/omniroute:latest
|
||||
|
||||
docker run -d \
|
||||
--name omniroute \
|
||||
--restart unless-stopped \
|
||||
--env-file /opt/omniroute/.env \
|
||||
-p 20128:20128 \
|
||||
-v omniroute-data:/app/data \
|
||||
diegosouzapw/omniroute:latest
|
||||
```
|
||||
|
||||
### 2.4 Verificar se está rodando
|
||||
|
||||
```bash
|
||||
docker ps | grep omniroute
|
||||
docker logs omniroute --tail 20
|
||||
```
|
||||
|
||||
Deve exibir: `[DB] SQLite database ready` e `listening on port 20128`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Configurar nginx (Reverse Proxy)
|
||||
|
||||
### 3.1 Gerar certificado SSL (Cloudflare Origin)
|
||||
|
||||
No painel da Cloudflare:
|
||||
|
||||
1. Vá em **SSL/TLS → Origin Server**
|
||||
2. Clique **Create Certificate**
|
||||
3. Deixe os padrões (15 anos, \*.seudominio.com)
|
||||
4. Copie o **Origin Certificate** e a **Private Key**
|
||||
|
||||
```bash
|
||||
mkdir -p /etc/nginx/ssl
|
||||
|
||||
# Colar o certificado
|
||||
nano /etc/nginx/ssl/origin.crt
|
||||
|
||||
# Colar a chave privada
|
||||
nano /etc/nginx/ssl/origin.key
|
||||
|
||||
chmod 600 /etc/nginx/ssl/origin.key
|
||||
```
|
||||
|
||||
### 3.2 Configuração do nginx
|
||||
|
||||
```bash
|
||||
cat > /etc/nginx/sites-available/omniroute << 'NGINX'
|
||||
# Default server — bloqueia acesso direto por IP
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
listen 443 ssl default_server;
|
||||
listen [::]:443 ssl default_server;
|
||||
ssl_certificate /etc/nginx/ssl/origin.crt;
|
||||
ssl_certificate_key /etc/nginx/ssl/origin.key;
|
||||
server_name _;
|
||||
return 444;
|
||||
}
|
||||
|
||||
# OmniRoute — HTTPS
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name llms.seudominio.com; # Altere para seu domínio
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/origin.crt;
|
||||
ssl_certificate_key /etc/nginx/ssl/origin.key;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
|
||||
client_max_body_size 100M;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:20128;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket support
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
# SSE (Server-Sent Events) — streaming AI responses
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTP → HTTPS redirect
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name llms.seudominio.com;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
NGINX
|
||||
```
|
||||
|
||||
### 3.3 Ativar e testar
|
||||
|
||||
```bash
|
||||
# Remover config padrão
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
|
||||
# Ativar OmniRoute
|
||||
ln -sf /etc/nginx/sites-available/omniroute /etc/nginx/sites-enabled/omniroute
|
||||
|
||||
# Testar e recarregar
|
||||
nginx -t && systemctl reload nginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Configurar Cloudflare DNS
|
||||
|
||||
### 4.1 Adicionar registro DNS
|
||||
|
||||
No painel da Cloudflare → DNS:
|
||||
|
||||
| Type | Name | Content | Proxy |
|
||||
| ---- | ------ | ------------------------- | ---------- |
|
||||
| A | `llms` | `203.0.113.10` (IP da VM) | ✅ Proxied |
|
||||
|
||||
### 4.2 Configurar SSL
|
||||
|
||||
Em **SSL/TLS → Overview**:
|
||||
|
||||
- Modo: **Full (Strict)**
|
||||
|
||||
Em **SSL/TLS → Edge Certificates**:
|
||||
|
||||
- Always Use HTTPS: ✅ On
|
||||
- Minimum TLS Version: TLS 1.2
|
||||
- Automatic HTTPS Rewrites: ✅ On
|
||||
|
||||
### 4.3 Testar
|
||||
|
||||
```bash
|
||||
curl -sI https://llms.seudominio.com/health
|
||||
# Deve retornar HTTP/2 200
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Operações e Manutenção
|
||||
|
||||
### Atualizar para nova versão
|
||||
|
||||
```bash
|
||||
docker pull diegosouzapw/omniroute:latest
|
||||
docker stop omniroute && docker rm omniroute
|
||||
docker run -d --name omniroute --restart unless-stopped \
|
||||
--env-file /opt/omniroute/.env \
|
||||
-p 20128:20128 \
|
||||
-v omniroute-data:/app/data \
|
||||
diegosouzapw/omniroute:latest
|
||||
```
|
||||
|
||||
### Ver logs
|
||||
|
||||
```bash
|
||||
docker logs -f omniroute # Stream em tempo real
|
||||
docker logs omniroute --tail 50 # Últimas 50 linhas
|
||||
```
|
||||
|
||||
### Backup manual do banco
|
||||
|
||||
```bash
|
||||
# Copiar dados do volume para o host
|
||||
docker cp omniroute:/app/data ./backup-$(date +%F)
|
||||
|
||||
# Ou comprimir todo o volume
|
||||
docker run --rm -v omniroute-data:/data -v $(pwd):/backup \
|
||||
alpine tar czf /backup/omniroute-data-$(date +%F).tar.gz /data
|
||||
```
|
||||
|
||||
### Restaurar de backup
|
||||
|
||||
```bash
|
||||
docker stop omniroute
|
||||
docker run --rm -v omniroute-data:/data -v $(pwd):/backup \
|
||||
alpine sh -c "rm -rf /data/* && tar xzf /backup/omniroute-data-YYYY-MM-DD.tar.gz -C /"
|
||||
docker start omniroute
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Segurança Avançada
|
||||
|
||||
### Restringir nginx para Cloudflare IPs
|
||||
|
||||
```bash
|
||||
cat > /etc/nginx/cloudflare-ips.conf << 'CF'
|
||||
# Cloudflare IPv4 ranges — atualizar periodicamente
|
||||
# https://www.cloudflare.com/ips-v4/
|
||||
set_real_ip_from 173.245.48.0/20;
|
||||
set_real_ip_from 103.21.244.0/22;
|
||||
set_real_ip_from 103.22.200.0/22;
|
||||
set_real_ip_from 103.31.4.0/22;
|
||||
set_real_ip_from 141.101.64.0/18;
|
||||
set_real_ip_from 108.162.192.0/18;
|
||||
set_real_ip_from 190.93.240.0/20;
|
||||
set_real_ip_from 188.114.96.0/20;
|
||||
set_real_ip_from 197.234.240.0/22;
|
||||
set_real_ip_from 198.41.128.0/17;
|
||||
set_real_ip_from 162.158.0.0/15;
|
||||
set_real_ip_from 104.16.0.0/13;
|
||||
set_real_ip_from 104.24.0.0/14;
|
||||
set_real_ip_from 172.64.0.0/13;
|
||||
set_real_ip_from 131.0.72.0/22;
|
||||
real_ip_header CF-Connecting-IP;
|
||||
CF
|
||||
```
|
||||
|
||||
Adicionar no `nginx.conf` dentro do bloco `http {}`:
|
||||
|
||||
```nginx
|
||||
include /etc/nginx/cloudflare-ips.conf;
|
||||
```
|
||||
|
||||
### Install fail2ban
|
||||
|
||||
```bash
|
||||
apt install -y fail2ban
|
||||
systemctl enable fail2ban
|
||||
systemctl start fail2ban
|
||||
|
||||
# Verificar status
|
||||
fail2ban-client status sshd
|
||||
```
|
||||
|
||||
### Bloquear acesso direto na porta do Docker
|
||||
|
||||
```bash
|
||||
# Impedir acesso externo direto à porta 20128
|
||||
iptables -I DOCKER-USER -p tcp --dport 20128 -j DROP
|
||||
iptables -I DOCKER-USER -i lo -p tcp --dport 20128 -j ACCEPT
|
||||
|
||||
# Persistir as regras
|
||||
apt install -y iptables-persistent
|
||||
netfilter-persistent save
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Deploy do Cloud Worker (Opcional)
|
||||
|
||||
Para acesso remoto via Cloudflare Workers (sem expor a VM diretamente):
|
||||
|
||||
```bash
|
||||
# No repositório local
|
||||
cd omnirouteCloud
|
||||
npm install
|
||||
npx wrangler login
|
||||
npx wrangler deploy
|
||||
```
|
||||
|
||||
Ver documentação completa em [omnirouteCloud/README.md](../omnirouteCloud/README.md).
|
||||
|
||||
---
|
||||
|
||||
## Resumo de Portas
|
||||
|
||||
| Porta | Serviço | Acesso |
|
||||
| ----- | ----------- | ----------------------------- |
|
||||
| 22 | SSH | Público (com fail2ban) |
|
||||
| 80 | nginx HTTP | Redirect → HTTPS |
|
||||
| 443 | nginx HTTPS | Via Cloudflare Proxy |
|
||||
| 20128 | OmniRoute | Somente localhost (via nginx) |
|
||||
@@ -1,37 +0,0 @@
|
||||
# ADR-001: Next.js as the Foundation for an AI Gateway
|
||||
|
||||
## Status: Accepted
|
||||
|
||||
## Context
|
||||
|
||||
OmniRoute is an AI routing gateway that translates, forwards, and manages requests across 20+ LLM providers. We needed a framework that could serve both the API proxy layer and a management dashboard from a single codebase.
|
||||
|
||||
**Alternatives considered:**
|
||||
|
||||
- **Express.js only** — Simpler proxy, but requires separate frontend tooling
|
||||
- **Fastify** — Fast, but no built-in SSR/dashboard support
|
||||
- **Next.js** — Unified full-stack framework with API routes, SSR, and static pages
|
||||
|
||||
## Decision
|
||||
|
||||
We chose Next.js because:
|
||||
|
||||
1. **Single deployment** — API routes (`/api/*`) and dashboard UI in one process
|
||||
2. **Middleware layer** — Native request interception for auth guards and request tracing
|
||||
3. **File-based routing** — Easy to map provider endpoints to handlers
|
||||
4. **Built-in TypeScript** — Type safety across the entire codebase
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
|
||||
- One `npm run build` produces both API and UI
|
||||
- Middleware provides centralized auth and request tracing
|
||||
- Dashboard gets automatic code splitting and optimization
|
||||
|
||||
**Negative:**
|
||||
|
||||
- Next.js middleware has limitations (no heavy imports, edge runtime constraints)
|
||||
- Serverless deployment model doesn't align with persistent WebSocket/SSE connections
|
||||
- Build times are longer than Express-only setups
|
||||
- The SSE proxy layer (`open-sse/`) operates outside Next.js conventions
|
||||
@@ -1,37 +0,0 @@
|
||||
# ADR-002: Hub-and-Spoke Translation with OpenAI as Intermediate Format
|
||||
|
||||
## Status: Accepted
|
||||
|
||||
## Context
|
||||
|
||||
OmniRoute routes requests across 20+ providers, each with its own API format (OpenAI, Anthropic Messages, Google Gemini, AWS Bedrock, etc.). Direct provider-to-provider translation would require O(n²) translators.
|
||||
|
||||
**Alternatives considered:**
|
||||
|
||||
- **Direct translation** — Each pair needs a dedicated translator (n² complexity)
|
||||
- **Common intermediate format** — Translate to/from a canonical format (2n complexity)
|
||||
- **Protocol buffers** — Strong typing but heavy overhead for a proxy
|
||||
|
||||
## Decision
|
||||
|
||||
We use the **OpenAI Chat Completions format** as the canonical intermediate representation. All incoming requests are normalized to OpenAI format, processed, then translated to the target provider's format.
|
||||
|
||||
```
|
||||
Client → [any format] → OpenAI canonical → [target format] → Provider
|
||||
Provider → [response] → OpenAI canonical → [original format] → Client
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
|
||||
- Only 2 translators per provider (inbound + outbound) instead of n² pairs
|
||||
- OpenAI format is the de facto standard — most clients already use it
|
||||
- Adding a new provider requires only implementing one translator pair
|
||||
- Streaming (SSE) works consistently through the canonical format
|
||||
|
||||
**Negative:**
|
||||
|
||||
- Some provider-specific features may be lost in translation
|
||||
- The double translation adds latency (typically < 5ms)
|
||||
- OpenAI format changes require updating the canonical representation
|
||||
@@ -1,39 +0,0 @@
|
||||
# ADR-003: Dual Storage — SQLite Primary with JSON Migration Path
|
||||
|
||||
## Status: Accepted
|
||||
|
||||
## Context
|
||||
|
||||
OmniRoute originally used LowDB (JSON file) for all persistence. As the project grew, JSON-based storage became a bottleneck for concurrent access, querying, and data integrity.
|
||||
|
||||
**Alternatives considered:**
|
||||
|
||||
- **LowDB only** — Simple but no concurrent access, no ACID, no querying
|
||||
- **SQLite only** — Fast, ACID-compliant, but breaks existing deployments
|
||||
- **PostgreSQL** — Production-grade but requires external dependency
|
||||
- **Dual storage with migration** — SQLite primary + automatic JSON migration
|
||||
|
||||
## Decision
|
||||
|
||||
We migrated to **SQLite as the primary store** with an automatic one-time migration from `db.json`:
|
||||
|
||||
1. On startup, if `db.json` exists and SQLite is empty, auto-migrate all data
|
||||
2. All new reads/writes go through SQLite
|
||||
3. The `db.json` file is preserved but no longer written to
|
||||
|
||||
Settings remain in a hybrid model where LowDB handles simple key-value configuration for backward compatibility.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
|
||||
- ACID transactions for provider connections, API keys, and usage data
|
||||
- Proper SQL queries for analytics and log filtering
|
||||
- Concurrent read/write safety via WAL mode
|
||||
- Zero-downtime migration from JSON — users upgrade transparently
|
||||
|
||||
**Negative:**
|
||||
|
||||
- Two storage engines to maintain (SQLite + LowDB for settings)
|
||||
- Migration code must handle edge cases and partial data
|
||||
- SQLite binary dependency needed in deployment environments
|
||||
@@ -1,36 +0,0 @@
|
||||
# Task 01 — Home Page (Dashboard)
|
||||
|
||||
**Status:** `[ ]` Não iniciado
|
||||
**Namespace JSON:** `home`
|
||||
|
||||
## Arquivos
|
||||
| Arquivo | Linhas | Strings |
|
||||
|---------|--------|---------|
|
||||
| `src/app/(dashboard)/dashboard/page.tsx` | 17 | 0 (wrapper) |
|
||||
| `src/app/(dashboard)/dashboard/HomePageClient.tsx` | 500 | ~25 |
|
||||
|
||||
## Strings a Traduzir
|
||||
|
||||
### HomePageClient.tsx
|
||||
| Linha | String EN | Chave i18n | String PT-BR |
|
||||
|-------|-----------|------------|--------------|
|
||||
| 138 | "Quick Start" | `home.quickStart` | "Início Rápido" |
|
||||
| 242 | "Providers Overview" | `home.providersOverview` | "Visão Geral dos Provedores" |
|
||||
| 436 | "No models available for this provider." | `home.noModelsAvailable` | "Nenhum modelo disponível para este provedor." |
|
||||
| — | "Total Requests" | `home.totalRequests` | "Total de Requisições" |
|
||||
| — | "Active Providers" | `home.activeProviders` | "Provedores Ativos" |
|
||||
| — | "Success Rate" | `home.successRate` | "Taxa de Sucesso" |
|
||||
| — | "Avg Latency" | `home.avgLatency` | "Latência Média" |
|
||||
| — | "Configure Endpoint" | `home.configureEndpoint` | "Configurar Endpoint" |
|
||||
| — | "Add Provider" | `home.addProvider` | "Adicionar Provedor" |
|
||||
| — | "View Docs" | `home.viewDocs` | "Ver Documentação" |
|
||||
| — | "Copied!" | `common.copied` | ✅ já existe |
|
||||
| — | "requests" | `home.requests` | "requisições" |
|
||||
| — | "models" | `home.models` | "modelos" |
|
||||
| — | "accounts" | `home.accounts` | "contas" |
|
||||
|
||||
## Checklist
|
||||
- [ ] Adicionar chaves no `en.json` (namespace `home`)
|
||||
- [ ] Adicionar traduções no `pt-BR.json`
|
||||
- [ ] Substituir strings por `t()` no `HomePageClient.tsx`
|
||||
- [ ] Testar em EN e PT-BR
|
||||
@@ -1,25 +0,0 @@
|
||||
# Task 02 — Analytics Page
|
||||
|
||||
**Status:** `[ ]` Não iniciado
|
||||
**Namespace JSON:** `analytics`
|
||||
|
||||
## Arquivos
|
||||
| Arquivo | Linhas | Strings |
|
||||
|---------|--------|---------|
|
||||
| `src/app/(dashboard)/dashboard/analytics/page.tsx` | 46 | ~8 |
|
||||
|
||||
## Strings a Traduzir
|
||||
|
||||
| Linha | String EN | Chave i18n | String PT-BR |
|
||||
|-------|-----------|------------|--------------|
|
||||
| 11 | "Monitor your API usage patterns..." | `analytics.overviewDescription` | "Monitore padrões de uso da API..." |
|
||||
| 13 | "Run evaluation suites to test..." | `analytics.evalsDescription` | "Execute suítes de avaliação..." |
|
||||
| 24 | "Analytics" | `analytics.title` | "Análises" |
|
||||
| 32 | "Overview" | `analytics.overview` | "Visão Geral" |
|
||||
| 33 | "Evals" | `analytics.evals` | "Avaliações" |
|
||||
|
||||
## Checklist
|
||||
- [ ] Adicionar chaves no `en.json`
|
||||
- [ ] Adicionar traduções no `pt-BR.json`
|
||||
- [ ] Substituir strings por `t()` em `page.tsx`
|
||||
- [ ] Testar em EN e PT-BR
|
||||
@@ -1,31 +0,0 @@
|
||||
# Task 03 — API Manager Page
|
||||
|
||||
**Status:** `[ ]` Não iniciado
|
||||
**Namespace JSON:** `apiManager`
|
||||
|
||||
## Arquivos
|
||||
| Arquivo | Linhas | Strings |
|
||||
|---------|--------|---------|
|
||||
| `src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx` | ~400 | ~20 |
|
||||
|
||||
## Strings a Traduzir (levantamento parcial — abrir código para completar)
|
||||
|
||||
| String EN | Chave i18n | String PT-BR |
|
||||
|-----------|------------|--------------|
|
||||
| "API Keys" | `apiManager.title` | "Chaves de API" |
|
||||
| "Create API Key" | `apiManager.createKey` | "Criar Chave de API" |
|
||||
| "Name" | `apiManager.name` | "Nome" |
|
||||
| "Key" | `apiManager.key` | "Chave" |
|
||||
| "Created" | `apiManager.created` | "Criado em" |
|
||||
| "Last Used" | `apiManager.lastUsed` | "Último Uso" |
|
||||
| "Actions" | `apiManager.actions` | "Ações" |
|
||||
| "Delete" | `common.delete` | ✅ já existe |
|
||||
| "No API keys found" | `apiManager.noKeys` | "Nenhuma chave de API encontrada" |
|
||||
| ~11 strings adicionais | — | Levantar no código |
|
||||
|
||||
## Checklist
|
||||
- [ ] Levantar todas as strings do `ApiManagerPageClient.tsx`
|
||||
- [ ] Adicionar chaves no `en.json`
|
||||
- [ ] Adicionar traduções no `pt-BR.json`
|
||||
- [ ] Substituir strings por `t()`
|
||||
- [ ] Testar em EN e PT-BR
|
||||
@@ -1,31 +0,0 @@
|
||||
# Task 04 — Audit Log Page
|
||||
|
||||
**Status:** `[ ]` Não iniciado
|
||||
**Namespace JSON:** `auditLog`
|
||||
|
||||
## Arquivos
|
||||
| Arquivo | Linhas | Strings |
|
||||
|---------|--------|---------|
|
||||
| `src/app/(dashboard)/dashboard/audit-log/page.tsx` | 241 | ~12 |
|
||||
| `src/app/(dashboard)/dashboard/logs/AuditLogTab.tsx` | ~100 | ~3 |
|
||||
|
||||
## Strings a Traduzir
|
||||
|
||||
| String EN | Chave i18n | String PT-BR |
|
||||
|-----------|------------|--------------|
|
||||
| "Audit Log" | `auditLog.title` | "Log de Auditoria" |
|
||||
| "Search actions..." | `auditLog.searchPlaceholder` | "Buscar ações..." |
|
||||
| "Action" | `auditLog.action` | "Ação" |
|
||||
| "Actor" | `auditLog.actor` | "Autor" |
|
||||
| "Target" | `auditLog.target` | "Alvo" |
|
||||
| "Details" | `auditLog.details` | "Detalhes" |
|
||||
| "IP Address" | `auditLog.ipAddress` | "Endereço IP" |
|
||||
| "Timestamp" | `auditLog.timestamp` | "Data/Hora" |
|
||||
| "No audit entries found" | `auditLog.noEntries` | "Nenhum registro de auditoria" |
|
||||
| "Load More" | `auditLog.loadMore` | "Carregar Mais" |
|
||||
|
||||
## Checklist
|
||||
- [ ] Adicionar chaves no `en.json`
|
||||
- [ ] Adicionar traduções no `pt-BR.json`
|
||||
- [ ] Substituir strings em `audit-log/page.tsx` e `AuditLogTab.tsx`
|
||||
- [ ] Testar em EN e PT-BR
|
||||
@@ -1,37 +0,0 @@
|
||||
# Task 05 — CLI Tools Page
|
||||
|
||||
**Status:** `[ ]` Não iniciado
|
||||
**Namespace JSON:** `cliTools`
|
||||
|
||||
## Arquivos
|
||||
| Arquivo | Strings |
|
||||
|---------|---------|
|
||||
| `cli-tools/components/AntigravityToolCard.tsx` | ~3 |
|
||||
| `cli-tools/components/ClaudeToolCard.tsx` | ~3 |
|
||||
| `cli-tools/components/ClineToolCard.tsx` | ~4 |
|
||||
| `cli-tools/components/CodexToolCard.tsx` | ~3 |
|
||||
| `cli-tools/components/DefaultToolCard.tsx` | ~3 |
|
||||
| `cli-tools/components/DroidToolCard.tsx` | ~2 |
|
||||
| `cli-tools/components/KiloToolCard.tsx` | ~4 |
|
||||
| `cli-tools/components/OpenClawToolCard.tsx` | ~2 |
|
||||
|
||||
## Strings comuns entre Tool Cards
|
||||
|
||||
| String EN | Chave i18n | String PT-BR |
|
||||
|-----------|------------|--------------|
|
||||
| "Status" | `cliTools.status` | "Status" |
|
||||
| "Connected" | `cliTools.connected` | "Conectado" |
|
||||
| "Not Connected" | `cliTools.notConnected` | "Não Conectado" |
|
||||
| "Configure" | `cliTools.configure` | "Configurar" |
|
||||
| "Test Connection" | `cliTools.testConnection` | "Testar Conexão" |
|
||||
| "Models" | `cliTools.models` | "Modelos" |
|
||||
| "Map Models" | `cliTools.mapModels` | "Mapear Modelos" |
|
||||
| "Save" | `common.save` | ✅ já existe |
|
||||
| "Cancel" | `common.cancel` | ✅ já existe |
|
||||
|
||||
## Checklist
|
||||
- [ ] Levantar strings de cada ToolCard
|
||||
- [ ] Adicionar chaves no `en.json`
|
||||
- [ ] Adicionar traduções no `pt-BR.json`
|
||||
- [ ] Substituir por `t()` em cada componente
|
||||
- [ ] Testar em EN e PT-BR
|
||||
@@ -1,34 +0,0 @@
|
||||
# Task 06 — Combos Page
|
||||
|
||||
**Status:** `[ ]` Não iniciado
|
||||
**Namespace JSON:** `combos`
|
||||
|
||||
## Arquivos
|
||||
| Arquivo | Linhas | Strings |
|
||||
|---------|--------|---------|
|
||||
| `src/app/(dashboard)/dashboard/combos/page.tsx` | ~1000 | ~20 |
|
||||
|
||||
## Strings a Traduzir
|
||||
|
||||
| Linha | String EN | Chave i18n | String PT-BR |
|
||||
|-------|-----------|------------|--------------|
|
||||
| 207 | "Combos" | `combos.title` | "Combos" |
|
||||
| 372 | "No models" | `combos.noModels` | "Sem modelos" |
|
||||
| 747 | "Routing Strategy" | `combos.routingStrategy` | "Estratégia de Roteamento" |
|
||||
| 791 | "Models" | `combos.models` | "Modelos" |
|
||||
| 807 | "No models added yet" | `combos.noModelsYet` | "Nenhum modelo adicionado" |
|
||||
| 922 | "Max Retries" | `combos.maxRetries` | "Máximo de Tentativas" |
|
||||
| 959 | "Timeout (ms)" | `combos.timeout` | "Timeout (ms)" |
|
||||
| 977 | "Healthcheck" | `combos.healthcheck` | "Verificação de Saúde" |
|
||||
| — | "Create Combo" | `combos.create` | "Criar Combo" |
|
||||
| — | "Edit Combo" | `combos.edit` | "Editar Combo" |
|
||||
| — | "Delete Combo" | `combos.deleteCombo` | "Excluir Combo" |
|
||||
| — | "Add Model" | `combos.addModel` | "Adicionar Modelo" |
|
||||
| — | "Priority" | `combos.priority` | "Prioridade" |
|
||||
| — | "Fallback" | `combos.fallback` | "Fallback" |
|
||||
|
||||
## Checklist
|
||||
- [ ] Adicionar chaves no `en.json`
|
||||
- [ ] Adicionar traduções no `pt-BR.json`
|
||||
- [ ] Substituir strings por `t()` em `combos/page.tsx`
|
||||
- [ ] Testar em EN e PT-BR
|
||||
@@ -1,23 +0,0 @@
|
||||
# Task 07 — Costs Page
|
||||
|
||||
**Status:** `[ ]` Não iniciado
|
||||
**Namespace JSON:** `costs`
|
||||
|
||||
## Arquivos
|
||||
| Arquivo | Linhas | Strings |
|
||||
|---------|--------|---------|
|
||||
| `src/app/(dashboard)/dashboard/costs/page.tsx` | ~200 | ~5 |
|
||||
|
||||
## Strings a Traduzir
|
||||
|
||||
| String EN | Chave i18n | String PT-BR |
|
||||
|-----------|------------|--------------|
|
||||
| "Costs" | `costs.title` | "Custos" |
|
||||
| "Total Cost" | `costs.totalCost` | "Custo Total" |
|
||||
| "Cost Breakdown" | `costs.breakdown` | "Detalhamento de Custos" |
|
||||
| "No cost data" | `costs.noData` | "Sem dados de custo" |
|
||||
|
||||
## Checklist
|
||||
- [ ] Levantar strings completas do código
|
||||
- [ ] Adicionar chaves no `en.json` / `pt-BR.json`
|
||||
- [ ] Substituir por `t()` e testar
|
||||
@@ -1,30 +0,0 @@
|
||||
# Task 08 — Endpoint Page
|
||||
|
||||
**Status:** `[ ]` Não iniciado
|
||||
**Namespace JSON:** `endpoint`
|
||||
|
||||
## Arquivos
|
||||
| Arquivo | Linhas | Strings |
|
||||
|---------|--------|---------|
|
||||
| `src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx` | ~750 | ~20 |
|
||||
|
||||
## Strings a Traduzir
|
||||
|
||||
| Linha | String EN | Chave i18n | String PT-BR |
|
||||
|-------|-----------|------------|--------------|
|
||||
| 320 | "API Endpoint" | `endpoint.title` | "Endpoint da API" |
|
||||
| 403 | "Available Endpoints" | `endpoint.available` | "Endpoints Disponíveis" |
|
||||
| 561 | "Cloud Proxy" | `endpoint.cloudProxy` | "Proxy na Nuvem" |
|
||||
| 632 | "Note" | `endpoint.note` | "Nota" |
|
||||
| 717 | "Warning" | `endpoint.warning` | "Aviso" |
|
||||
| 740 | "Are you sure you want to disable cloud proxy?" | `endpoint.disableConfirm` | "Tem certeza que deseja desativar o proxy na nuvem?" |
|
||||
| — | "Copy" | `common.copy` | ✅ já existe |
|
||||
| — | "Base URL" | `endpoint.baseUrl` | "URL Base" |
|
||||
| — | "Connected" | `endpoint.connected` | "Conectado" |
|
||||
| — | "Enable" | `endpoint.enable` | "Ativar" |
|
||||
| — | "Disable" | `endpoint.disable` | "Desativar" |
|
||||
|
||||
## Checklist
|
||||
- [ ] Levantar strings restantes
|
||||
- [ ] Adicionar chaves no `en.json` / `pt-BR.json`
|
||||
- [ ] Substituir por `t()` e testar
|
||||
@@ -1,30 +0,0 @@
|
||||
# Task 09 — Health Page
|
||||
|
||||
**Status:** `[ ]` Não iniciado
|
||||
**Namespace JSON:** `health`
|
||||
|
||||
## Arquivos
|
||||
| Arquivo | Linhas | Strings |
|
||||
|---------|--------|---------|
|
||||
| `src/app/(dashboard)/dashboard/health/page.tsx` | ~350 | ~15 |
|
||||
|
||||
## Strings a Traduzir
|
||||
|
||||
| String EN | Chave i18n | String PT-BR |
|
||||
|-----------|------------|--------------|
|
||||
| "System Health" | `health.title` | "Saúde do Sistema" |
|
||||
| "Healthy" | `health.healthy` | "Saudável" |
|
||||
| "Degraded" | `health.degraded` | "Degradado" |
|
||||
| "Down" | `health.down` | "Offline" |
|
||||
| "Uptime" | `health.uptime` | "Tempo Ativo" |
|
||||
| "Memory" | `health.memory` | "Memória" |
|
||||
| "CPU" | `health.cpu` | "CPU" |
|
||||
| "Database" | `health.database` | "Banco de Dados" |
|
||||
| "Last Check" | `health.lastCheck` | "Última Verificação" |
|
||||
| "Refresh" | `common.refresh` | ✅ já existe |
|
||||
| ~5 strings adicionais | — | Levantar |
|
||||
|
||||
## Checklist
|
||||
- [ ] Levantar strings restantes
|
||||
- [ ] Adicionar chaves / traduções
|
||||
- [ ] Substituir por `t()` e testar
|
||||
@@ -1,24 +0,0 @@
|
||||
# Task 10 — Limits Page
|
||||
|
||||
**Status:** `[ ]` Não iniciado
|
||||
**Namespace JSON:** `limits`
|
||||
|
||||
## Arquivos
|
||||
| Arquivo | Linhas | Strings |
|
||||
|---------|--------|---------|
|
||||
| `src/app/(dashboard)/dashboard/limits/page.tsx` | ~150 | ~5 |
|
||||
|
||||
## Strings a Traduzir
|
||||
|
||||
| String EN | Chave i18n | String PT-BR |
|
||||
|-----------|------------|--------------|
|
||||
| "Limits & Quotas" | `limits.title` | "Limites e Cotas" |
|
||||
| "Rate Limit" | `limits.rateLimit` | "Limite de Taxa" |
|
||||
| "Provider" | `limits.provider` | "Provedor" |
|
||||
| "Remaining" | `limits.remaining` | "Restante" |
|
||||
| "Reset" | `limits.reset` | "Reiniciar" |
|
||||
|
||||
## Checklist
|
||||
- [ ] Levantar strings completas
|
||||
- [ ] Adicionar chaves / traduções
|
||||
- [ ] Substituir por `t()` e testar
|
||||
@@ -1,24 +0,0 @@
|
||||
# Task 11 — Logs Page
|
||||
|
||||
**Status:** `[ ]` Não iniciado
|
||||
**Namespace JSON:** `logs`
|
||||
|
||||
## Arquivos
|
||||
| Arquivo | Linhas | Strings |
|
||||
|---------|--------|---------|
|
||||
| `src/app/(dashboard)/dashboard/logs/` | ~200 | ~5 |
|
||||
|
||||
## Strings a Traduzir
|
||||
|
||||
| String EN | Chave i18n | String PT-BR |
|
||||
|-----------|------------|--------------|
|
||||
| "Logs" | `logs.title` | "Logs" |
|
||||
| "Request Logs" | `logs.requestLogs` | "Logs de Requisições" |
|
||||
| "Proxy Logs" | `logs.proxyLogs` | "Logs do Proxy" |
|
||||
| "Audit Log" | `logs.auditLog` | "Log de Auditoria" |
|
||||
| "Console" | `logs.console` | "Console" |
|
||||
|
||||
## Checklist
|
||||
- [ ] Levantar strings completas
|
||||
- [ ] Adicionar chaves / traduções
|
||||
- [ ] Substituir por `t()` e testar
|
||||
@@ -1,26 +0,0 @@
|
||||
# Task 12 — Onboarding Page
|
||||
|
||||
**Status:** `[ ]` Não iniciado
|
||||
**Namespace JSON:** `onboarding`
|
||||
|
||||
## Arquivos
|
||||
| Arquivo | Linhas | Strings |
|
||||
|---------|--------|---------|
|
||||
| `src/app/(dashboard)/dashboard/onboarding/page.tsx` | ~300 | ~10 |
|
||||
|
||||
## Strings a Traduzir
|
||||
|
||||
| String EN | Chave i18n | String PT-BR |
|
||||
|-----------|------------|--------------|
|
||||
| "Welcome to OmniRoute" | `onboarding.welcome` | "Bem-vindo ao OmniRoute" |
|
||||
| "Set Password" | `onboarding.setPassword` | "Definir Senha" |
|
||||
| "Add Provider" | `onboarding.addProvider` | "Adicionar Provedor" |
|
||||
| "Get Started" | `onboarding.getStarted` | "Começar" |
|
||||
| "Skip" | `onboarding.skip` | "Pular" |
|
||||
| "Next" | `common.next` | ✅ já existe |
|
||||
| ~4 strings adicionais | — | Levantar |
|
||||
|
||||
## Checklist
|
||||
- [ ] Levantar strings completas
|
||||
- [ ] Adicionar chaves / traduções
|
||||
- [ ] Substituir por `t()` e testar
|
||||
@@ -1,35 +0,0 @@
|
||||
# Task 13 — Providers Pages
|
||||
|
||||
**Status:** `[ ]` Não iniciado
|
||||
**Namespace JSON:** `providers`
|
||||
|
||||
## Arquivos
|
||||
| Arquivo | Strings |
|
||||
|---------|---------|
|
||||
| `providers/page.tsx` | ~5 |
|
||||
| `providers/[id]/page.tsx` | ~12 |
|
||||
| `providers/new/page.tsx` | ~3 |
|
||||
| `providers/components/ModelAvailabilityPanel.tsx` | ~3 |
|
||||
| `providers/components/ModelAvailabilityBadge.tsx` | ~1 |
|
||||
|
||||
## Strings a Traduzir
|
||||
|
||||
| String EN | Chave i18n | String PT-BR |
|
||||
|-----------|------------|--------------|
|
||||
| "Providers" | `providers.title` | "Provedores" |
|
||||
| "Add Provider" | `providers.add` | "Adicionar Provedor" |
|
||||
| "Edit Provider" | `providers.edit` | "Editar Provedor" |
|
||||
| "Test Connection" | `providers.testConnection` | "Testar Conexão" |
|
||||
| "Connected" | `providers.connected` | "Conectado" |
|
||||
| "Disconnected" | `providers.disconnected` | "Desconectado" |
|
||||
| "Models" | `providers.models` | "Modelos" |
|
||||
| "Accounts" | `providers.accounts` | "Contas" |
|
||||
| "Delete Provider" | `providers.deleteProvider` | "Excluir Provedor" |
|
||||
| "No providers configured" | `providers.noProviders` | "Nenhum provedor configurado" |
|
||||
| "Model Availability" | `providers.modelAvailability` | "Disponibilidade de Modelos" |
|
||||
| ~9 strings adicionais | — | Levantar |
|
||||
|
||||
## Checklist
|
||||
- [ ] Levantar strings completas de todos os arquivos
|
||||
- [ ] Adicionar chaves / traduções
|
||||
- [ ] Substituir por `t()` e testar
|
||||
@@ -1,51 +0,0 @@
|
||||
# Task 14 — Settings Page (MAIOR TAREFA)
|
||||
|
||||
**Status:** `[ ]` Não iniciado
|
||||
**Namespace JSON:** `settings`
|
||||
|
||||
## Arquivos (20 componentes!)
|
||||
| Arquivo | Strings |
|
||||
|---------|---------|
|
||||
| `settings/components/AppearanceTab.tsx` | ~4 |
|
||||
| `settings/components/CacheStatsCard.tsx` | ~4 |
|
||||
| `settings/components/ComboDefaultsTab.tsx` | ~8 |
|
||||
| `settings/components/FallbackChainsEditor.tsx` | ~2 |
|
||||
| `settings/components/IPFilterSection.tsx` | ~2 |
|
||||
| `settings/components/PoliciesPanel.tsx` | ~5 |
|
||||
| `settings/components/PricingTab.tsx` | ~8 |
|
||||
| `settings/components/ProxyTab.tsx` | ~2 |
|
||||
| `settings/components/ResilienceTab.tsx` | ~7 |
|
||||
| `settings/components/RoutingTab.tsx` | ~4 |
|
||||
| `settings/components/SecurityTab.tsx` | ~5 |
|
||||
| `settings/components/SessionInfoCard.tsx` | ~5 |
|
||||
| `settings/components/SystemPromptTab.tsx` | ~2 |
|
||||
| `settings/components/SystemStorageTab.tsx` | ~8 |
|
||||
| `settings/components/ThinkingBudgetTab.tsx` | ~5 |
|
||||
| `settings/pricing/page.tsx` | ~17 |
|
||||
|
||||
## Strings a Traduzir (amostra)
|
||||
|
||||
| String EN | Chave i18n | String PT-BR |
|
||||
|-----------|------------|--------------|
|
||||
| "General" | `settings.general` | "Geral" |
|
||||
| "Security" | `settings.security` | "Segurança" |
|
||||
| "Appearance" | `settings.appearance` | "Aparência" |
|
||||
| "Routing" | `settings.routing` | "Roteamento" |
|
||||
| "Cache" | `settings.cache` | "Cache" |
|
||||
| "Resilience" | `settings.resilience` | "Resiliência" |
|
||||
| "System Prompt" | `settings.systemPrompt` | "Prompt do Sistema" |
|
||||
| "Thinking Budget" | `settings.thinkingBudget` | "Orçamento de Raciocínio" |
|
||||
| "Proxy" | `settings.proxy` | "Proxy" |
|
||||
| "Pricing" | `settings.pricing` | "Preços" |
|
||||
| "Storage" | `settings.storage` | "Armazenamento" |
|
||||
| "Policies" | `settings.policies` | "Políticas" |
|
||||
| "IP Filter" | `settings.ipFilter` | "Filtro de IP" |
|
||||
| "Combo Defaults" | `settings.comboDefaults` | "Padrões de Combo" |
|
||||
| "Fallback Chains" | `settings.fallbackChains` | "Cadeias de Fallback" |
|
||||
| ~40 strings adicionais | — | Levantar em cada tab |
|
||||
|
||||
## Checklist
|
||||
- [ ] Levantar strings de CADA componente (16 arquivos)
|
||||
- [ ] Adicionar chaves / traduções
|
||||
- [ ] Substituir por `t()` em todos os 16 arquivos
|
||||
- [ ] Testar cada aba em EN e PT-BR
|
||||
@@ -1,41 +0,0 @@
|
||||
# Task 15 — Translator Page
|
||||
|
||||
**Status:** `[ ]` Não iniciado
|
||||
**Namespace JSON:** `translator`
|
||||
|
||||
## Arquivos
|
||||
| Arquivo | Strings |
|
||||
|---------|---------|
|
||||
| `translator/components/LiveMonitorMode.tsx` | ~11 |
|
||||
| `translator/components/PlaygroundMode.tsx` | ~5 |
|
||||
| `translator/components/TestBenchMode.tsx` | ~3 |
|
||||
| `translator/components/ChatTesterMode.tsx` | ~4 |
|
||||
|
||||
## Strings a Traduzir
|
||||
|
||||
| String EN | Chave i18n | String PT-BR |
|
||||
|-----------|------------|--------------|
|
||||
| "Real-Time Translation Activity" | `translator.realtime` | "Atividade de Tradução em Tempo Real" |
|
||||
| "Chat Tester" | `translator.chatTester` | "Testador de Chat" |
|
||||
| "Test Bench" | `translator.testBench` | "Bancada de Testes" |
|
||||
| "Recent Translations" | `translator.recentTranslations` | "Traduções Recentes" |
|
||||
| "No translations yet" | `translator.noTranslations` | "Nenhuma tradução ainda" |
|
||||
| "Time" | `translator.time` | "Tempo" |
|
||||
| "Source" | `translator.source` | "Origem" |
|
||||
| "Target" | `translator.target` | "Destino" |
|
||||
| "Model" | `translator.model` | "Modelo" |
|
||||
| "Status" | `translator.status` | "Status" |
|
||||
| "Latency" | `translator.latency` | "Latência" |
|
||||
| "Format Converter" | `translator.formatConverter` | "Conversor de Formato" |
|
||||
| "Input" | `translator.input` | "Entrada" |
|
||||
| "Output" | `translator.output` | "Saída" |
|
||||
| "Example Templates" | `translator.exampleTemplates` | "Modelos de Exemplo" |
|
||||
| "Compatibility Tester" | `translator.compatibilityTester` | "Testador de Compatibilidade" |
|
||||
| "Compatibility Report" | `translator.compatibilityReport` | "Relatório de Compatibilidade" |
|
||||
| "Pipeline Debugger" | `translator.pipelineDebugger` | "Depurador de Pipeline" |
|
||||
| "Translation Pipeline" | `translator.translationPipeline` | "Pipeline de Tradução" |
|
||||
|
||||
## Checklist
|
||||
- [ ] Adicionar chaves / traduções
|
||||
- [ ] Substituir por `t()` em cada componente
|
||||
- [ ] Testar em EN e PT-BR
|
||||
@@ -1,57 +0,0 @@
|
||||
# Task 16 — Usage Page
|
||||
|
||||
**Status:** `[ ]` Não iniciado
|
||||
**Namespace JSON:** `usage`
|
||||
|
||||
## Arquivos
|
||||
| Arquivo | Strings |
|
||||
|---------|---------|
|
||||
| `usage/components/BudgetTab.tsx` | ~4 |
|
||||
| `usage/components/BudgetTelemetryCards.tsx` | ~9 |
|
||||
| `usage/components/EvalsTab.tsx` | ~6 |
|
||||
| `usage/components/RateLimitStatus.tsx` | ~2 |
|
||||
| `usage/components/SessionsTab.tsx` | ~7 |
|
||||
| `usage/components/ProviderLimits/index.tsx` | ~7 |
|
||||
| `usage/components/ProviderLimits/ProviderLimitCard.tsx` | ~1 |
|
||||
| `usage/components/ProviderLimits/QuotaTable.tsx` | ~1 |
|
||||
|
||||
## Strings a Traduzir
|
||||
|
||||
| String EN | Chave i18n | String PT-BR |
|
||||
|-----------|------------|--------------|
|
||||
| "Budget Management" | `usage.budgetManagement` | "Gerenciamento de Orçamento" |
|
||||
| "API Key" | `usage.apiKey` | "Chave de API" |
|
||||
| "This Month" | `usage.thisMonth` | "Este Mês" |
|
||||
| "Set Limits" | `usage.setLimits` | "Definir Limites" |
|
||||
| "Total requests" | `usage.totalRequests` | "Total de requisições" |
|
||||
| "No data yet" | `usage.noData` | "Sem dados ainda" |
|
||||
| "Entries" | `usage.entries` | "Entradas" |
|
||||
| "Hit Rate" | `usage.hitRate` | "Taxa de Acerto" |
|
||||
| "Circuit Breakers" | `usage.circuitBreakers` | "Disjuntores" |
|
||||
| "Locked IPs" | `usage.lockedIPs` | "IPs Bloqueados" |
|
||||
| "How It Works" | `usage.howItWorks` | "Como Funciona" |
|
||||
| "Define" | `usage.define` | "Definir" |
|
||||
| "Run" | `usage.run` | "Executar" |
|
||||
| "Evaluate" | `usage.evaluate` | "Avaliar" |
|
||||
| "Evaluation Suites" | `usage.evalSuites` | "Suítes de Avaliação" |
|
||||
| "Model Evaluations" | `usage.modelEvals` | "Avaliações de Modelos" |
|
||||
| "Model Lockouts" | `usage.modelLockouts` | "Bloqueios de Modelo" |
|
||||
| "No models currently locked" | `usage.noLockouts` | "Nenhum modelo bloqueado" |
|
||||
| "Active Sessions" | `usage.activeSessions` | "Sessões Ativas" |
|
||||
| "No active sessions" | `usage.noSessions` | "Sem sessões ativas" |
|
||||
| "Session" | `usage.session` | "Sessão" |
|
||||
| "Age" | `usage.age` | "Idade" |
|
||||
| "Requests" | `usage.requests` | "Requisições" |
|
||||
| "Connection" | `usage.connection` | "Conexão" |
|
||||
| "Provider Limits" | `usage.providerLimits` | "Limites do Provedor" |
|
||||
| "No Providers Connected" | `usage.noProviders` | "Nenhum Provedor Conectado" |
|
||||
| "Account" | `usage.account` | "Conta" |
|
||||
| "Model Quotas" | `usage.modelQuotas` | "Cotas de Modelo" |
|
||||
| "Last Used" | `usage.lastUsed` | "Último Uso" |
|
||||
| "Actions" | `usage.actions` | "Ações" |
|
||||
| "No quota data" | `usage.noQuota` | "Sem dados de cota" |
|
||||
|
||||
## Checklist
|
||||
- [ ] Adicionar chaves / traduções
|
||||
- [ ] Substituir por `t()` em cada componente
|
||||
- [ ] Testar em EN e PT-BR
|
||||
@@ -1,44 +0,0 @@
|
||||
# Task 17 — Shared Modals
|
||||
|
||||
**Status:** `[ ]` Não iniciado
|
||||
**Namespace JSON:** `modals`
|
||||
|
||||
## Arquivos
|
||||
| Arquivo | Strings |
|
||||
|---------|---------|
|
||||
| `shared/components/OAuthModal.tsx` | ~6 |
|
||||
| `shared/components/KiroAuthModal.tsx` | ~10 |
|
||||
| `shared/components/KiroSocialOAuthModal.tsx` | ~3 |
|
||||
| `shared/components/CursorAuthModal.tsx` | ~2 |
|
||||
| `shared/components/PricingModal.tsx` | ~10 |
|
||||
| `shared/components/ModelSelectModal.tsx` | ~2 |
|
||||
| `shared/components/ProxyConfigModal.tsx` | ~1 |
|
||||
|
||||
## Strings a Traduzir
|
||||
|
||||
| String EN | String PT-BR |
|
||||
|-----------|--------------|
|
||||
| "Waiting for Authorization" | "Aguardando Autorização" |
|
||||
| "Verification URL" | "URL de Verificação" |
|
||||
| "Your Code" | "Seu Código" |
|
||||
| "Remote access:" | "Acesso remoto:" |
|
||||
| "Connected Successfully!" | "Conectado com Sucesso!" |
|
||||
| "Connection Failed" | "Falha na Conexão" |
|
||||
| "Choose your authentication method:" | "Escolha seu método de autenticação:" |
|
||||
| "AWS Builder ID" | "AWS Builder ID" |
|
||||
| "AWS IAM Identity Center" | "AWS IAM Identity Center" |
|
||||
| "Google Account" | "Conta Google" |
|
||||
| "GitHub Account" | "Conta GitHub" |
|
||||
| "Import Token" | "Importar Token" |
|
||||
| "Auto-detecting tokens..." | "Detectando tokens automaticamente..." |
|
||||
| "Pricing Configuration" | "Configuração de Preços" |
|
||||
| "Loading pricing data..." | "Carregando dados de preços..." |
|
||||
| "Model" / "Input" / "Output" / "Cached" | "Modelo" / "Entrada" / "Saída" / "Em Cache" |
|
||||
| "Combos" | "Combos" |
|
||||
| "No models found" | "Nenhum modelo encontrado" |
|
||||
| "Connected" | "Conectado" |
|
||||
|
||||
## Checklist
|
||||
- [ ] Adicionar chaves / traduções
|
||||
- [ ] Substituir por `t()` em cada modal
|
||||
- [ ] Testar cada modal em EN e PT-BR
|
||||
@@ -1,37 +0,0 @@
|
||||
# Task 18 — Shared Loggers
|
||||
|
||||
**Status:** `[ ]` Não iniciado
|
||||
**Namespace JSON:** `loggers`
|
||||
|
||||
## Arquivos
|
||||
| Arquivo | Strings |
|
||||
|---------|---------|
|
||||
| `shared/components/RequestLoggerV2.tsx` | ~12 |
|
||||
| `shared/components/RequestLoggerDetail.tsx` | ~4 |
|
||||
| `shared/components/ProxyLogger.tsx` | ~7 |
|
||||
| `shared/components/ProxyLogDetail.tsx` | ~6 |
|
||||
| `shared/components/ConsoleLogViewer.tsx` | ~2 |
|
||||
|
||||
## Strings a Traduzir
|
||||
|
||||
| String EN | String PT-BR |
|
||||
|-----------|--------------|
|
||||
| "All Providers" | "Todos os Provedores" |
|
||||
| "All Models" | "Todos os Modelos" |
|
||||
| "All Accounts" | "Todas as Contas" |
|
||||
| "All API Keys" | "Todas as Chaves de API" |
|
||||
| "Newest" / "Oldest" | "Mais Recente" / "Mais Antigo" |
|
||||
| "Model A-Z" / "Model Z-A" | "Modelo A-Z" / "Modelo Z-A" |
|
||||
| "Columns" | "Colunas" |
|
||||
| "Loading logs..." | "Carregando logs..." |
|
||||
| "All Types" | "Todos os Tipos" |
|
||||
| "All Levels" | "Todos os Níveis" |
|
||||
| "Proxy Event" | "Evento do Proxy" |
|
||||
| "Time" / "Model" / "Combo" | "Tempo" / "Modelo" / "Combo" |
|
||||
| "No log entries found" | "Nenhuma entrada de log encontrada" |
|
||||
| "No payload data available" | "Nenhum dado de payload disponível" |
|
||||
|
||||
## Checklist
|
||||
- [ ] Adicionar chaves / traduções
|
||||
- [ ] Substituir por `t()` em cada logger
|
||||
- [ ] Testar em EN e PT-BR
|
||||
@@ -1,36 +0,0 @@
|
||||
# Task 19 — Shared Charts & Stats
|
||||
|
||||
**Status:** `[ ]` Não iniciado
|
||||
**Namespace JSON:** `stats`
|
||||
|
||||
## Arquivos
|
||||
| Arquivo | Strings |
|
||||
|---------|---------|
|
||||
| `shared/components/UsageStats.tsx` | ~6 |
|
||||
| `shared/components/analytics/charts.tsx` | ~15 |
|
||||
| `shared/components/TokenHealthBadge.tsx` | ~6 |
|
||||
| `shared/components/SystemMonitor.tsx` | ~1 |
|
||||
| `shared/components/Footer.tsx` | ~3 |
|
||||
|
||||
## Strings a Traduzir
|
||||
|
||||
| String EN | String PT-BR |
|
||||
|-----------|--------------|
|
||||
| "Usage Overview" | "Visão Geral de Uso" |
|
||||
| "Output Tokens" | "Tokens de Saída" |
|
||||
| "Total Cost" | "Custo Total" |
|
||||
| "Usage by Model" | "Uso por Modelo" |
|
||||
| "Usage by Account" | "Uso por Conta" |
|
||||
| "Failed to load usage statistics." | "Falha ao carregar estatísticas." |
|
||||
| "Token Health" | "Saúde dos Tokens" |
|
||||
| "Total OAuth" | "Total OAuth" |
|
||||
| "Healthy" / "Errored" / "Warning" | "Saudável" / "Com Erro" / "Aviso" |
|
||||
| "Last check" | "Última verificação" |
|
||||
| "No data" / "Share" | "Sem dados" / "Compartilhar" |
|
||||
| "Unable to load system metrics" | "Não foi possível carregar métricas" |
|
||||
| "Product" / "Resources" / "Company" (Footer) | "Produto" / "Recursos" / "Empresa" |
|
||||
|
||||
## Checklist
|
||||
- [ ] Adicionar chaves / traduções
|
||||
- [ ] Substituir por `t()` em cada componente
|
||||
- [ ] Testar em EN e PT-BR
|
||||
@@ -1,36 +0,0 @@
|
||||
# Task 20 — Login & Auth Pages
|
||||
|
||||
**Status:** `[ ]` Não iniciado
|
||||
**Namespace JSON:** `auth`
|
||||
|
||||
## Arquivos
|
||||
| Arquivo | Strings |
|
||||
|---------|---------|
|
||||
| `src/app/login/page.tsx` | ~8 |
|
||||
| `src/app/forgot-password/page.tsx` | ~3 |
|
||||
| `src/app/callback/page.tsx` | ~5 |
|
||||
| `src/app/forbidden/page.tsx` | ~1 |
|
||||
|
||||
## Strings a Traduzir
|
||||
|
||||
| String EN | String PT-BR |
|
||||
|-----------|--------------|
|
||||
| "Welcome" | "Bem-vindo" |
|
||||
| "OmniRoute" | "OmniRoute" (não traduzir) |
|
||||
| "Sign in" | "Entrar" |
|
||||
| "Enter your password to continue" | "Digite sua senha para continuar" |
|
||||
| "Password" | "Senha" |
|
||||
| "Unified AI API Proxy" | "Proxy Unificado de API de IA" |
|
||||
| "Loading..." | "Carregando..." |
|
||||
| "Password protection is not enabled" | "Proteção por senha não está ativada" |
|
||||
| "Reset Password" | "Redefinir Senha" |
|
||||
| "Choose a method to recover access" | "Escolha um método para recuperar acesso" |
|
||||
| "Processing..." | "Processando..." |
|
||||
| "Authorization Successful!" | "Autorização bem-sucedida!" |
|
||||
| "Copy This URL" | "Copiar esta URL" |
|
||||
| "Access Denied" | "Acesso Negado" |
|
||||
|
||||
## Checklist
|
||||
- [ ] Adicionar chaves / traduções
|
||||
- [ ] Substituir por `t()` em cada página
|
||||
- [ ] Testar em EN e PT-BR
|
||||
@@ -1,34 +0,0 @@
|
||||
# Task 21 — Landing Page
|
||||
|
||||
**Status:** `[ ]` Não iniciado
|
||||
**Namespace JSON:** `landing`
|
||||
|
||||
## Arquivos
|
||||
| Arquivo | Strings |
|
||||
|---------|---------|
|
||||
| `landing/components/HeroSection.tsx` | ~3 |
|
||||
| `landing/components/Features.tsx` | ~10 |
|
||||
| `landing/components/HowItWorks.tsx` | ~5 |
|
||||
| `landing/components/GetStarted.tsx` | ~5 |
|
||||
| `landing/components/Navigation.tsx` | ~2 |
|
||||
| `landing/components/FlowAnimation.tsx` | ~2 |
|
||||
| `landing/components/Footer.tsx` | ~5 |
|
||||
|
||||
## Strings a Traduzir (amostra)
|
||||
|
||||
| String EN | String PT-BR |
|
||||
|-----------|--------------|
|
||||
| "All AI Providers" | "Todos os Provedores de IA" |
|
||||
| "One Endpoint" | "Um Endpoint" |
|
||||
| "Powerful Features" | "Recursos Poderosos" |
|
||||
| "How OmniRoute Works" | "Como o OmniRoute Funciona" |
|
||||
| "Install OmniRoute" | "Instalar o OmniRoute" |
|
||||
| "Open Dashboard" | "Abrir Painel" |
|
||||
| "Route Requests" | "Rotear Requisições" |
|
||||
| "Data Location:" | "Local dos Dados:" |
|
||||
| "Product" / "Resources" / "Legal" | "Produto" / "Recursos" / "Legal" |
|
||||
|
||||
## Checklist
|
||||
- [ ] Levantar strings completas de cada componente
|
||||
- [ ] Adicionar chaves / traduções
|
||||
- [ ] Substituir por `t()` e testar
|
||||
@@ -1,32 +0,0 @@
|
||||
# Task 22 — Docs Page
|
||||
|
||||
**Status:** `[ ]` Não iniciado
|
||||
**Namespace JSON:** `docs`
|
||||
|
||||
## Arquivos
|
||||
| Arquivo | Strings |
|
||||
|---------|---------|
|
||||
| `src/app/docs/page.tsx` | ~25 |
|
||||
|
||||
## Strings a Traduzir
|
||||
|
||||
| String EN | String PT-BR |
|
||||
|-----------|--------------|
|
||||
| "Quick Start" | "Início Rápido" |
|
||||
| "Features" | "Recursos" |
|
||||
| "Supported Providers" | "Provedores Suportados" |
|
||||
| "Common Use Cases" | "Casos de Uso Comuns" |
|
||||
| "Client Compatibility" | "Compatibilidade de Clientes" |
|
||||
| "Cherry Studio" | "Cherry Studio" (não traduzir) |
|
||||
| "Codex / GitHub Copilot Models" | "Modelos Codex / GitHub Copilot" |
|
||||
| "Cursor IDE" | "Cursor IDE" (não traduzir) |
|
||||
| "Claude Code / Antigravity" | "Claude Code / Antigravity" |
|
||||
| "API Reference" | "Referência da API" |
|
||||
| "Method" / "Path" / "Notes" | "Método" / "Caminho" / "Notas" |
|
||||
| "Model Prefixes" | "Prefixos de Modelo" |
|
||||
| "Prefix" / "Provider" / "Type" | "Prefixo" / "Provedor" / "Tipo" |
|
||||
| "Troubleshooting" | "Solução de Problemas" |
|
||||
|
||||
## Checklist
|
||||
- [ ] Adicionar chaves / traduções
|
||||
- [ ] Substituir por `t()` e testar
|
||||
@@ -1,31 +0,0 @@
|
||||
# Task 23 — Legal Pages (Privacy & Terms)
|
||||
|
||||
**Status:** `[ ]` Não iniciado
|
||||
**Namespace JSON:** `legal`
|
||||
|
||||
## Arquivos
|
||||
| Arquivo | Strings |
|
||||
|---------|---------|
|
||||
| `src/app/privacy/page.tsx` | ~10 |
|
||||
| `src/app/terms/page.tsx` | ~5 |
|
||||
|
||||
## Strings a Traduzir
|
||||
|
||||
| String EN | String PT-BR |
|
||||
|-----------|--------------|
|
||||
| "Privacy Policy" | "Política de Privacidade" |
|
||||
| "Terms of Service" | "Termos de Serviço" |
|
||||
| "Provider configurations" | "Configurações de provedores" |
|
||||
| "API keys" | "Chaves de API" |
|
||||
| "Usage logs" | "Logs de uso" |
|
||||
| "Application settings" | "Configurações do aplicativo" |
|
||||
| "View and export usage analytics" | "Visualizar e exportar análises de uso" |
|
||||
| "Clear usage history at any time" | "Limpar histórico de uso a qualquer momento" |
|
||||
| "Configure log retention policies" | "Configurar políticas de retenção de logs" |
|
||||
| "Back up and restore your database" | "Fazer backup e restaurar seu banco de dados" |
|
||||
|
||||
## Checklist
|
||||
- [ ] Adicionar chaves / traduções
|
||||
- [ ] Substituir por `t()` e testar
|
||||
|
||||
> **Nota:** Textos legais podem requerer revisão jurídica para tradução formal.
|
||||
@@ -1,51 +0,0 @@
|
||||
# i18n Translation Tasks
|
||||
|
||||
Cada arquivo `.md` nesta pasta representa **uma tarefa de tradução** para uma página ou componente do OmniRoute.
|
||||
|
||||
## Status Legend
|
||||
|
||||
- `[ ]` — Não iniciado
|
||||
- `[/]` — Em progresso
|
||||
- `[x]` — Concluído
|
||||
|
||||
## Dashboard Pages (~260 strings)
|
||||
|
||||
| # | Tarefa | Arquivo | Strings | Status |
|
||||
| --- | ---------------------------------- | ----------------------------------------------------- | ------- | ------ |
|
||||
| 01 | [Home](./01-home.md) | `HomePageClient.tsx` | ~25 | `[ ]` |
|
||||
| 02 | [Analytics](./02-analytics.md) | `analytics/page.tsx` | ~8 | `[ ]` |
|
||||
| 03 | [API Manager](./03-api-manager.md) | `api-manager/` | ~20 | `[ ]` |
|
||||
| 04 | [Audit Log](./04-audit-log.md) | `audit-log/page.tsx`, `logs/AuditLogTab.tsx` | ~15 | `[ ]` |
|
||||
| 05 | [CLI Tools](./05-cli-tools.md) | `cli-tools/components/*.tsx` | ~20 | `[ ]` |
|
||||
| 06 | [Combos](./06-combos.md) | `combos/page.tsx` | ~20 | `[ ]` |
|
||||
| 07 | [Costs](./07-costs.md) | `costs/page.tsx` | ~5 | `[ ]` |
|
||||
| 08 | [Endpoint](./08-endpoint.md) | `endpoint/EndpointPageClient.tsx` | ~20 | `[ ]` |
|
||||
| 09 | [Health](./09-health.md) | `health/page.tsx` | ~15 | `[ ]` |
|
||||
| 10 | [Limits](./10-limits.md) | `limits/page.tsx` | ~5 | `[ ]` |
|
||||
| 11 | [Logs](./11-logs.md) | `logs/` | ~5 | `[ ]` |
|
||||
| 12 | [Onboarding](./12-onboarding.md) | `onboarding/page.tsx` | ~10 | `[ ]` |
|
||||
| 13 | [Providers](./13-providers.md) | `providers/page.tsx`, `[id]/page.tsx`, `new/page.tsx` | ~20 | `[ ]` |
|
||||
| 14 | [Settings](./14-settings.md) | `settings/components/*.tsx` | ~55 | `[ ]` |
|
||||
| 15 | [Translator](./15-translator.md) | `translator/components/*.tsx` | ~25 | `[ ]` |
|
||||
| 16 | [Usage](./16-usage.md) | `usage/components/*.tsx` | ~35 | `[ ]` |
|
||||
|
||||
## Shared Components (~95 strings)
|
||||
|
||||
| # | Tarefa | Arquivo(s) | Strings | Status |
|
||||
| --- | ---------------------------------------------- | ---------------------------------------------------- | ------- | ------ |
|
||||
| 17 | [Shared Modals](./17-shared-modals.md) | `OAuthModal`, `KiroAuthModal`, `PricingModal`, etc. | ~40 | `[ ]` |
|
||||
| 18 | [Shared Loggers](./18-shared-loggers.md) | `RequestLoggerV2`, `ProxyLogger`, `ProxyLogDetail` | ~30 | `[ ]` |
|
||||
| 19 | [Shared Charts & Stats](./19-shared-charts.md) | `UsageStats`, `analytics/charts`, `TokenHealthBadge` | ~25 | `[ ]` |
|
||||
|
||||
## Non-Dashboard Pages (~75 strings)
|
||||
|
||||
| # | Tarefa | Arquivo(s) | Strings | Status |
|
||||
| --- | ---------------------------------- | ------------------------------------------------------- | ------- | ------ |
|
||||
| 20 | [Login & Auth](./20-login-auth.md) | `login/`, `forgot-password/`, `callback/`, `forbidden/` | ~20 | `[ ]` |
|
||||
| 21 | [Landing Page](./21-landing.md) | `landing/components/*.tsx` | ~25 | `[ ]` |
|
||||
| 22 | [Docs Page](./22-docs.md) | `docs/page.tsx` | ~25 | `[ ]` |
|
||||
| 23 | [Legal Pages](./23-legal.md) | `privacy/`, `terms/` | ~15 | `[ ]` |
|
||||
|
||||
---
|
||||
|
||||
**Total estimado: ~460 strings em 23 tarefas**
|
||||
|
Before Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 173 KiB |
@@ -1,10 +1,9 @@
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
/** @type {import("eslint").Linter.Config[]} */
|
||||
const eslintConfig = [
|
||||
...nextVitals,
|
||||
// FASE-02: Security rules (strict everywhere)
|
||||
// FASE-02: Security rules
|
||||
{
|
||||
rules: {
|
||||
"no-eval": "error",
|
||||
@@ -12,28 +11,18 @@ const eslintConfig = [
|
||||
"no-new-func": "error",
|
||||
},
|
||||
},
|
||||
// Relaxed rules for open-sse and tests (incremental adoption)
|
||||
{
|
||||
files: ["open-sse/**/*.ts", "tests/**/*.mjs", "tests/**/*.ts"],
|
||||
plugins: {
|
||||
"@typescript-eslint": tseslint.plugin,
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@next/next/no-assign-module-variable": "off",
|
||||
"react-hooks/rules-of-hooks": "off",
|
||||
},
|
||||
},
|
||||
// Global ignores (open-sse and tests REMOVED — now linted)
|
||||
// Global ignores
|
||||
{
|
||||
ignores: [
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
"tests/**",
|
||||
"scripts/**",
|
||||
"bin/**",
|
||||
"node_modules/**",
|
||||
"open-sse/**",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
# OmniRoute
|
||||
|
||||
> OmniRoute is a free, open-source AI Gateway that acts as a universal API proxy for multi-provider LLMs. It provides smart routing, automatic fallback, load balancing, and format translation across 36+ AI providers — all through a single OpenAI-compatible endpoint.
|
||||
|
||||
## Overview
|
||||
|
||||
OmniRoute solves the problem of managing multiple AI provider subscriptions, quotas, and rate limits. It sits between your AI-powered tools (IDE agents, CLI tools) and AI providers, routing requests intelligently through a 4-tier fallback system: Subscription → API Key → Cheap → Free.
|
||||
|
||||
**Key value:** One endpoint (`http://localhost:20128/v1`), unlimited models, zero downtime, minimal cost.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Runtime:** Node.js >= 18
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config)
|
||||
- **State management:** Zustand (client), lowdb (server JSON persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
- **Background jobs:** Custom token health check scheduler
|
||||
- **Streaming:** Server-Sent Events (SSE) for real-time proxy responses
|
||||
- **Proxy engine:** Custom pipeline with format translation, circuit breaker, rate limiting
|
||||
- **Package:** Published on npm (`omniroute`) and Docker Hub (`diegosouzapw/omniroute`)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
/
|
||||
├── src/ # Main application source
|
||||
│ ├── app/ # Next.js App Router pages and API routes
|
||||
│ │ ├── (dashboard)/ # Dashboard UI pages (providers, combos, analytics, logs, etc.)
|
||||
│ │ ├── api/ # REST API endpoints
|
||||
│ │ │ ├── v1/ # OpenAI-compatible API (chat, models, embeddings, images, audio)
|
||||
│ │ │ ├── oauth/ # OAuth flows per provider (authorize, exchange, callback)
|
||||
│ │ │ ├── providers/ # Provider CRUD and batch testing
|
||||
│ │ │ ├── models/ # Dashboard model listing and aliases
|
||||
│ │ │ ├── combos/ # Combo CRUD (multi-model fallback chains)
|
||||
│ │ │ └── ... # Other endpoints (usage, logs, health, settings, etc.)
|
||||
│ │ └── login/ # Login page
|
||||
│ ├── domain/ # Domain types and business logic interfaces
|
||||
│ ├── lib/ # Core libraries
|
||||
│ │ ├── db/ # SQLite database layer (providers, combos, prompts, logs)
|
||||
│ │ ├── oauth/ # OAuth providers, services, and utilities
|
||||
│ │ │ ├── providers/ # Provider-specific OAuth configs (GitHub, Google, Claude, etc.)
|
||||
│ │ │ ├── services/ # Provider-specific token exchange logic
|
||||
│ │ │ └── utils/ # PKCE, callback server, token helpers
|
||||
│ │ ├── cloudSync.ts # Cloud sync via Cloudflare Workers
|
||||
│ │ ├── tokenHealthCheck.ts # Background OAuth token refresh scheduler
|
||||
│ │ └── localDb.ts # Unified database access layer
|
||||
│ ├── shared/ # Shared utilities, components, and constants
|
||||
│ │ ├── components/ # Reusable UI components (Card, Badge, Button, Modal, etc.)
|
||||
│ │ ├── constants/ # Provider definitions, model lists, pricing
|
||||
│ │ └── utils/ # Helpers (auth, CORS, error codes, machine ID)
|
||||
│ ├── sse/ # SSE proxy pipeline
|
||||
│ │ ├── services/ # Auth resolution, format translation, response handling
|
||||
│ │ └── middleware/ # Rate limiting, circuit breaker, caching, idempotency
|
||||
│ ├── store/ # Zustand client-side stores
|
||||
│ ├── types/ # TypeScript type definitions
|
||||
│ ├── proxy.ts # Main proxy request handler
|
||||
│ └── server-init.ts # Server initialization (DB, health checks)
|
||||
├── open-sse/ # Standalone SSE server (npm workspace)
|
||||
│ ├── config/ # Model registries (embedding, image, audio, rerank, moderation)
|
||||
│ ├── handlers/ # Request handlers per API type
|
||||
│ └── translators/ # Format translators (OpenAI ↔ Claude ↔ Gemini ↔ Responses)
|
||||
├── tests/ # Test suites
|
||||
│ ├── unit/ # Unit tests (32+ test files)
|
||||
│ └── integration/ # Integration tests
|
||||
├── docs/ # Documentation and screenshots
|
||||
├── bin/ # CLI entry points (omniroute, reset-password)
|
||||
└── .env.example # Environment variable template
|
||||
```
|
||||
|
||||
## Key Architectural Decisions
|
||||
|
||||
1. **OpenAI-compatible API surface:** All incoming requests follow the OpenAI API format (`/v1/chat/completions`, `/v1/models`, etc.). This makes OmniRoute a drop-in replacement for any tool that supports custom OpenAI endpoints.
|
||||
|
||||
2. **Provider abstraction via format translators:** Each AI provider (Claude, Gemini, etc.) has a translator in `open-sse/translators/` that converts between the OpenAI format and the provider's native format. This happens transparently.
|
||||
|
||||
3. **Connection-based provider model:** Providers are stored as "connections" in SQLite. Each connection has an `id`, `provider`, `authType` (oauth/apikey/free), `isActive` flag, and credentials. Multiple connections per provider are supported for multi-account rotation.
|
||||
|
||||
4. **Combo system for fallback:** Users create "combos" — ordered lists of `provider/model` pairs. The proxy tries each in order until one succeeds. Supports 6 strategies: fill-first, round-robin, power-of-two-choices, random, least-used, cost-optimized.
|
||||
|
||||
5. **SSE proxy pipeline (`src/sse/`):** The proxy pipeline is middleware-based: request → auth resolution → rate limiting → circuit breaker → format translation → upstream call → response translation → SSE streaming back to client.
|
||||
|
||||
6. **SQLite for persistence:** All state (providers, combos, logs, settings) is stored in a single SQLite database file at `data/omniroute.db`. This keeps the app self-contained and zero-config.
|
||||
|
||||
7. **OAuth with PKCE:** OAuth flows use PKCE for security. A local callback server (`src/lib/oauth/utils/server.ts`) handles the redirect. Token refresh is handled by a background job (`tokenHealthCheck.ts`).
|
||||
|
||||
## Main Flows
|
||||
|
||||
### Proxy Request Flow
|
||||
1. Client sends OpenAI-format request to `/v1/chat/completions`
|
||||
2. API key validation (`src/shared/utils/apiAuth.ts`)
|
||||
3. Model resolution: direct model or combo lookup
|
||||
4. For combos: iterate through models in fallback order
|
||||
5. Auth resolution: get credentials for the target provider
|
||||
6. Format translation: OpenAI → provider native format
|
||||
7. Upstream request with circuit breaker and rate limiting
|
||||
8. Response translation: provider → OpenAI format
|
||||
9. SSE streaming back to client
|
||||
|
||||
### OAuth Flow
|
||||
1. Dashboard initiates `/api/oauth/[provider]/authorize`
|
||||
2. User completes OAuth login in browser
|
||||
3. Callback hits `/api/oauth/[provider]/exchange`
|
||||
4. Tokens stored as a provider connection in SQLite
|
||||
5. Background job refreshes tokens before expiry
|
||||
|
||||
### Model Listing
|
||||
- `/api/models` — Dashboard endpoint, lists all defined models with aliases
|
||||
- `/v1/models` — OpenAI-compatible endpoint, lists only models from active providers
|
||||
|
||||
## Important Notes for LLMs
|
||||
|
||||
1. **Two model endpoints exist:** `/api/models` (dashboard, all models) and `/v1/models` (OpenAI-compatible, active only). Don't confuse them.
|
||||
|
||||
2. **Provider IDs vs aliases:** Providers have both an ID (`claude`, `github`) and a short alias (`cc`, `gh`). Models are referenced as `alias/model-name` (e.g., `cc/claude-opus-4-6`).
|
||||
|
||||
3. **The `open-sse/` directory is a separate npm workspace** with its own config, handlers, and translators. It handles the actual SSE streaming and format translation.
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database migrations:** SQLite schema is managed inline in `src/lib/db/core.ts` and `src/lib/db/providers.ts`. No migration framework — schema changes are applied on startup.
|
||||
|
||||
6. **Tests use Node.js built-in test runner:** Run `npm test` or `node --test tests/unit/*.test.mjs`. Playwright is used for E2E tests.
|
||||
|
||||
7. **The proxy pipeline is in `src/sse/`**, not in `src/app/api/v1/`. The API routes in `src/app/api/v1/` delegate to the SSE server running on a separate Express instance.
|
||||
|
||||
## Links
|
||||
|
||||
- Repository: https://github.com/diegosouzapw/OmniRoute
|
||||
- Website: https://omniroute.online
|
||||
- npm: https://www.npmjs.com/package/omniroute
|
||||
- Docker Hub: https://hub.docker.com/r/diegosouzapw/omniroute
|
||||
- Documentation: See `/docs/` directory
|
||||
@@ -1,16 +1,12 @@
|
||||
import createNextIntlPlugin from "next-intl/plugin";
|
||||
|
||||
const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
turbopack: {},
|
||||
output: "standalone",
|
||||
serverExternalPackages: ["better-sqlite3"],
|
||||
transpilePackages: ["@omniroute/open-sse"],
|
||||
allowedDevOrigins: ["192.168.*"],
|
||||
typescript: {
|
||||
// TODO: Re-enable after fixing all sub-component useTranslations scope issues
|
||||
// Migration Phase: ignore TS errors during build.
|
||||
// Remove after all 984 type errors are resolved.
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
images: {
|
||||
@@ -67,4 +63,4 @@ const nextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
export default withNextIntl(nextConfig);
|
||||
export default nextConfig;
|
||||
|
||||
@@ -43,7 +43,6 @@ export interface RegistryEntry {
|
||||
extraHeaders?: Record<string, string>;
|
||||
oauth?: RegistryOAuth;
|
||||
models: RegistryModel[];
|
||||
modelsUrl?: string;
|
||||
chatPath?: string;
|
||||
clientVersion?: string;
|
||||
passthroughModels?: boolean;
|
||||
@@ -108,7 +107,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
clientIdEnv: "GEMINI_OAUTH_CLIENT_ID",
|
||||
clientIdDefault: "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com",
|
||||
clientSecretEnv: "GEMINI_OAUTH_CLIENT_SECRET",
|
||||
clientSecretDefault: "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",
|
||||
clientSecretDefault: "",
|
||||
},
|
||||
models: [
|
||||
{ id: "gemini-3-pro-preview", name: "Gemini 3 Pro Preview" },
|
||||
@@ -134,7 +133,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
clientIdEnv: "GEMINI_CLI_OAUTH_CLIENT_ID",
|
||||
clientIdDefault: "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com",
|
||||
clientSecretEnv: "GEMINI_CLI_OAUTH_CLIENT_SECRET",
|
||||
clientSecretDefault: "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",
|
||||
clientSecretDefault: "",
|
||||
},
|
||||
models: [
|
||||
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" },
|
||||
@@ -212,7 +211,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
id: "iflow",
|
||||
alias: "if",
|
||||
format: "openai",
|
||||
executor: "iflow",
|
||||
executor: "default",
|
||||
baseUrl: "https://apis.iflow.cn/v1/chat/completions",
|
||||
authType: "oauth",
|
||||
authHeader: "bearer",
|
||||
@@ -223,7 +222,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
clientIdEnv: "IFLOW_OAUTH_CLIENT_ID",
|
||||
clientIdDefault: "10009311001",
|
||||
clientSecretEnv: "IFLOW_OAUTH_CLIENT_SECRET",
|
||||
clientSecretDefault: "4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW",
|
||||
clientSecretDefault: "",
|
||||
tokenUrl: "https://iflow.cn/oauth/token",
|
||||
authUrl: "https://iflow.cn/oauth",
|
||||
},
|
||||
@@ -261,15 +260,18 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
clientIdEnv: "ANTIGRAVITY_OAUTH_CLIENT_ID",
|
||||
clientIdDefault: "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com",
|
||||
clientSecretEnv: "ANTIGRAVITY_OAUTH_CLIENT_SECRET",
|
||||
clientSecretDefault: "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",
|
||||
clientSecretDefault: "",
|
||||
},
|
||||
models: [
|
||||
{ id: "claude-opus-4-6-thinking", name: "Claude Opus 4.6 Thinking" },
|
||||
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
|
||||
{ id: "gemini-3.1-pro-high", name: "Gemini 3.1 Pro High" },
|
||||
{ id: "gemini-3.1-pro-low", name: "Gemini 3.1 Pro Low" },
|
||||
{ id: "claude-opus-4-5-thinking", name: "Claude Opus 4.5 Thinking" },
|
||||
{ id: "claude-sonnet-4-5-thinking", name: "Claude Sonnet 4.5 Thinking" },
|
||||
{ id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5" },
|
||||
{ id: "gemini-3-pro-high", name: "Gemini 3 Pro High" },
|
||||
{ id: "gemini-3-pro-low", name: "Gemini 3 Pro Low" },
|
||||
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" },
|
||||
{ id: "gemini-3-flash", name: "Gemini 3 Flash" },
|
||||
{ id: "gpt-oss-120b-medium", name: "GPT OSS 120B Medium" },
|
||||
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash" },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -501,7 +503,6 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
format: "openrouter",
|
||||
executor: "openrouter",
|
||||
baseUrl: "https://api.kilo.ai/api/openrouter/chat/completions",
|
||||
modelsUrl: "https://api.kilo.ai/api/openrouter/models",
|
||||
authType: "oauth",
|
||||
authHeader: "Authorization",
|
||||
authPrefix: "Bearer ",
|
||||
@@ -510,32 +511,14 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
pollUrlBase: "https://api.kilo.ai/api/device-auth/codes",
|
||||
},
|
||||
models: [
|
||||
{ id: "openrouter/free", name: "Free Models Router" },
|
||||
{ id: "qwen/qwen3-vl-235b-a22b-thinking", name: "Qwen3 VL 235B A22B Thinking" },
|
||||
{ id: "qwen/qwen3-235b-a22b-thinking-2507", name: "Qwen3 235B A22B Thinking 2507" },
|
||||
{ id: "qwen/qwen3-vl-30b-a3b-thinking", name: "Qwen3 VL 30B A3B Thinking" },
|
||||
{ id: "stepfun/step-3.5-flash:free", name: "StepFun Step 3.5 Flash" },
|
||||
{ id: "arcee-ai/trinity-large-preview:free", name: "Arcee AI Trinity Large Preview" },
|
||||
{ id: "openai/gpt-4o-mini", name: "GPT-4o Mini" },
|
||||
{ id: "openai/gpt-4.1-nano", name: "GPT-4.1 Nano" },
|
||||
{ id: "openai/gpt-5-nano", name: "GPT-5 Nano" },
|
||||
{ id: "openai/gpt-5-mini", name: "GPT-5 Mini" },
|
||||
{ id: "anthropic/claude-3-haiku", name: "Claude 3 Haiku" },
|
||||
{ id: "google/gemini-2.0-flash", name: "Gemini 2.0 Flash" },
|
||||
{ id: "google/gemini-2.5-flash-lite", name: "Gemini 2.5 Flash Lite" },
|
||||
{ id: "deepseek/deepseek-chat-v3.1", name: "DeepSeek V3.1" },
|
||||
{ id: "deepseek/deepseek-v3.2", name: "DeepSeek V3.2" },
|
||||
{ id: "meta-llama/llama-3.3-70b-instruct", name: "Llama 3.3 70B" },
|
||||
{ id: "meta-llama/llama-4-scout", name: "Llama 4 Scout" },
|
||||
{ id: "meta-llama/llama-4-maverick", name: "Llama 4 Maverick" },
|
||||
{ id: "qwen/qwen3-8b", name: "Qwen3 8B" },
|
||||
{ id: "qwen/qwen3-32b", name: "Qwen3 32B" },
|
||||
{ id: "qwen/qwen3-coder", name: "Qwen3 Coder 480B" },
|
||||
{ id: "qwen/qwq-32b", name: "QwQ 32B" },
|
||||
{ id: "mistralai/mistral-small-24b-instruct-2501", name: "Mistral Small 3" },
|
||||
{ id: "mistralai/mistral-7b-instruct", name: "Mistral 7B" },
|
||||
{ id: "x-ai/grok-code-fast-1", name: "Grok Code Fast 1" },
|
||||
{ id: "moonshotai/kimi-k2.5", name: "Kimi K2.5" },
|
||||
{ id: "anthropic/claude-sonnet-4-20250514", name: "Claude Sonnet 4" },
|
||||
{ id: "anthropic/claude-opus-4-20250514", name: "Claude Opus 4" },
|
||||
{ id: "google/gemini-2.5-pro", name: "Gemini 2.5 Pro" },
|
||||
{ id: "google/gemini-2.5-flash", name: "Gemini 2.5 Flash" },
|
||||
{ id: "openai/gpt-4.1", name: "GPT-4.1" },
|
||||
{ id: "openai/o3", name: "o3" },
|
||||
{ id: "deepseek/deepseek-chat", name: "DeepSeek Chat" },
|
||||
{ id: "deepseek/deepseek-reasoner", name: "DeepSeek Reasoner" },
|
||||
],
|
||||
passthroughModels: true,
|
||||
},
|
||||
|
||||
@@ -4,15 +4,6 @@ import { PROVIDERS, OAUTH_ENDPOINTS, HTTP_STATUS } from "../config/constants.ts"
|
||||
|
||||
const MAX_RETRY_AFTER_MS = 10000;
|
||||
|
||||
/**
|
||||
* Strip any provider prefix (e.g. "antigravity/model" → "model").
|
||||
* Ensures the model name sent to the upstream API never contains a routing prefix.
|
||||
*/
|
||||
function cleanModelName(model: string): string {
|
||||
if (!model) return model;
|
||||
return model.includes("/") ? model.split("/").pop()! : model;
|
||||
}
|
||||
|
||||
export class AntigravityExecutor extends BaseExecutor {
|
||||
constructor() {
|
||||
super("antigravity", PROVIDERS.antigravity);
|
||||
@@ -69,12 +60,10 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||
: body.request?.toolConfig,
|
||||
};
|
||||
|
||||
const upstreamModel = cleanModelName(model);
|
||||
|
||||
return {
|
||||
...body,
|
||||
project: projectId,
|
||||
model: upstreamModel,
|
||||
model: model,
|
||||
userAgent: "antigravity",
|
||||
requestType: "agent",
|
||||
requestId: `agent-${crypto.randomUUID()}`,
|
||||
|
||||
@@ -4,8 +4,7 @@ import { PROVIDERS } from "../config/constants.ts";
|
||||
|
||||
/**
|
||||
* Codex Executor - handles OpenAI Codex API (Responses API format)
|
||||
* Automatically injects default instructions if missing.
|
||||
* IMPORTANT: Includes chatgpt-account-id header for workspace binding.
|
||||
* Automatically injects default instructions if missing
|
||||
*/
|
||||
export class CodexExecutor extends BaseExecutor {
|
||||
constructor() {
|
||||
@@ -15,18 +14,9 @@ export class CodexExecutor extends BaseExecutor {
|
||||
/**
|
||||
* Codex Responses endpoint is SSE-first.
|
||||
* Always request event-stream from upstream, even when client requested stream=false.
|
||||
* Includes chatgpt-account-id header for strict workspace binding.
|
||||
*/
|
||||
buildHeaders(credentials, stream = true) {
|
||||
const headers = super.buildHeaders(credentials, true);
|
||||
|
||||
// Add workspace binding header if workspaceId is persisted
|
||||
const workspaceId = credentials?.providerSpecificData?.workspaceId;
|
||||
if (workspaceId) {
|
||||
headers["chatgpt-account-id"] = workspaceId;
|
||||
}
|
||||
|
||||
return headers;
|
||||
return super.buildHeaders(credentials, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import crypto from "crypto";
|
||||
import { BaseExecutor } from "./base.ts";
|
||||
import { PROVIDERS } from "../config/constants.ts";
|
||||
|
||||
/**
|
||||
* IFlowExecutor - Executor for iFlow API with HMAC-SHA256 signature.
|
||||
*
|
||||
* iFlow requires custom headers including a session ID, timestamp,
|
||||
* and an HMAC-SHA256 signature for request authentication.
|
||||
* Without these headers, the API returns a 406 error.
|
||||
*
|
||||
* Fixes: https://github.com/diegosouzapw/OmniRoute/issues/114
|
||||
*/
|
||||
export class IFlowExecutor extends BaseExecutor {
|
||||
constructor() {
|
||||
super("iflow", PROVIDERS.iflow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create iFlow signature using HMAC-SHA256
|
||||
* @param userAgent - User agent string
|
||||
* @param sessionID - Session ID
|
||||
* @param timestamp - Unix timestamp in milliseconds
|
||||
* @param apiKey - API key for signing
|
||||
* @returns Hex-encoded signature
|
||||
*/
|
||||
createIFlowSignature(
|
||||
userAgent: string,
|
||||
sessionID: string,
|
||||
timestamp: number,
|
||||
apiKey: string
|
||||
): string {
|
||||
if (!apiKey) return "";
|
||||
const payload = `${userAgent}:${sessionID}:${timestamp}`;
|
||||
const hmac = crypto.createHmac("sha256", apiKey);
|
||||
hmac.update(payload);
|
||||
return hmac.digest("hex");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build headers with iFlow-specific HMAC-SHA256 signature.
|
||||
* Includes session-id, x-iflow-timestamp, and x-iflow-signature.
|
||||
*/
|
||||
buildHeaders(credentials: any, stream = true) {
|
||||
// Generate session ID and timestamp
|
||||
const sessionID = `session-${crypto.randomUUID()}`;
|
||||
const timestamp = Date.now();
|
||||
|
||||
// Get user agent from config
|
||||
const userAgent = this.config.headers?.["User-Agent"] || "iFlow-Cli";
|
||||
|
||||
// Get API key (prefer apiKey, fallback to accessToken)
|
||||
const apiKey = credentials.apiKey || credentials.accessToken || "";
|
||||
|
||||
// Create HMAC-SHA256 signature
|
||||
const signature = this.createIFlowSignature(userAgent, sessionID, timestamp, apiKey);
|
||||
|
||||
// Build headers
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
...this.config.headers,
|
||||
"session-id": sessionID,
|
||||
"x-iflow-timestamp": timestamp.toString(),
|
||||
"x-iflow-signature": signature,
|
||||
};
|
||||
|
||||
// Add authorization
|
||||
if (credentials.apiKey) {
|
||||
headers["Authorization"] = `Bearer ${credentials.apiKey}`;
|
||||
} else if (credentials.accessToken) {
|
||||
headers["Authorization"] = `Bearer ${credentials.accessToken}`;
|
||||
}
|
||||
|
||||
// Add streaming header
|
||||
if (stream) {
|
||||
headers["Accept"] = "text/event-stream";
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build URL for iFlow API — uses baseUrl directly.
|
||||
*/
|
||||
buildUrl(model: string, stream: boolean, urlIndex = 0, credentials: any = null) {
|
||||
return this.config.baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform request body (passthrough for iFlow).
|
||||
*/
|
||||
transformRequest(model: string, body: any, stream: boolean, credentials: any) {
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
export default IFlowExecutor;
|
||||
@@ -1,7 +1,6 @@
|
||||
import { AntigravityExecutor } from "./antigravity.ts";
|
||||
import { GeminiCLIExecutor } from "./gemini-cli.ts";
|
||||
import { GithubExecutor } from "./github.ts";
|
||||
import { IFlowExecutor } from "./iflow.ts";
|
||||
import { KiroExecutor } from "./kiro.ts";
|
||||
import { CodexExecutor } from "./codex.ts";
|
||||
import { CursorExecutor } from "./cursor.ts";
|
||||
@@ -11,7 +10,6 @@ const executors = {
|
||||
antigravity: new AntigravityExecutor(),
|
||||
"gemini-cli": new GeminiCLIExecutor(),
|
||||
github: new GithubExecutor(),
|
||||
iflow: new IFlowExecutor(),
|
||||
kiro: new KiroExecutor(),
|
||||
codex: new CodexExecutor(),
|
||||
cursor: new CursorExecutor(),
|
||||
@@ -34,7 +32,6 @@ export { BaseExecutor } from "./base.ts";
|
||||
export { AntigravityExecutor } from "./antigravity.ts";
|
||||
export { GeminiCLIExecutor } from "./gemini-cli.ts";
|
||||
export { GithubExecutor } from "./github.ts";
|
||||
export { IFlowExecutor } from "./iflow.ts";
|
||||
export { KiroExecutor } from "./kiro.ts";
|
||||
export { CodexExecutor } from "./codex.ts";
|
||||
export { CursorExecutor } from "./cursor.ts";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getCorsOrigin } from "../utils/cors.ts";
|
||||
/**
|
||||
* Audio Speech Handler (TTS)
|
||||
*
|
||||
@@ -41,10 +40,7 @@ async function handleHyperbolicSpeech(providerConfig, body, token) {
|
||||
const errText = await res.text();
|
||||
return new Response(errText, {
|
||||
status: res.status,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": getCorsOrigin(),
|
||||
},
|
||||
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,7 +52,7 @@ async function handleHyperbolicSpeech(providerConfig, body, token) {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "audio/mpeg",
|
||||
"Access-Control-Allow-Origin": getCorsOrigin(),
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -81,10 +77,7 @@ async function handleDeepgramSpeech(providerConfig, body, modelId, token) {
|
||||
const errText = await res.text();
|
||||
return new Response(errText, {
|
||||
status: res.status,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": getCorsOrigin(),
|
||||
},
|
||||
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -93,7 +86,7 @@ async function handleDeepgramSpeech(providerConfig, body, modelId, token) {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": contentType,
|
||||
"Access-Control-Allow-Origin": getCorsOrigin(),
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Transfer-Encoding": "chunked",
|
||||
},
|
||||
});
|
||||
@@ -161,10 +154,7 @@ export async function handleAudioSpeech({ body, credentials }) {
|
||||
const errText = await res.text();
|
||||
return new Response(errText, {
|
||||
status: res.status,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": getCorsOrigin(),
|
||||
},
|
||||
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -174,7 +164,7 @@ export async function handleAudioSpeech({ body, credentials }) {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": contentType,
|
||||
"Access-Control-Allow-Origin": getCorsOrigin(),
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Transfer-Encoding": "chunked",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getCorsOrigin } from "../utils/cors.ts";
|
||||
/**
|
||||
* Audio Transcription Handler
|
||||
*
|
||||
@@ -47,10 +46,7 @@ async function handleDeepgramTranscription(providerConfig, file, modelId, token)
|
||||
const errText = await res.text();
|
||||
return new Response(errText, {
|
||||
status: res.status,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": getCorsOrigin(),
|
||||
},
|
||||
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,7 +54,7 @@ async function handleDeepgramTranscription(providerConfig, file, modelId, token)
|
||||
// Transform Deepgram response to OpenAI Whisper format
|
||||
const text = data.results?.channels?.[0]?.alternatives?.[0]?.transcript || "";
|
||||
|
||||
return Response.json({ text }, { headers: { "Access-Control-Allow-Origin": getCorsOrigin() } });
|
||||
return Response.json({ text }, { headers: { "Access-Control-Allow-Origin": "*" } });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,10 +78,7 @@ async function handleAssemblyAITranscription(providerConfig, file, modelId, toke
|
||||
const errText = await uploadRes.text();
|
||||
return new Response(errText, {
|
||||
status: uploadRes.status,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": getCorsOrigin(),
|
||||
},
|
||||
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -109,10 +102,7 @@ async function handleAssemblyAITranscription(providerConfig, file, modelId, toke
|
||||
const errText = await submitRes.text();
|
||||
return new Response(errText, {
|
||||
status: submitRes.status,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": getCorsOrigin(),
|
||||
},
|
||||
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -134,7 +124,7 @@ async function handleAssemblyAITranscription(providerConfig, file, modelId, toke
|
||||
if (result.status === "completed") {
|
||||
return Response.json(
|
||||
{ text: result.text || "" },
|
||||
{ headers: { "Access-Control-Allow-Origin": getCorsOrigin() } }
|
||||
{ headers: { "Access-Control-Allow-Origin": "*" } }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -192,11 +182,7 @@ export async function handleAudioTranscription({ formData, credentials }) {
|
||||
|
||||
// Default: OpenAI/Groq-compatible multipart proxy
|
||||
const upstreamForm = new FormData();
|
||||
upstreamForm.append(
|
||||
"file",
|
||||
/** @type {Blob} */ file,
|
||||
/** @type {any} */ file.name || "audio.wav"
|
||||
);
|
||||
upstreamForm.append("file", /** @type {Blob} */ (file), /** @type {any} */ (file).name || "audio.wav");
|
||||
upstreamForm.append("model", modelId);
|
||||
|
||||
// Forward optional parameters
|
||||
@@ -209,7 +195,7 @@ export async function handleAudioTranscription({ formData, credentials }) {
|
||||
]) {
|
||||
const val = formData.get(key);
|
||||
if (val !== null && val !== undefined) {
|
||||
upstreamForm.append(key, /** @type {string} */ val);
|
||||
upstreamForm.append(key, /** @type {string} */ (val));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,10 +210,7 @@ export async function handleAudioTranscription({ formData, credentials }) {
|
||||
const errText = await res.text();
|
||||
return new Response(errText, {
|
||||
status: res.status,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": getCorsOrigin(),
|
||||
},
|
||||
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -236,7 +219,7 @@ export async function handleAudioTranscription({ formData, credentials }) {
|
||||
|
||||
return new Response(data, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": contentType, "Access-Control-Allow-Origin": getCorsOrigin() },
|
||||
headers: { "Content-Type": contentType, "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
} catch (err) {
|
||||
return errorResponse(500, `Transcription request failed: ${err.message}`);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getCorsOrigin } from "../utils/cors.ts";
|
||||
import { detectFormat, getTargetFormat } from "../services/provider.ts";
|
||||
import { translateRequest, needsTranslation } from "../translator/index.ts";
|
||||
import { FORMATS } from "../translator/formats.ts";
|
||||
@@ -25,7 +24,6 @@ import { getExecutor } from "../executors/index.ts";
|
||||
import { translateNonStreamingResponse } from "./responseTranslator.ts";
|
||||
import { extractUsageFromResponse } from "./usageExtractor.ts";
|
||||
import { parseSSEToOpenAIResponse, parseSSEToResponsesOutput } from "./sseParser.ts";
|
||||
import { sanitizeOpenAIResponse } from "./responseSanitizer.ts";
|
||||
import {
|
||||
withRateLimit,
|
||||
updateFromHeaders,
|
||||
@@ -83,7 +81,7 @@ export async function handleChatCore({
|
||||
status: cachedIdemp.status,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": getCorsOrigin(),
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"X-OmniRoute-Idempotent": "true",
|
||||
},
|
||||
}),
|
||||
@@ -110,8 +108,8 @@ export async function handleChatCore({
|
||||
const modelTargetFormat = getModelTargetFormat(alias, model);
|
||||
const targetFormat = modelTargetFormat || getTargetFormat(provider);
|
||||
|
||||
// Default to false unless client explicitly sets stream: true (OpenAI spec compliant)
|
||||
const stream = body.stream === true;
|
||||
// Default to streaming unless client explicitly sets stream: false
|
||||
const stream = body.stream !== false;
|
||||
|
||||
// ── Phase 9.1: Semantic cache check (non-streaming, temp=0 only) ──
|
||||
if (isCacheable(body, clientRawRequest?.headers)) {
|
||||
@@ -124,7 +122,7 @@ export async function handleChatCore({
|
||||
response: new Response(JSON.stringify(cached), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": getCorsOrigin(),
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"X-OmniRoute-Cache": "HIT",
|
||||
},
|
||||
}),
|
||||
@@ -190,7 +188,7 @@ export async function handleChatCore({
|
||||
status: statusCode,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": getCorsOrigin(),
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
}
|
||||
),
|
||||
@@ -473,17 +471,10 @@ export async function handleChatCore({
|
||||
}
|
||||
|
||||
// Translate response to client's expected format (usually OpenAI)
|
||||
let translatedResponse = needsTranslation(targetFormat, sourceFormat)
|
||||
const translatedResponse = needsTranslation(targetFormat, sourceFormat)
|
||||
? translateNonStreamingResponse(responseBody, targetFormat, sourceFormat)
|
||||
: responseBody;
|
||||
|
||||
// Sanitize response for OpenAI SDK compatibility
|
||||
// Strips non-standard fields (x_groq, usage_breakdown, service_tier, etc.)
|
||||
// Extracts <think> tags into reasoning_content
|
||||
if (sourceFormat === FORMATS.OPENAI) {
|
||||
translatedResponse = sanitizeOpenAIResponse(translatedResponse);
|
||||
}
|
||||
|
||||
// Add buffer and filter usage for client (to prevent CLI context errors)
|
||||
if (translatedResponse?.usage) {
|
||||
const buffered = addBufferToUsage(translatedResponse.usage);
|
||||
@@ -515,7 +506,7 @@ export async function handleChatCore({
|
||||
response: new Response(JSON.stringify(translatedResponse), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": getCorsOrigin(),
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"X-OmniRoute-Cache": "MISS",
|
||||
},
|
||||
}),
|
||||
@@ -533,7 +524,7 @@ export async function handleChatCore({
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
"Access-Control-Allow-Origin": getCorsOrigin(),
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
};
|
||||
|
||||
// Create transform stream with logger for streaming response
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getCorsOrigin } from "../utils/cors.ts";
|
||||
/**
|
||||
* Moderation Handler
|
||||
*
|
||||
@@ -56,16 +55,13 @@ export async function handleModeration({ body, credentials }) {
|
||||
const errText = await res.text();
|
||||
return new Response(errText, {
|
||||
status: res.status,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": getCorsOrigin(),
|
||||
},
|
||||
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
return Response.json(data, {
|
||||
headers: { "Access-Control-Allow-Origin": getCorsOrigin() },
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
} catch (err) {
|
||||
return errorResponse(500, `Moderation request failed: ${err.message}`);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getCorsOrigin } from "../utils/cors.ts";
|
||||
/**
|
||||
* Rerank Handler
|
||||
*
|
||||
@@ -130,7 +129,7 @@ export async function handleRerank({
|
||||
const result = transformResponseFromProvider(providerConfig, data);
|
||||
|
||||
return Response.json(result, {
|
||||
headers: { "Access-Control-Allow-Origin": getCorsOrigin() },
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
} catch (err) {
|
||||
return errorResponse(500, `Rerank request failed: ${err.message}`);
|
||||
|
||||
@@ -1,269 +0,0 @@
|
||||
/**
|
||||
* Response Sanitizer — Normalizes LLM responses to strict OpenAI SDK format.
|
||||
*
|
||||
* Fixes Issues:
|
||||
* 1. Strips non-standard fields (x_groq, usage_breakdown, service_tier) that
|
||||
* break OpenAI Python SDK v1.83+ Pydantic validation (returns str instead of object)
|
||||
* 2. Extracts <think> tags from thinking models into reasoning_content
|
||||
* 3. Normalizes response id, object, and usage fields
|
||||
* 4. Converts developer role → system for non-OpenAI providers
|
||||
*/
|
||||
|
||||
// ── Standard OpenAI ChatCompletion fields ──────────────────────────────────
|
||||
const ALLOWED_TOP_LEVEL_FIELDS = new Set([
|
||||
"id",
|
||||
"object",
|
||||
"created",
|
||||
"model",
|
||||
"choices",
|
||||
"usage",
|
||||
"system_fingerprint",
|
||||
]);
|
||||
|
||||
const ALLOWED_USAGE_FIELDS = new Set([
|
||||
"prompt_tokens",
|
||||
"completion_tokens",
|
||||
"total_tokens",
|
||||
"prompt_tokens_details",
|
||||
"completion_tokens_details",
|
||||
]);
|
||||
|
||||
const ALLOWED_MESSAGE_FIELDS = new Set([
|
||||
"role",
|
||||
"content",
|
||||
"tool_calls",
|
||||
"function_call",
|
||||
"refusal",
|
||||
"reasoning_content",
|
||||
]);
|
||||
|
||||
const ALLOWED_CHOICE_FIELDS = new Set(["index", "message", "delta", "finish_reason", "logprobs"]);
|
||||
|
||||
// ── Think tag regex ────────────────────────────────────────────────────────
|
||||
// Matches <think>...</think> blocks (greedy, dotAll)
|
||||
const THINK_TAG_REGEX = /<think>([\s\S]*?)<\/think>/gi;
|
||||
|
||||
/**
|
||||
* Extract <think> blocks from text content and return separated parts.
|
||||
* @returns {{ content: string, thinking: string | null }}
|
||||
*/
|
||||
export function extractThinkingFromContent(text: string): {
|
||||
content: string;
|
||||
thinking: string | null;
|
||||
} {
|
||||
if (!text || typeof text !== "string") {
|
||||
return { content: text || "", thinking: null };
|
||||
}
|
||||
|
||||
const thinkingParts: string[] = [];
|
||||
let hasThinkTags = false;
|
||||
|
||||
const cleaned = text.replace(THINK_TAG_REGEX, (_, thinkContent) => {
|
||||
hasThinkTags = true;
|
||||
const trimmed = thinkContent.trim();
|
||||
if (trimmed) {
|
||||
thinkingParts.push(trimmed);
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
if (!hasThinkTags) {
|
||||
return { content: text, thinking: null };
|
||||
}
|
||||
|
||||
return {
|
||||
content: cleaned.trim(),
|
||||
thinking: thinkingParts.length > 0 ? thinkingParts.join("\n\n") : null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a non-streaming OpenAI ChatCompletion response.
|
||||
* Strips non-standard fields and normalizes required fields.
|
||||
*/
|
||||
export function sanitizeOpenAIResponse(body: any): any {
|
||||
if (!body || typeof body !== "object") return body;
|
||||
|
||||
// Build sanitized response with only allowed top-level fields
|
||||
const sanitized: Record<string, any> = {};
|
||||
|
||||
// Ensure required fields exist
|
||||
sanitized.id = normalizeResponseId(body.id);
|
||||
sanitized.object = body.object || "chat.completion";
|
||||
sanitized.created = body.created || Math.floor(Date.now() / 1000);
|
||||
sanitized.model = body.model || "unknown";
|
||||
|
||||
// Sanitize choices
|
||||
if (Array.isArray(body.choices)) {
|
||||
sanitized.choices = body.choices.map((choice: any, idx: number) => sanitizeChoice(choice, idx));
|
||||
} else {
|
||||
sanitized.choices = [];
|
||||
}
|
||||
|
||||
// Sanitize usage
|
||||
if (body.usage && typeof body.usage === "object") {
|
||||
sanitized.usage = sanitizeUsage(body.usage);
|
||||
}
|
||||
|
||||
// Keep system_fingerprint if present (it's a valid OpenAI field)
|
||||
if (body.system_fingerprint) {
|
||||
sanitized.system_fingerprint = body.system_fingerprint;
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a single choice object.
|
||||
*/
|
||||
function sanitizeChoice(choice: any, defaultIndex: number): any {
|
||||
const sanitized: Record<string, any> = {
|
||||
index: choice.index ?? defaultIndex,
|
||||
finish_reason: choice.finish_reason || null,
|
||||
};
|
||||
|
||||
// Sanitize message (non-streaming) or delta (streaming)
|
||||
if (choice.message) {
|
||||
sanitized.message = sanitizeMessage(choice.message);
|
||||
}
|
||||
if (choice.delta) {
|
||||
sanitized.delta = sanitizeMessage(choice.delta);
|
||||
}
|
||||
|
||||
// Keep logprobs if present
|
||||
if (choice.logprobs !== undefined) {
|
||||
sanitized.logprobs = choice.logprobs;
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a message object, extracting <think> tags if present.
|
||||
*/
|
||||
function sanitizeMessage(msg: any): any {
|
||||
if (!msg || typeof msg !== "object") return msg;
|
||||
|
||||
const sanitized: Record<string, any> = {};
|
||||
|
||||
// Copy only allowed fields
|
||||
if (msg.role) sanitized.role = msg.role;
|
||||
if (msg.refusal !== undefined) sanitized.refusal = msg.refusal;
|
||||
|
||||
// Handle content — extract <think> tags
|
||||
if (typeof msg.content === "string") {
|
||||
const { content, thinking } = extractThinkingFromContent(msg.content);
|
||||
sanitized.content = content;
|
||||
|
||||
// Set reasoning_content from <think> tags (if not already set)
|
||||
if (thinking && !msg.reasoning_content) {
|
||||
sanitized.reasoning_content = thinking;
|
||||
}
|
||||
} else if (msg.content !== undefined) {
|
||||
sanitized.content = msg.content;
|
||||
}
|
||||
|
||||
// Preserve existing reasoning_content (from providers that natively support it)
|
||||
if (msg.reasoning_content && !sanitized.reasoning_content) {
|
||||
sanitized.reasoning_content = msg.reasoning_content;
|
||||
}
|
||||
|
||||
// Preserve tool_calls
|
||||
if (msg.tool_calls) {
|
||||
sanitized.tool_calls = msg.tool_calls;
|
||||
}
|
||||
|
||||
// Preserve function_call (legacy)
|
||||
if (msg.function_call) {
|
||||
sanitized.function_call = msg.function_call;
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize usage object — keep only standard fields.
|
||||
*/
|
||||
function sanitizeUsage(usage: any): any {
|
||||
if (!usage || typeof usage !== "object") return usage;
|
||||
|
||||
const sanitized: Record<string, any> = {};
|
||||
for (const key of ALLOWED_USAGE_FIELDS) {
|
||||
if (usage[key] !== undefined) {
|
||||
sanitized[key] = usage[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure required fields
|
||||
if (sanitized.prompt_tokens === undefined) sanitized.prompt_tokens = 0;
|
||||
if (sanitized.completion_tokens === undefined) sanitized.completion_tokens = 0;
|
||||
if (sanitized.total_tokens === undefined) {
|
||||
sanitized.total_tokens = sanitized.prompt_tokens + sanitized.completion_tokens;
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize response ID to use chatcmpl- prefix.
|
||||
*/
|
||||
function normalizeResponseId(id: any): string {
|
||||
if (!id || typeof id !== "string") {
|
||||
return `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 29)}`;
|
||||
}
|
||||
// Already correct format
|
||||
if (id.startsWith("chatcmpl-")) return id;
|
||||
// Keep custom IDs but don't break them
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a streaming SSE chunk for passthrough mode.
|
||||
* Lighter than full sanitization — only strips problematic extra fields.
|
||||
*/
|
||||
export function sanitizeStreamingChunk(parsed: any): any {
|
||||
if (!parsed || typeof parsed !== "object") return parsed;
|
||||
|
||||
// Build sanitized chunk
|
||||
const sanitized: Record<string, any> = {};
|
||||
|
||||
// Keep only standard fields
|
||||
if (parsed.id !== undefined) sanitized.id = parsed.id;
|
||||
sanitized.object = parsed.object || "chat.completion.chunk";
|
||||
if (parsed.created !== undefined) sanitized.created = parsed.created;
|
||||
if (parsed.model !== undefined) sanitized.model = parsed.model;
|
||||
|
||||
// Sanitize choices with delta
|
||||
if (Array.isArray(parsed.choices)) {
|
||||
sanitized.choices = parsed.choices.map((choice: any) => {
|
||||
const c: Record<string, any> = {
|
||||
index: choice.index ?? 0,
|
||||
};
|
||||
if (choice.delta !== undefined) {
|
||||
c.delta = {};
|
||||
const delta = choice.delta;
|
||||
if (delta.role !== undefined) c.delta.role = delta.role;
|
||||
if (delta.content !== undefined) c.delta.content = delta.content;
|
||||
if (delta.reasoning_content !== undefined)
|
||||
c.delta.reasoning_content = delta.reasoning_content;
|
||||
if (delta.tool_calls !== undefined) c.delta.tool_calls = delta.tool_calls;
|
||||
if (delta.function_call !== undefined) c.delta.function_call = delta.function_call;
|
||||
}
|
||||
if (choice.finish_reason !== undefined) c.finish_reason = choice.finish_reason;
|
||||
if (choice.logprobs !== undefined) c.logprobs = choice.logprobs;
|
||||
return c;
|
||||
});
|
||||
}
|
||||
|
||||
// Sanitize usage if present
|
||||
if (parsed.usage && typeof parsed.usage === "object") {
|
||||
sanitized.usage = sanitizeUsage(parsed.usage);
|
||||
}
|
||||
|
||||
// Keep system_fingerprint if present
|
||||
if (parsed.system_fingerprint) {
|
||||
sanitized.system_fingerprint = parsed.system_fingerprint;
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getCorsOrigin } from "../utils/cors.ts";
|
||||
/**
|
||||
* Responses API Handler for Workers
|
||||
* Converts Chat Completions to Codex Responses API format
|
||||
@@ -76,7 +75,7 @@ export async function handleResponsesCore({
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
"Access-Control-Allow-Origin": getCorsOrigin(),
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -22,7 +22,9 @@ const PROVIDER_MODEL_ALIASES = {
|
||||
"gemini-3-flash": "gemini-3-flash-preview",
|
||||
"raptor-mini": "oswe-vscode-prime",
|
||||
},
|
||||
antigravity: {},
|
||||
antigravity: {
|
||||
"gemini-3-flash": "gemini-3-flash-preview",
|
||||
},
|
||||
};
|
||||
|
||||
// Reverse index: modelId -> providerIds that expose this model
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
/**
|
||||
* Role Normalizer — Converts message roles for provider compatibility.
|
||||
*
|
||||
* Fixes Issues:
|
||||
* 1. GLM/ZhipuAI rejects `system` role → merged into first `user` message
|
||||
* 2. OpenAI `developer` role not understood by non-OpenAI providers → normalized to `system`
|
||||
* 3. Some providers don't support `system` role at all → prepended to user message
|
||||
*
|
||||
* Provider capability matrix is defined here rather than in the registry to
|
||||
* avoid breaking changes to the existing RegistryEntry interface.
|
||||
*/
|
||||
|
||||
// ── Provider capabilities ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Providers that do NOT support the `system` role in messages.
|
||||
* For these, system messages are merged into the first user message.
|
||||
*
|
||||
* Note: This applies only to OpenAI-format passthrough providers.
|
||||
* Claude and Gemini have their own system message handling in dedicated translators.
|
||||
*/
|
||||
const PROVIDERS_WITHOUT_SYSTEM_ROLE = new Set([
|
||||
// Known to reject system role (from troubleshooting report)
|
||||
// GLM uses Claude format, so this is handled through claude translator
|
||||
// But if accessed through OpenAI-format providers like nvidia, it needs this:
|
||||
]);
|
||||
|
||||
/**
|
||||
* Models that are known to reject the `system` role regardless of provider.
|
||||
* Uses prefix matching (e.g., "glm-" matches "glm-4.7", "glm-4.5", etc.)
|
||||
*/
|
||||
const MODELS_WITHOUT_SYSTEM_ROLE = [
|
||||
"glm-", // ZhipuAI GLM models
|
||||
"ernie-", // Baidu ERNIE models
|
||||
];
|
||||
|
||||
/**
|
||||
* Check if a provider+model combo supports the system role.
|
||||
*/
|
||||
function supportsSystemRole(provider: string, model: string): boolean {
|
||||
if (PROVIDERS_WITHOUT_SYSTEM_ROLE.has(provider)) return false;
|
||||
|
||||
const modelLower = (model || "").toLowerCase();
|
||||
for (const prefix of MODELS_WITHOUT_SYSTEM_ROLE) {
|
||||
if (modelLower.startsWith(prefix)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the `developer` role to `system` for non-OpenAI providers.
|
||||
* OpenAI introduced `developer` as a replacement for `system` in newer models,
|
||||
* but most other providers still expect `system`.
|
||||
*
|
||||
* @param messages - Array of messages
|
||||
* @param targetFormat - The target format (e.g., "openai", "claude", "gemini")
|
||||
* @returns Modified messages array
|
||||
*/
|
||||
export function normalizeDeveloperRole(messages: any[], targetFormat: string): any[] {
|
||||
if (!Array.isArray(messages)) return messages;
|
||||
|
||||
// For OpenAI format, keep developer role as-is (it's valid)
|
||||
// For all other formats, convert developer → system
|
||||
if (targetFormat === "openai") return messages;
|
||||
|
||||
return messages.map((msg) => {
|
||||
if (msg.role === "developer") {
|
||||
return { ...msg, role: "system" };
|
||||
}
|
||||
return msg;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert `system` messages to user messages for providers that don't support
|
||||
* the system role. The system content is prepended to the first user message
|
||||
* with a clear delimiter.
|
||||
*
|
||||
* @param messages - Array of messages
|
||||
* @param provider - Provider name
|
||||
* @param model - Model name
|
||||
* @returns Modified messages array
|
||||
*/
|
||||
export function normalizeSystemRole(messages: any[], provider: string, model: string): any[] {
|
||||
if (!Array.isArray(messages) || messages.length === 0) return messages;
|
||||
if (supportsSystemRole(provider, model)) return messages;
|
||||
|
||||
// Extract system messages
|
||||
const systemMessages = messages.filter((m) => m.role === "system" || m.role === "developer");
|
||||
if (systemMessages.length === 0) return messages;
|
||||
|
||||
// Build system content string
|
||||
const systemContent = systemMessages
|
||||
.map((m) => {
|
||||
if (typeof m.content === "string") return m.content;
|
||||
if (Array.isArray(m.content)) {
|
||||
return m.content
|
||||
.filter((c: any) => c.type === "text")
|
||||
.map((c: any) => c.text)
|
||||
.join("\n");
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
|
||||
if (!systemContent) {
|
||||
return messages.filter((m) => m.role !== "system" && m.role !== "developer");
|
||||
}
|
||||
|
||||
// Remove system messages and merge into first user message
|
||||
const nonSystemMessages = messages.filter((m) => m.role !== "system" && m.role !== "developer");
|
||||
|
||||
// Find first user message and prepend system content
|
||||
const firstUserIdx = nonSystemMessages.findIndex((m) => m.role === "user");
|
||||
if (firstUserIdx >= 0) {
|
||||
const userMsg = nonSystemMessages[firstUserIdx];
|
||||
const userContent =
|
||||
typeof userMsg.content === "string"
|
||||
? userMsg.content
|
||||
: Array.isArray(userMsg.content)
|
||||
? userMsg.content
|
||||
.filter((c: any) => c.type === "text")
|
||||
.map((c: any) => c.text)
|
||||
.join("\n")
|
||||
: "";
|
||||
|
||||
nonSystemMessages[firstUserIdx] = {
|
||||
...userMsg,
|
||||
content: `[System Instructions]\n${systemContent}\n\n[User Message]\n${userContent}`,
|
||||
};
|
||||
} else {
|
||||
// No user message found — insert as a user message at the beginning
|
||||
nonSystemMessages.unshift({
|
||||
role: "user",
|
||||
content: `[System Instructions]\n${systemContent}`,
|
||||
});
|
||||
}
|
||||
|
||||
return nonSystemMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full role normalization pipeline.
|
||||
* Call this before sending the request to the provider.
|
||||
*
|
||||
* @param messages - Array of messages
|
||||
* @param provider - Provider name/id
|
||||
* @param model - Model name
|
||||
* @param targetFormat - Target API format
|
||||
* @returns Normalized messages array
|
||||
*/
|
||||
export function normalizeRoles(
|
||||
messages: any[],
|
||||
provider: string,
|
||||
model: string,
|
||||
targetFormat: string
|
||||
): any[] {
|
||||
if (!Array.isArray(messages)) return messages;
|
||||
|
||||
// Step 1: Normalize developer → system (for non-OpenAI formats)
|
||||
let result = normalizeDeveloperRole(messages, targetFormat);
|
||||
|
||||
// Step 2: Normalize system → user (for providers that don't support system role)
|
||||
result = normalizeSystemRole(result, provider, model);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -53,7 +53,7 @@ export async function getUsageForProvider(connection) {
|
||||
case "claude":
|
||||
return await getClaudeUsage(accessToken);
|
||||
case "codex":
|
||||
return await getCodexUsage(accessToken, providerSpecificData);
|
||||
return await getCodexUsage(accessToken);
|
||||
case "kiro":
|
||||
return await getKiroUsage(accessToken, providerSpecificData);
|
||||
case "qwen":
|
||||
@@ -318,11 +318,14 @@ async function getAntigravityUsage(accessToken, providerSpecificData) {
|
||||
// Filter only recommended/important models (must match PROVIDER_MODELS ag ids)
|
||||
const importantModels = [
|
||||
"claude-opus-4-6-thinking",
|
||||
"claude-sonnet-4-6",
|
||||
"gemini-3.1-pro-high",
|
||||
"gemini-3.1-pro-low",
|
||||
"claude-opus-4-5-thinking",
|
||||
"claude-opus-4-5",
|
||||
"claude-sonnet-4-5-thinking",
|
||||
"claude-sonnet-4-5",
|
||||
"gemini-3-pro-high",
|
||||
"gemini-3-pro-low",
|
||||
"gemini-3-flash",
|
||||
"gpt-oss-120b-medium",
|
||||
"gemini-2.5-flash",
|
||||
];
|
||||
|
||||
for (const [modelKey, info] of Object.entries(data.models) as [string, any][]) {
|
||||
@@ -480,26 +483,15 @@ async function getClaudeUsage(accessToken) {
|
||||
|
||||
/**
|
||||
* Codex (OpenAI) Usage - Fetch from ChatGPT backend API
|
||||
* IMPORTANT: Uses persisted workspaceId from OAuth to ensure correct workspace binding.
|
||||
* No fallback to other workspaces - strict binding to user's selected workspace.
|
||||
*/
|
||||
async function getCodexUsage(accessToken, providerSpecificData: Record<string, any> = {}) {
|
||||
async function getCodexUsage(accessToken) {
|
||||
try {
|
||||
// Use persisted workspace ID from OAuth - NO FALLBACK
|
||||
const accountId = providerSpecificData?.workspaceId || null;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
};
|
||||
if (accountId) {
|
||||
headers["chatgpt-account-id"] = accountId;
|
||||
}
|
||||
|
||||
const response = await fetch(CODEX_CONFIG.usageUrl, {
|
||||
method: "GET",
|
||||
headers,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -508,75 +500,38 @@ async function getCodexUsage(accessToken, providerSpecificData: Record<string, a
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Helper to get field with snake_case/camelCase fallback
|
||||
const getField = (obj: any, snakeKey: string, camelKey: string) =>
|
||||
obj?.[snakeKey] ?? obj?.[camelKey] ?? null;
|
||||
// Parse rate limit info
|
||||
const rateLimit = data.rate_limit || {};
|
||||
const primaryWindow = rateLimit.primary_window || {};
|
||||
const secondaryWindow = rateLimit.secondary_window || {};
|
||||
|
||||
// Parse rate limit info (supports both snake_case and camelCase)
|
||||
const rateLimit = getField(data, "rate_limit", "rateLimit") || {};
|
||||
const primaryWindow = getField(rateLimit, "primary_window", "primaryWindow") || {};
|
||||
const secondaryWindow = getField(rateLimit, "secondary_window", "secondaryWindow") || {};
|
||||
|
||||
// Parse reset times (reset_at is Unix timestamp in seconds)
|
||||
const parseWindowReset = (window: any) => {
|
||||
const resetAt = getField(window, "reset_at", "resetAt");
|
||||
const resetAfterSeconds = getField(window, "reset_after_seconds", "resetAfterSeconds");
|
||||
if (resetAt) return parseResetTime(resetAt * 1000);
|
||||
if (resetAfterSeconds) return parseResetTime(Date.now() + resetAfterSeconds * 1000);
|
||||
return null;
|
||||
};
|
||||
|
||||
// Build quota windows
|
||||
const quotas: Record<string, any> = {};
|
||||
|
||||
// Primary window (5-hour)
|
||||
if (Object.keys(primaryWindow).length > 0) {
|
||||
quotas.session = {
|
||||
used: getField(primaryWindow, "used_percent", "usedPercent") || 0,
|
||||
total: 100,
|
||||
remaining: 100 - (getField(primaryWindow, "used_percent", "usedPercent") || 0),
|
||||
resetAt: parseWindowReset(primaryWindow),
|
||||
unlimited: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Secondary window (weekly)
|
||||
if (Object.keys(secondaryWindow).length > 0) {
|
||||
quotas.weekly = {
|
||||
used: getField(secondaryWindow, "used_percent", "usedPercent") || 0,
|
||||
total: 100,
|
||||
remaining: 100 - (getField(secondaryWindow, "used_percent", "usedPercent") || 0),
|
||||
resetAt: parseWindowReset(secondaryWindow),
|
||||
unlimited: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Code review rate limit (3rd window — differs per plan: Plus/Pro/Team)
|
||||
const codeReviewRateLimit =
|
||||
getField(data, "code_review_rate_limit", "codeReviewRateLimit") || {};
|
||||
const codeReviewWindow = getField(codeReviewRateLimit, "primary_window", "primaryWindow") || {};
|
||||
|
||||
// Only include code review quota if the API returned data for it
|
||||
const codeReviewUsedPercent = getField(codeReviewWindow, "used_percent", "usedPercent");
|
||||
const codeReviewRemainingCount = getField(
|
||||
codeReviewWindow,
|
||||
"remaining_count",
|
||||
"remainingCount"
|
||||
// Parse reset dates (reset_at is Unix timestamp in seconds, multiply by 1000 for ms)
|
||||
const sessionResetAt = parseResetTime(
|
||||
primaryWindow.reset_at ? primaryWindow.reset_at * 1000 : null
|
||||
);
|
||||
const weeklyResetAt = parseResetTime(
|
||||
secondaryWindow.reset_at ? secondaryWindow.reset_at * 1000 : null
|
||||
);
|
||||
if (codeReviewUsedPercent !== null || codeReviewRemainingCount !== null) {
|
||||
quotas.code_review = {
|
||||
used: codeReviewUsedPercent || 0,
|
||||
total: 100,
|
||||
remaining: 100 - (codeReviewUsedPercent || 0),
|
||||
resetAt: parseWindowReset(codeReviewWindow),
|
||||
unlimited: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
plan: getField(data, "plan_type", "planType") || "unknown",
|
||||
limitReached: getField(rateLimit, "limit_reached", "limitReached") || false,
|
||||
quotas,
|
||||
plan: data.plan_type || "unknown",
|
||||
limitReached: rateLimit.limit_reached || false,
|
||||
quotas: {
|
||||
session: {
|
||||
used: primaryWindow.used_percent || 0,
|
||||
total: 100,
|
||||
remaining: 100 - (primaryWindow.used_percent || 0),
|
||||
resetAt: sessionResetAt,
|
||||
unlimited: false,
|
||||
},
|
||||
weekly: {
|
||||
used: secondaryWindow.used_percent || 0,
|
||||
total: 100,
|
||||
remaining: 100 - (secondaryWindow.used_percent || 0),
|
||||
resetAt: weeklyResetAt,
|
||||
unlimited: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to fetch Codex usage: ${error.message}`);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { prepareClaudeRequest } from "./helpers/claudeHelper.ts";
|
||||
import { filterToOpenAIFormat } from "./helpers/openaiHelper.ts";
|
||||
import { normalizeThinkingConfig } from "../services/provider.ts";
|
||||
import { applyThinkingBudget } from "../services/thinkingBudget.ts";
|
||||
import { normalizeRoles } from "../services/roleNormalizer.ts";
|
||||
|
||||
// Registry for translators.
|
||||
// NOTE: translator modules import this file and call register() at module-load time.
|
||||
@@ -133,11 +132,6 @@ export function translateRequest(
|
||||
// Fix missing tool responses (insert empty tool_result if needed)
|
||||
fixMissingToolResponses(result);
|
||||
|
||||
// Normalize roles: developer→system for non-OpenAI, system→user for incompatible models
|
||||
if (result.messages && Array.isArray(result.messages)) {
|
||||
result.messages = normalizeRoles(result.messages, provider || "", model || "", targetFormat);
|
||||
}
|
||||
|
||||
// If same format, skip translation steps
|
||||
if (sourceFormat !== targetFormat) {
|
||||
// Check for direct translation path first (e.g., Claude → Gemini)
|
||||
|
||||
@@ -199,24 +199,6 @@ function openaiToGeminiBase(model, body, stream) {
|
||||
}
|
||||
}
|
||||
|
||||
// Convert response_format to Gemini's responseMimeType/responseSchema
|
||||
if (body.response_format) {
|
||||
if (body.response_format.type === "json_schema" && body.response_format.json_schema) {
|
||||
result.generationConfig.responseMimeType = "application/json";
|
||||
// Extract the schema (may be nested under .schema key)
|
||||
const schema = body.response_format.json_schema.schema || body.response_format.json_schema;
|
||||
if (schema && typeof schema === "object") {
|
||||
// Remove unsupported keywords for Gemini (it uses a subset of JSON Schema)
|
||||
const { $schema, additionalProperties, ...cleanSchema } = schema;
|
||||
result.generationConfig.responseSchema = cleanSchema;
|
||||
}
|
||||
} else if (body.response_format.type === "json_object") {
|
||||
result.generationConfig.responseMimeType = "application/json";
|
||||
} else if (body.response_format.type === "text") {
|
||||
result.generationConfig.responseMimeType = "text/plain";
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -271,11 +253,9 @@ export function openaiToGeminiCLIRequest(model, body, stream) {
|
||||
function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigravity = false) {
|
||||
const projectId = credentials?.projectId || generateProjectId();
|
||||
|
||||
const cleanModel = model.includes("/") ? model.split("/").pop()! : model;
|
||||
|
||||
const envelope: Record<string, any> = {
|
||||
project: projectId,
|
||||
model: cleanModel,
|
||||
model: model,
|
||||
userAgent: isAntigravity ? "antigravity" : "gemini-cli",
|
||||
requestId: isAntigravity ? `agent-${generateUUID()}` : generateRequestId(),
|
||||
request: {
|
||||
@@ -317,11 +297,9 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra
|
||||
function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = null) {
|
||||
const projectId = credentials?.projectId || generateProjectId();
|
||||
|
||||
const cleanModel = model.includes("/") ? model.split("/").pop()! : model;
|
||||
|
||||
const envelope: Record<string, any> = {
|
||||
project: projectId,
|
||||
model: cleanModel,
|
||||
model: model,
|
||||
userAgent: "antigravity",
|
||||
requestId: `agent-${generateUUID()}`,
|
||||
requestType: "agent",
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"noEmit": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"strict": false,
|
||||
@@ -19,5 +18,8 @@
|
||||
"@omniroute/open-sse/*": ["./open-sse/*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.js"]
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.js"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getCorsOrigin } from "./cors.ts";
|
||||
import { detectFormat } from "../services/provider.ts";
|
||||
import { translateResponse, initState } from "../translator/index.ts";
|
||||
import { FORMATS } from "../translator/formats.ts";
|
||||
@@ -123,7 +122,7 @@ function createNonStreamingResponse(sourceFormat, model) {
|
||||
response: new Response(JSON.stringify(openaiResponse), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": getCorsOrigin(),
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -157,7 +156,7 @@ function createNonStreamingResponse(sourceFormat, model) {
|
||||
response: new Response(JSON.stringify(finalResponse), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": getCorsOrigin(),
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -205,7 +204,7 @@ function createStreamingResponse(sourceFormat, model) {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
"Access-Control-Allow-Origin": getCorsOrigin(),
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* CORS configuration for open-sse handlers.
|
||||
*
|
||||
* Reads `CORS_ORIGIN` env var (default: "*") so that all handlers
|
||||
* use the same configurable origin. Equivalent to src/shared/utils/cors.ts
|
||||
* for the open-sse package boundary.
|
||||
*/
|
||||
|
||||
const CORS_ORIGIN = process.env.CORS_ORIGIN || "*";
|
||||
|
||||
export const CORS_HEADERS: Record<string, string> = {
|
||||
"Access-Control-Allow-Origin": CORS_ORIGIN,
|
||||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type, Authorization, x-api-key, anthropic-version",
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns just the origin header for merging into existing header objects.
|
||||
*/
|
||||
export function getCorsOrigin(): string {
|
||||
return CORS_ORIGIN;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getCorsOrigin } from "./cors.ts";
|
||||
import { ERROR_TYPES, DEFAULT_ERROR_MESSAGES } from "../config/constants.ts";
|
||||
|
||||
/**
|
||||
@@ -34,7 +33,7 @@ export function errorResponse(statusCode, message) {
|
||||
status: statusCode,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": getCorsOrigin(),
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -132,11 +131,7 @@ 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: number,
|
||||
message: string,
|
||||
retryAfterMs: number | null = null
|
||||
) {
|
||||
export function createErrorResult(statusCode: number, message: string, retryAfterMs: number | null = null) {
|
||||
const result: Record<string, any> = {
|
||||
success: false,
|
||||
status: statusCode,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getCorsOrigin } from "./cors.ts";
|
||||
// Transform OpenAI SSE stream to Ollama JSON lines format
|
||||
export function transformToOllama(response, model) {
|
||||
let buffer = "";
|
||||
@@ -86,9 +85,6 @@ export function transformToOllama(response, model) {
|
||||
});
|
||||
|
||||
return new Response(response.body.pipeThrough(transform), {
|
||||
headers: {
|
||||
"Content-Type": "application/x-ndjson",
|
||||
"Access-Control-Allow-Origin": getCorsOrigin(),
|
||||
},
|
||||
headers: { "Content-Type": "application/x-ndjson", "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,10 +12,6 @@ import {
|
||||
} from "./usageTracking.ts";
|
||||
import { parseSSELine, hasValuableContent, fixInvalidId, formatSSE } from "./streamHelpers.ts";
|
||||
import { STREAM_IDLE_TIMEOUT_MS, HTTP_STATUS } from "../config/constants.ts";
|
||||
import {
|
||||
sanitizeStreamingChunk,
|
||||
extractThinkingFromContent,
|
||||
} from "../handlers/responseSanitizer.ts";
|
||||
|
||||
export { COLORS, formatSSE };
|
||||
|
||||
@@ -134,10 +130,7 @@ export function createSSEStream(options: any = {}) {
|
||||
|
||||
if (trimmed.startsWith("data:") && trimmed.slice(5).trim() !== "[DONE]") {
|
||||
try {
|
||||
let parsed = JSON.parse(trimmed.slice(5).trim());
|
||||
|
||||
// Sanitize: strip non-standard fields for OpenAI SDK compatibility
|
||||
parsed = sanitizeStreamingChunk(parsed);
|
||||
const parsed = JSON.parse(trimmed.slice(5).trim());
|
||||
|
||||
const idFixed = fixInvalidId(parsed);
|
||||
|
||||
@@ -146,16 +139,6 @@ export function createSSEStream(options: any = {}) {
|
||||
}
|
||||
|
||||
const delta = parsed.choices?.[0]?.delta;
|
||||
|
||||
// Extract <think> tags from streaming content
|
||||
if (delta?.content && typeof delta.content === "string") {
|
||||
const { content, thinking } = extractThinkingFromContent(delta.content);
|
||||
delta.content = content;
|
||||
if (thinking && !delta.reasoning_content) {
|
||||
delta.reasoning_content = thinking;
|
||||
}
|
||||
}
|
||||
|
||||
const content = delta?.content || delta?.reasoning_content;
|
||||
if (content && typeof content === "string") {
|
||||
totalContentLength += content.length;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "1.4.9",
|
||||
"version": "0.9.0",
|
||||
"description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
@@ -17,7 +17,7 @@
|
||||
"open-sse"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <24.0.0"
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"ai",
|
||||
@@ -47,13 +47,13 @@
|
||||
"build:cli": "node scripts/prepublish.mjs",
|
||||
"start": "next start --port 20128",
|
||||
"lint": "eslint .",
|
||||
"test": "node --import tsx/esm --test tests/unit/*.test.mjs",
|
||||
"test": "node --test tests/unit/*.test.mjs",
|
||||
"test:unit": "node --import tsx/esm --test tests/unit/*.test.mjs",
|
||||
"test:plan3": "node --test tests/unit/plan3-p0.test.mjs",
|
||||
"test:fixes": "node --test tests/unit/fixes-p1.test.mjs",
|
||||
"test:security": "node --test tests/unit/security-fase01.test.mjs",
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:coverage": "npx c8 --exclude=open-sse --check-coverage --lines 50 --functions 50 --branches 50 node --import tsx/esm --test tests/unit/*.test.mjs",
|
||||
"test:coverage": "npx c8 --check-coverage --lines 50 --functions 40 --branches 40 node --test tests/unit/*.test.mjs",
|
||||
"test:all": "npm run test:unit && npm run test:e2e",
|
||||
"check": "npm run lint && npm run test",
|
||||
"prepublishOnly": "npm run build:cli",
|
||||
@@ -72,7 +72,6 @@
|
||||
"lowdb": "^7.0.1",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"next": "^16.1.6",
|
||||
"next-intl": "^4.8.3",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"open": "^11.0.0",
|
||||
"ora": "^9.1.0",
|
||||
@@ -91,7 +90,7 @@
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/node": "^25.2.3",
|
||||
"@types/react": "^19.2.14",
|
||||
@@ -103,8 +102,7 @@
|
||||
"prettier": "^3.8.1",
|
||||
"tailwindcss": "^4",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.56.0"
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,mjs}": [
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Image from "next/image";
|
||||
@@ -12,8 +10,6 @@ import { AI_PROVIDERS, FREE_PROVIDERS, OAUTH_PROVIDERS } from "@/shared/constant
|
||||
import { useNotificationStore } from "@/store/notificationStore";
|
||||
|
||||
export default function HomePageClient({ machineId }) {
|
||||
const t = useTranslations("home");
|
||||
const tc = useTranslations("common");
|
||||
const [providerConnections, setProviderConnections] = useState([]);
|
||||
const [models, setModels] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -107,14 +103,14 @@ export default function HomePageClient({ machineId }) {
|
||||
}, [selectedProvider, models]);
|
||||
|
||||
const quickStartLinks = [
|
||||
{ label: t("documentation"), href: "/docs", icon: "menu_book" },
|
||||
{ label: tc("provider") + "s", href: "/dashboard/providers", icon: "dns" },
|
||||
{ label: "Documentation", href: "/docs", icon: "menu_book" },
|
||||
{ label: "Providers", href: "/dashboard/providers", icon: "dns" },
|
||||
{ label: "Combos", href: "/dashboard/combos", icon: "layers" },
|
||||
{ label: "Analytics", href: "/dashboard/analytics", icon: "analytics" },
|
||||
{ label: t("healthMonitor"), href: "/dashboard/health", icon: "health_and_safety" },
|
||||
{ label: "Health Monitor", href: "/dashboard/health", icon: "health_and_safety" },
|
||||
{ label: "CLI Tools", href: "/dashboard/cli-tools", icon: "terminal" },
|
||||
{
|
||||
label: t("reportIssue"),
|
||||
label: "Report issue",
|
||||
href: "https://github.com/diegosouzapw/OmniRoute/issues",
|
||||
external: true,
|
||||
icon: "bug_report",
|
||||
@@ -139,15 +135,17 @@ export default function HomePageClient({ machineId }) {
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">{t("quickStart")}</h2>
|
||||
<p className="text-sm text-text-muted">{t("quickStartDesc")}</p>
|
||||
<h2 className="text-lg font-semibold">Quick Start</h2>
|
||||
<p className="text-sm text-text-muted">
|
||||
Get up and running in 4 steps. Connect providers, route models, monitor everything.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/docs"
|
||||
className="hidden sm:inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border border-border text-text-muted hover:text-text-main hover:bg-bg-subtle transition-colors"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">menu_book</span>
|
||||
{t("fullDocs")}
|
||||
Full Docs
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -157,13 +155,13 @@ export default function HomePageClient({ machineId }) {
|
||||
<span className="material-symbols-outlined text-[18px]">key</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">{t("step1Title")}</span>
|
||||
<span className="font-semibold">1. Create API key</span>
|
||||
<p className="text-text-muted mt-0.5">
|
||||
Go to{" "}
|
||||
<Link href="/dashboard/endpoint" className="text-primary hover:underline">
|
||||
Endpoint
|
||||
<Link href="/dashboard/settings" className="text-primary hover:underline">
|
||||
Settings
|
||||
</Link>{" "}
|
||||
→ Registered Keys. Generate one key per environment.
|
||||
→ API Keys. Generate one key per environment.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
@@ -172,7 +170,7 @@ export default function HomePageClient({ machineId }) {
|
||||
<span className="material-symbols-outlined text-[18px]">dns</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">{t("step2Title")}</span>
|
||||
<span className="font-semibold">2. Connect providers</span>
|
||||
<p className="text-text-muted mt-0.5">
|
||||
Add accounts in{" "}
|
||||
<Link href="/dashboard/providers" className="text-primary hover:underline">
|
||||
@@ -187,7 +185,7 @@ export default function HomePageClient({ machineId }) {
|
||||
<span className="material-symbols-outlined text-[18px]">link</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">{t("step3Title")}</span>
|
||||
<span className="font-semibold">3. Point your client</span>
|
||||
<p className="text-text-muted mt-0.5">
|
||||
Set base URL to{" "}
|
||||
<code className="px-1.5 py-0.5 rounded bg-surface text-xs font-mono">
|
||||
@@ -202,7 +200,7 @@ export default function HomePageClient({ machineId }) {
|
||||
<span className="material-symbols-outlined text-[18px]">analytics</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">{t("step4Title")}</span>
|
||||
<span className="font-semibold">4. Monitor & optimize</span>
|
||||
<p className="text-text-muted mt-0.5">
|
||||
Track tokens, cost and errors in{" "}
|
||||
<Link href="/dashboard/usage" className="text-primary hover:underline">
|
||||
@@ -241,7 +239,7 @@ export default function HomePageClient({ machineId }) {
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">{t("providersOverview")}</h2>
|
||||
<h2 className="text-lg font-semibold">Providers Overview</h2>
|
||||
<p className="text-sm text-text-muted">
|
||||
{providerStats.filter((item) => item.total > 0).length} configured of{" "}
|
||||
{providerStats.length} available providers
|
||||
@@ -299,7 +297,6 @@ HomePageClient.propTypes = {
|
||||
|
||||
function ProviderOverviewCard({ item, metrics, onClick }) {
|
||||
const [imgError, setImgError] = useState(false);
|
||||
const tc = useTranslations("common");
|
||||
|
||||
const statusVariant =
|
||||
item.errors > 0 ? "text-red-500" : item.connected > 0 ? "text-green-500" : "text-text-muted";
|
||||
@@ -351,7 +348,7 @@ function ProviderOverviewCard({ item, metrics, onClick }) {
|
||||
</div>
|
||||
<p className={`text-xs ${statusVariant}`}>
|
||||
{item.total === 0
|
||||
? tc("notConfigured")
|
||||
? "Not configured"
|
||||
: `${item.connected} active · ${item.errors} error`}
|
||||
</p>
|
||||
{metrics && metrics.totalRequests > 0 && (
|
||||
@@ -404,8 +401,6 @@ function ProviderModelsModal({ provider, models, onClose }) {
|
||||
const [copiedModel, setCopiedModel] = useState(null);
|
||||
const notify = useNotificationStore();
|
||||
const router = useRouter();
|
||||
const t = useTranslations("home");
|
||||
const tc = useTranslations("common");
|
||||
|
||||
const navigateTo = (path) => {
|
||||
onClose();
|
||||
@@ -438,7 +433,7 @@ function ProviderModelsModal({ provider, models, onClose }) {
|
||||
<span className="material-symbols-outlined text-[32px] text-text-muted mb-2">
|
||||
search_off
|
||||
</span>
|
||||
<p className="text-sm text-text-muted">{t("noModelsAvailable")}</p>
|
||||
<p className="text-sm text-text-muted">No models available for this provider.</p>
|
||||
<p className="text-xs text-text-muted mt-1">
|
||||
Configure a connection first in{" "}
|
||||
<button
|
||||
@@ -465,7 +460,7 @@ function ProviderModelsModal({ provider, models, onClose }) {
|
||||
<button
|
||||
onClick={() => handleCopy(m.fullModel)}
|
||||
className="shrink-0 ml-2 p-1.5 rounded-lg text-text-muted hover:text-text-main hover:bg-bg-subtle transition-colors opacity-0 group-hover:opacity-100"
|
||||
title={t("copyModelName")}
|
||||
title="Copy model name"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">
|
||||
{copiedModel === m.fullModel ? "check" : "content_copy"}
|
||||
@@ -486,7 +481,7 @@ function ProviderModelsModal({ provider, models, onClose }) {
|
||||
className="flex-1"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">settings</span>
|
||||
{t("configureProvider")}
|
||||
Configure Provider
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={onClose}>
|
||||
Close
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
"use client";
|
||||
|
||||
export default function AnalyticsLoading() {
|
||||
return (
|
||||
<div className="space-y-6 animate-pulse p-6">
|
||||
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-40" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className="h-24 bg-gray-200 dark:bg-gray-700 rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
<div className="h-64 bg-gray-200 dark:bg-gray-700 rounded-lg" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,15 +3,15 @@
|
||||
import { useState, Suspense } from "react";
|
||||
import { UsageAnalytics, CardSkeleton, SegmentedControl } from "@/shared/components";
|
||||
import EvalsTab from "../usage/components/EvalsTab";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function AnalyticsPage() {
|
||||
const [activeTab, setActiveTab] = useState("overview");
|
||||
const t = useTranslations("analytics");
|
||||
|
||||
const tabDescriptions = {
|
||||
overview: t("overviewDescription"),
|
||||
evals: t("evalsDescription"),
|
||||
overview:
|
||||
"Monitor your API usage patterns, token consumption, costs, and activity trends across all providers and models.",
|
||||
evals:
|
||||
"Run evaluation suites to test and validate your LLM endpoints. Compare model quality, detect regressions, and benchmark latency.",
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -20,15 +20,15 @@ export default function AnalyticsPage() {
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-primary text-[28px]">analytics</span>
|
||||
{t("title")}
|
||||
Analytics
|
||||
</h1>
|
||||
<p className="text-sm text-text-muted mt-1">{tabDescriptions[activeTab]}</p>
|
||||
</div>
|
||||
|
||||
<SegmentedControl
|
||||
options={[
|
||||
{ value: "overview", label: t("overview") },
|
||||
{ value: "evals", label: t("evals") },
|
||||
{ value: "overview", label: "Overview" },
|
||||
{ value: "evals", label: "Evals" },
|
||||
]}
|
||||
value={activeTab}
|
||||
onChange={setActiveTab}
|
||||
|
||||