mirror of
https://github.com/romgX/openrelay.git
synced 2026-09-03 06:53:02 +08:00
Initial release v0.8.3
29 providers (7 IDE + 22 direct API), Anthropic + OpenAI API compatibility, IDE RPC proxy servers, Web dashboard, Free/Pro licensing.
This commit is contained in:
28
CHANGELOG.md
Normal file
28
CHANGELOG.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Changelog
|
||||
|
||||
## [0.8.3] - 2026-03-07
|
||||
|
||||
### Initial Release
|
||||
|
||||
**Providers (29 total)**
|
||||
- 7 IDE providers: Claude Desktop, Claude Code, Kiro (AWS Q), Windsurf, Antigravity, OpenCode, VS Code Copilot
|
||||
- 22 direct API providers: Groq, Cerebras, OpenRouter, SambaNova, DashScope, DeepSeek, Gemini, Mistral, xAI, SiliconFlow, Zhipu, Together, Fireworks, NVIDIA NIM, GitHub Models, Volcengine, Moonshot, Baichuan, Stepfun, MiniMax, Hunyuan, Ollama
|
||||
|
||||
**API Compatibility**
|
||||
- Anthropic Messages API (streaming + non-streaming)
|
||||
- OpenAI Chat Completions API (streaming + non-streaming)
|
||||
- Azure OpenAI compatible endpoint
|
||||
- Bidirectional format translation with full tool/function calling support
|
||||
|
||||
**Consumer Access Methods**
|
||||
- IDE RPC proxy servers: Windsurf (18766), Antigravity (18767), Cursor (18780), VS Code Copilot (18769)
|
||||
- Shell environment functions for CLI tools (Claude Code, OpenCode, Aider, Goose, Amp)
|
||||
- API key auth: `sk-or-{provider}-{hex}` format for SDK/curl access
|
||||
- Custom model groups with round-robin routing
|
||||
|
||||
**Platform**
|
||||
- macOS (ARM64 / x64) + Windows (x64)
|
||||
- Single-file binary (Node.js SEA) — no runtime dependencies
|
||||
- Web management dashboard (bilingual EN/ZH)
|
||||
- Auto-detection of installed AI apps and credentials
|
||||
- License system: Free (30 req/day) / Pro (unlimited)
|
||||
69
COMMERCIAL-LICENSE.txt
Normal file
69
COMMERCIAL-LICENSE.txt
Normal file
@@ -0,0 +1,69 @@
|
||||
OpenRelay Commercial License / 商业许可协议
|
||||
|
||||
Copyright (c) 2025-2026 OpenRelay Contributors
|
||||
|
||||
================================================================================
|
||||
ENGLISH
|
||||
================================================================================
|
||||
|
||||
This software is distributed under a dual-license model:
|
||||
|
||||
1. OPEN SOURCE FRAMEWORK (MIT License)
|
||||
The framework code (routing, proxy infrastructure, format translation, and
|
||||
configuration) is open source under the MIT License. See the LICENSE file
|
||||
for full terms.
|
||||
|
||||
2. PRO FEATURES (Commercial License)
|
||||
The following features require a valid Pro license for unlimited use:
|
||||
|
||||
- Unlimited request volume (Free tier: 30 requests/day)
|
||||
- Custom model groups with round-robin routing across multiple providers
|
||||
- Priority support
|
||||
|
||||
All other features — including all 29 providers, IDE RPC proxy servers,
|
||||
Web dashboard, and API format translation — are available in both
|
||||
Free and Pro tiers.
|
||||
|
||||
3. RESTRICTIONS
|
||||
- Decompilation, reverse engineering, and redistribution of obfuscated or
|
||||
compiled Pro feature code are prohibited without written permission.
|
||||
- Circumvention of license verification or usage limits is prohibited.
|
||||
- Redistribution of the complete package (including Pro features) requires
|
||||
a commercial redistribution agreement.
|
||||
|
||||
4. FREE TIER
|
||||
All providers and core features are available with a daily request limit.
|
||||
No Pro license is required for use within this limit.
|
||||
|
||||
For licensing inquiries: https://github.com/romgX/openrelay/issues
|
||||
|
||||
================================================================================
|
||||
中文
|
||||
================================================================================
|
||||
|
||||
本软件采用双重许可模式分发:
|
||||
|
||||
1. 开源框架(MIT 许可证)
|
||||
框架代码(路由、代理基础设施、格式转换和配置)以 MIT 许可证开源。
|
||||
完整条款见 LICENSE 文件。
|
||||
|
||||
2. Pro 功能(商业许可)
|
||||
以下功能需要有效的 Pro 许可证方可无限制使用:
|
||||
|
||||
- 无限请求量(免费版:每日 30 次)
|
||||
- 自定义模型组(跨多个 Provider 轮询路由)
|
||||
- 优先支持
|
||||
|
||||
其他所有功能——包括全部 29 个提供商、IDE RPC 代理服务器、
|
||||
Web 管理面板和 API 格式转换——在免费版和 Pro 版中均可使用。
|
||||
|
||||
3. 限制条款
|
||||
- 未经书面许可,禁止反编译、逆向工程和再分发经混淆或编译的 Pro 功能代码。
|
||||
- 禁止规避许可证验证或使用限制。
|
||||
- 再分发完整软件包(包含 Pro 功能)需要签订商业再分发协议。
|
||||
|
||||
4. 免费版
|
||||
所有提供商和核心功能均可在每日请求限额内使用。
|
||||
在此限额内无需 Pro 许可证。
|
||||
|
||||
许可咨询:https://github.com/romgX/openrelay/issues
|
||||
45
DISCLAIMER.md
Normal file
45
DISCLAIMER.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Disclaimer
|
||||
|
||||
## Nature of the Software
|
||||
|
||||
OpenRelay is a **local network proxy tool** intended for personal learning, research, and development purposes. It runs entirely on your own machine and is functionally equivalent to well-known local proxy tools such as Charles Proxy, Fiddler, mitmproxy, and Surge — it intercepts and forwards network requests on localhost.
|
||||
|
||||
OpenRelay is **not** an AI service provider. It does not generate, host, or distribute any AI content. It is a local utility that translates and forwards API requests between tools you already use.
|
||||
|
||||
## What OpenRelay Does
|
||||
|
||||
- Reads **your own** locally stored authentication credentials (cookies, tokens, API keys) from AI desktop applications **you have already installed and logged into**
|
||||
- Exposes a unified HTTP proxy on your local machine
|
||||
- Translates between API formats (Anthropic Messages API, OpenAI Chat Completions API) so that different AI clients can connect to different AI backends through a single endpoint
|
||||
|
||||
## What OpenRelay Does NOT Do
|
||||
|
||||
- **Does not generate AI content** — it only relays requests and responses between your tools and AI backends
|
||||
- **Does not bypass paywalls or access controls** — it only uses credentials you already possess
|
||||
- **Does not share accounts** — all credentials stay on your local machine
|
||||
- **Does not transmit credentials externally** — authentication data never leaves your device
|
||||
- **Does not modify or redistribute any third-party software**
|
||||
- **Does not intercept traffic from other users or devices** (unless you explicitly configure LAN access)
|
||||
|
||||
## No Affiliation
|
||||
|
||||
OpenRelay is an independent project. It is **not** affiliated with, endorsed by, or associated with Anthropic, Amazon (AWS), Google, Microsoft, Cursor, Codeium (Windsurf), or any other AI service provider mentioned in this project.
|
||||
|
||||
All product names, trademarks, and registered trademarks are the property of their respective owners.
|
||||
|
||||
## User Responsibility
|
||||
|
||||
This software is provided for **learning, research, and personal development use**. By using OpenRelay, you acknowledge and agree that:
|
||||
|
||||
1. **You bear full responsibility** for all consequences arising from your use of this software
|
||||
2. You are responsible for complying with the Terms of Service of all AI services you access through this tool
|
||||
3. You must only use credentials for accounts you own or are authorized to use
|
||||
4. You must comply with all applicable laws and regulations in your jurisdiction
|
||||
5. The authors and contributors of OpenRelay assume **no liability** for any direct, indirect, incidental, or consequential damages resulting from the use or misuse of this software
|
||||
6. This software is provided "as is" without warranty of any kind, express or implied
|
||||
|
||||
## Legal Notice
|
||||
|
||||
Local proxy tools that read the user's own credentials from their own machine operate in a well-established category of developer utilities. Users should independently evaluate the legal implications of using such tools in their specific jurisdiction and context.
|
||||
|
||||
For questions or concerns, please open an issue on GitHub.
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025-2026 OpenRelay Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
56
PRIVACY.md
Normal file
56
PRIVACY.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Privacy Policy
|
||||
|
||||
## Overview
|
||||
|
||||
OpenRelay is a local proxy tool that runs entirely on your machine. Your privacy is fundamental to its design.
|
||||
|
||||
## Credential Handling
|
||||
|
||||
OpenRelay reads authentication credentials (cookies, tokens, API keys) from AI desktop applications you have already installed and logged into. These credentials are:
|
||||
|
||||
- **Read-only** — OpenRelay reads but never modifies your stored credentials
|
||||
- **In-memory only** — credentials are held in process memory during runtime, never written to OpenRelay's own files
|
||||
- **Never transmitted** — credentials are only used to authenticate requests directly to the original AI service backends, never sent to OpenRelay servers or any third party
|
||||
|
||||
## Data Handling
|
||||
|
||||
### What stays on your machine
|
||||
|
||||
- **All authentication credentials** — read from your local filesystem, used in-memory only
|
||||
- **All AI conversations** — pass directly from your client to the AI backend; OpenRelay does not store, log, or inspect message content
|
||||
- **Configuration data** — stored locally at `~/.openrelay/`
|
||||
|
||||
### What is never collected
|
||||
|
||||
- Message content is never logged, stored, or analyzed
|
||||
- No usage telemetry, analytics, or behavioral data is collected
|
||||
- Your credentials are never sent to OpenRelay servers
|
||||
|
||||
## Network Connections
|
||||
|
||||
OpenRelay makes the following network connections:
|
||||
|
||||
1. **AI service backends** (e.g., api.anthropic.com, amazonaws.com, server.codeium.com) — to forward your requests using your own credentials
|
||||
2. **License verification server** (`license.limitlessmeto.com`, hosted on Cloudflare) — periodic license status check
|
||||
- **Sent**: anonymous device identifier and license token
|
||||
- **NOT sent**: credentials, conversation content, usage details, or personal information
|
||||
- **Purpose**: verify license tier (Free / Pro) and active device count
|
||||
3. **GitHub Releases API** (optional) — to check for software updates
|
||||
|
||||
## Local Logging
|
||||
|
||||
- The `openrelay.log` file may contain request metadata (timestamps, model names, token counts) for debugging purposes
|
||||
- Log files never contain message content, credentials, or personal information
|
||||
- Logs are stored locally and are never transmitted
|
||||
|
||||
## Data Deletion
|
||||
|
||||
To completely remove all OpenRelay data:
|
||||
|
||||
```bash
|
||||
rm -rf ~/.openrelay/
|
||||
```
|
||||
|
||||
## Contact
|
||||
|
||||
For privacy-related questions, please open an issue on GitHub.
|
||||
277
README.md
Normal file
277
README.md
Normal file
@@ -0,0 +1,277 @@
|
||||
# OpenRelay
|
||||
|
||||
**Your AI subscriptions, everywhere.** Stop paying for AI tools you can't fully use.
|
||||
|
||||
[中文说明](#中文)
|
||||
|
||||
---
|
||||
|
||||
## The Problem
|
||||
|
||||
How do you find more free AI model quotas?
|
||||
How do you connect free or paid quotas to the tools you're already using?
|
||||
How do you configure Claude Code, OpenCode, or any AI tool in one click?
|
||||
|
||||
**OpenRelay solves all of this.**
|
||||
|
||||
## What OpenRelay Actually Does
|
||||
|
||||
### 1. Auto-discover all your AI quotas
|
||||
|
||||
Launch OpenRelay and it instantly finds every AI subscription and free quota on your machine — Claude Desktop, Claude Code, Kiro, Windsurf, Antigravity, OpenCode, VS Code Copilot. All quotas at your command, zero manual configuration.
|
||||
|
||||
Plus 22 direct API providers (Groq, Gemini, DeepSeek, Mistral, OpenRouter, etc.) — enter an API key once and it's available everywhere.
|
||||
|
||||
**29 providers. One dashboard. One endpoint.**
|
||||
|
||||
### 2. Use any quota in any tool
|
||||
|
||||
Your Claude Pro subscription can now power Claude Code, Aider, Continue, Goose, Amp, or any tool that speaks Anthropic/OpenAI API:
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_BASE_URL=http://localhost:18765
|
||||
export ANTHROPIC_API_KEY=unused
|
||||
# That's it. Claude Code now uses your Claude Desktop quota.
|
||||
```
|
||||
|
||||
Want to use Kiro's free Claude Sonnet quota in Aider? Just change the URL:
|
||||
```bash
|
||||
export ANTHROPIC_BASE_URL=http://localhost:18765/kiro
|
||||
```
|
||||
|
||||
### 3. One-click setup for every CLI tool
|
||||
|
||||
No more editing `.zshrc` or juggling environment variables. Open the Web dashboard, pick a provider for each tool, flip a switch:
|
||||
|
||||
- **Claude Code** → route through Kiro (free Claude Sonnet)
|
||||
- **Aider** → route through Groq (free, blazing fast)
|
||||
- **Goose** → route through Gemini API (free 1M context)
|
||||
- **OpenCode** → route through DeepSeek (cheapest coding model)
|
||||
|
||||
Reopen your terminal. Done. Every tool is configured.
|
||||
|
||||
### 4. Supercharge your IDE with external quotas
|
||||
|
||||
Cursor quota burned through? Windsurf credits gone? Don't stop coding — seamlessly plug in any other quota source:
|
||||
|
||||
| IDE | How it works | What you get |
|
||||
|-----|-------------|--------------|
|
||||
| **Cursor** | RPC proxy (ConnectRPC, HTTP/2) | Use Claude/Kiro/Groq/any provider inside Cursor |
|
||||
| **Windsurf** | RPC proxy (ConnectRPC) | Replace Windsurf's built-in models |
|
||||
| **VS Code Copilot** | Ollama BYOK bridge | Use any model as a Copilot backend |
|
||||
| **Antigravity** | Gemini REST proxy | Route through any provider |
|
||||
|
||||
Start the proxy from the dashboard. Your IDE doesn't know the difference.
|
||||
|
||||
### 5. Combine quotas into unstoppable model groups
|
||||
|
||||
This is the killer feature. Take quotas from multiple providers and merge them into a single virtual model:
|
||||
|
||||
```
|
||||
"fast-group" = Groq (Llama 90B) + Cerebras (Llama 70B) + SambaNova (Llama 405B)
|
||||
```
|
||||
|
||||
When Groq's free quota runs out → automatic failover to Cerebras → then SambaNova. **Your AI never stops.** Round-robin across providers ensures maximum uptime with zero manual switching.
|
||||
|
||||
---
|
||||
|
||||
## Install
|
||||
|
||||
### Binary (recommended, no Node.js needed)
|
||||
|
||||
Download from [GitHub Releases](https://github.com/romgX/openrelay/releases):
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
curl -L -o openrelay https://github.com/romgX/openrelay/releases/latest/download/openrelay-macos-arm64
|
||||
chmod +x openrelay
|
||||
xattr -d com.apple.quarantine openrelay # first time only
|
||||
./openrelay
|
||||
```
|
||||
|
||||
```powershell
|
||||
# Windows
|
||||
.\openrelay-windows-x64.exe
|
||||
```
|
||||
|
||||
### npm (requires Node.js >= 18)
|
||||
|
||||
```bash
|
||||
npm install -g openrelay
|
||||
openrelay
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
openrelay # Start the proxy
|
||||
openrelay --setup # First time on macOS: authorize Keychain
|
||||
openrelay --test # Test all provider connections
|
||||
```
|
||||
|
||||
Open `http://localhost:18765` → everything is managed from the Web dashboard.
|
||||
|
||||
## All 29 Providers
|
||||
|
||||
### IDE Providers (auto-detect, zero config)
|
||||
|
||||
| Provider | What you get | Auth |
|
||||
|----------|-------------|------|
|
||||
| Claude Desktop | Your Claude Pro/Free quota | Keychain cookie |
|
||||
| Claude Code | Your Claude Code subscription | Keychain OAuth |
|
||||
| Kiro (AWS Q) | Free Claude Sonnet + Haiku + more | `~/.aws/sso/cache/` |
|
||||
| Windsurf (Codeium) | SWE-1, Kimi, and more | `state.vscdb` |
|
||||
| Antigravity (Google) | Free Gemini 2.5 | Local Language Server |
|
||||
| OpenCode | 35+ models including free GLM-4 | CLI subprocess |
|
||||
| VS Code (Copilot) | Your Copilot subscription | GitHub OAuth |
|
||||
|
||||
### Direct API Providers (bring your own key)
|
||||
|
||||
Groq, Cerebras, OpenRouter, SambaNova, DashScope (Qwen), DeepSeek, Gemini, Mistral, xAI (Grok), SiliconFlow, Zhipu (GLM), Together, Fireworks, NVIDIA NIM, GitHub Models, Volcengine, Moonshot, Baichuan, Stepfun, MiniMax, Hunyuan, Ollama
|
||||
|
||||
### IDE RPC Proxy Servers
|
||||
|
||||
| IDE | Port | Protocol | Use case |
|
||||
|-----|------|----------|----------|
|
||||
| Windsurf | 18766 | ConnectRPC | Replace Windsurf's built-in models |
|
||||
| Antigravity | 18767 | Gemini REST | Route Antigravity through any provider |
|
||||
| Cursor | 18780 | ConnectRPC (HTTP/2+TLS) | Use any model inside Cursor |
|
||||
| VS Code Copilot | 18769 | Ollama REST | Any model as Copilot backend |
|
||||
|
||||
## API Endpoints
|
||||
|
||||
```
|
||||
POST /v1/messages → Anthropic Messages API
|
||||
POST /{provider}/v1/messages → Specific provider (Anthropic)
|
||||
POST /v1/chat/completions → OpenAI Chat Completions API
|
||||
POST /{provider}/v1/chat/completions → Specific provider (OpenAI)
|
||||
GET /v1/models → All available models
|
||||
GET /health → Provider status
|
||||
GET / → Web dashboard
|
||||
```
|
||||
|
||||
## Free vs Pro
|
||||
|
||||
| | Free | Pro |
|
||||
|---|---|---|
|
||||
| Providers | All 29 | All 29 |
|
||||
| Daily requests | 30 | Unlimited |
|
||||
| IDE RPC proxy | Yes | Yes |
|
||||
| Custom model groups | View only | Full access |
|
||||
| Price | $0 | [See pricing](https://github.com/romgX/openrelay/releases) |
|
||||
|
||||
## Security
|
||||
|
||||
**Credentials never leave your machine.** All tokens, cookies, and API keys stay in local process memory. Nothing is uploaded anywhere.
|
||||
|
||||
**Direct connections only.** Requests go straight from your machine to the AI provider. No relay server in between.
|
||||
|
||||
**No request logging.** Message content is never logged, cached, or persisted.
|
||||
|
||||
**Auditable.** The credential handling code ([cookie.ts](src/cookie.ts)) is open source for security review.
|
||||
|
||||
See [DISCLAIMER.md](DISCLAIMER.md) and [PRIVACY.md](PRIVACY.md) for details.
|
||||
|
||||
## License
|
||||
|
||||
Open Core model:
|
||||
- **Framework** (proxy, format translation, config): [MIT](LICENSE)
|
||||
- **Pro features** (custom model groups, unlimited requests): [Commercial](COMMERCIAL-LICENSE.txt)
|
||||
|
||||
---
|
||||
|
||||
<a name="中文"></a>
|
||||
## 中文说明
|
||||
|
||||
**你的 AI 订阅,处处可用。** 别再为用不完的 AI 工具白白付费。
|
||||
|
||||
---
|
||||
|
||||
## 痛点
|
||||
怎么样找到更多的免费AI模型配额?
|
||||
怎么样把免费或收费的配额接到你正在使用的工具中?
|
||||
怎么样能一键配置你的claude code 、openclaw AI模型?
|
||||
|
||||
**OpenRelay 一键解决。**
|
||||
|
||||
## OpenRelay 能做什么
|
||||
|
||||
### 1. 自动发现你所有的 AI 配额
|
||||
|
||||
启动 OpenRelay,它立刻找到你机器上每一份 AI 订阅和免费配额 — Claude Desktop、Claude Code、Kiro、Windsurf、Antigravity、OpenCode、VS Code Copilot。所有配额任你调度,无需手动配置。
|
||||
|
||||
支持 22 个直连 API(Groq、Gemini、DeepSeek、Mistral、OpenRouter 等)— 输入一次 API Key,到处可用。
|
||||
|
||||
**29 个提供商。一个面板。一个端点。**
|
||||
|
||||
### 2. 任意配额用在任意工具
|
||||
|
||||
你的 Claude Pro 订阅现在可以驱动 Claude Code、Aider、Continue、Goose、Amp,或任何支持 Anthropic/OpenAI API 的工具:
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_BASE_URL=http://localhost:18765
|
||||
export ANTHROPIC_API_KEY=unused
|
||||
# 搞定。Claude Code 现在使用你的 Claude Desktop 配额。
|
||||
```
|
||||
|
||||
想在 Aider 里用 Kiro 的免费 Claude Sonnet?改一下 URL:
|
||||
```bash
|
||||
export ANTHROPIC_BASE_URL=http://localhost:18765/kiro
|
||||
```
|
||||
|
||||
### 3. 一键配置所有 CLI 工具
|
||||
|
||||
不再手动编辑 `.zshrc`,不再来回倒腾环境变量。打开 Web 面板,选择 Provider,点一下开关:
|
||||
|
||||
- **Claude Code** → 走 Kiro(免费 Claude Sonnet)
|
||||
- **Aider** → 走 Groq(免费,极速推理)
|
||||
- **Goose** → 走 Gemini API(免费,100 万上下文)
|
||||
- **OpenCode** → 走 DeepSeek(最便宜的编程模型)
|
||||
|
||||
重开终端,完事。每个工具都配好了。
|
||||
|
||||
### 4. 给你的 IDE 无缝接入外部配额
|
||||
|
||||
Cursor 配额烧完了?Windsurf 额度用光了?别停下编码 — 无缝插入任何其他配额来源:
|
||||
|
||||
| IDE | 接入方式 | 效果 |
|
||||
|-----|---------|------|
|
||||
| **Cursor** | RPC 代理 (ConnectRPC, HTTP/2) | 在 Cursor 里用 Claude/Kiro/Groq/任意 Provider |
|
||||
| **Windsurf** | RPC 代理 (ConnectRPC) | 用任意 Provider 替换 Windsurf 内置模型 |
|
||||
| **VS Code Copilot** | Ollama BYOK 桥接 | 用任意模型作为 Copilot 后端 |
|
||||
| **Antigravity** | Gemini REST 代理 | 通过任意 Provider 路由 |
|
||||
|
||||
在面板启动代理,IDE 无感切换。
|
||||
|
||||
### 5. 组合配额,AI 永不停机
|
||||
|
||||
把多个 Provider 的配额合并成一个虚拟模型:
|
||||
|
||||
```
|
||||
"fast-group" = Groq (Llama 90B) + Cerebras (Llama 70B) + SambaNova (Llama 405B)
|
||||
```
|
||||
|
||||
Groq 免费额度用完 → 自动切到 Cerebras → 再切 SambaNova。**你的 AI 永不停机。** 跨 Provider 轮询确保最大可用时间,零手动切换。
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
# 下载二进制(无需 Node.js)
|
||||
curl -L -o openrelay https://github.com/romgX/openrelay/releases/latest/download/openrelay-macos-arm64
|
||||
chmod +x openrelay
|
||||
xattr -d com.apple.quarantine openrelay # 仅首次需要
|
||||
./openrelay
|
||||
|
||||
# 或通过 npm
|
||||
npm install -g openrelay && openrelay
|
||||
```
|
||||
|
||||
浏览器打开 `http://localhost:18765` — 一切在 Web 面板中管理,支持中英双语。
|
||||
|
||||
### 安全
|
||||
|
||||
**凭据不离开本机** — 所有 token、cookie、API key 仅在本地内存中使用,不会上传到任何服务器。
|
||||
|
||||
**直连 API** — 请求从你的机器直接发送到 AI 后端,没有中转服务器。
|
||||
|
||||
**可审计** — 凭据处理代码([cookie.ts](src/cookie.ts))开源可审。
|
||||
22
package.json
Normal file
22
package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "openrelay",
|
||||
"version": "0.8.3",
|
||||
"description": "Multi-provider AI local proxy — extracts auth from locally installed AI desktop apps and exposes a unified HTTP proxy",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"openrelay": "./dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"sql.js": "^1.14.0"
|
||||
},
|
||||
"files": [
|
||||
"dist/",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"COMMERCIAL-LICENSE.txt"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
344
src/cookie.ts
Normal file
344
src/cookie.ts
Normal file
@@ -0,0 +1,344 @@
|
||||
/**
|
||||
* Claude Desktop Cookie decryption module
|
||||
*
|
||||
* macOS Chromium standard encryption scheme:
|
||||
* 1. Read Safe Storage password from Keychain
|
||||
* 2. PBKDF2(password, "saltysalt", 1003, 16, SHA1) → AES-128-CBC key
|
||||
* 3. encrypted_value format: v10 (3 bytes) + ciphertext, IV = 16 bytes of 0x20
|
||||
*
|
||||
* Windows Chromium encryption scheme:
|
||||
* 1. Read master key from Local State (DPAPI-protected)
|
||||
* 2. AES-256-GCM decrypt with v10 + nonce(12B) + ciphertext + tag(16B)
|
||||
*/
|
||||
|
||||
import * as crypto from 'node:crypto'
|
||||
import * as fs from 'node:fs'
|
||||
import * as path from 'node:path'
|
||||
import * as os from 'node:os'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { openReadonlyDb, queryAll } from './sqlite.js'
|
||||
|
||||
export interface DesktopCookies {
|
||||
sessionKey: string
|
||||
organizationId: string
|
||||
deviceId: string
|
||||
cfClearance: string
|
||||
}
|
||||
|
||||
// ---- macOS-specific functions ----
|
||||
|
||||
/**
|
||||
* Get Claude Safe Storage password from macOS Keychain
|
||||
*/
|
||||
function getKeychainPassword(): string {
|
||||
try {
|
||||
const result = execFileSync('security', [
|
||||
'find-generic-password', '-w',
|
||||
'-s', 'Claude Safe Storage',
|
||||
'-a', 'Claude Key',
|
||||
], { timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] })
|
||||
return result.toString().trim()
|
||||
} catch (err: any) {
|
||||
throw new Error(
|
||||
`Failed to read Keychain: ${err?.message || 'unknown error'}\n` +
|
||||
'Please verify: 1) macOS system 2) Claude Desktop is installed and has been logged in'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive AES-128-CBC key
|
||||
*/
|
||||
function deriveKey(password: string): Buffer {
|
||||
return crypto.pbkdf2Sync(password, 'saltysalt', 1003, 16, 'sha1')
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt a single Chromium Cookie value (macOS: AES-128-CBC)
|
||||
*/
|
||||
function decryptValue(encryptedValue: Buffer, key: Buffer): string {
|
||||
if (encryptedValue.length < 4) return ''
|
||||
const prefix = encryptedValue.subarray(0, 3).toString('utf-8')
|
||||
if (prefix !== 'v10') return ''
|
||||
|
||||
const data = encryptedValue.subarray(3)
|
||||
const iv = Buffer.alloc(16, 0x20)
|
||||
try {
|
||||
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv)
|
||||
const decrypted = Buffer.concat([decipher.update(data), decipher.final()])
|
||||
// Chromium has a 32-byte internal prefix before cookie value, skip it
|
||||
if (decrypted.length <= 32) return ''
|
||||
return decrypted.subarray(32).toString('utf-8')
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find Claude Desktop Cookies DB path (macOS)
|
||||
*/
|
||||
function findCookiesDbPathDarwin(): string | null {
|
||||
const candidates = [
|
||||
path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'Cookies'),
|
||||
path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'Default', 'Cookies'),
|
||||
]
|
||||
for (const p of candidates) {
|
||||
if (fs.existsSync(p)) return p
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Read device ID from ant-did file (macOS)
|
||||
*/
|
||||
function findDeviceIdDarwin(): string {
|
||||
const candidates = [
|
||||
path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'ant-did'),
|
||||
path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'Default', 'ant-did'),
|
||||
]
|
||||
for (const p of candidates) {
|
||||
try {
|
||||
if (fs.existsSync(p)) return fs.readFileSync(p, 'utf-8').trim()
|
||||
} catch { /* continue */ }
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract cookies on macOS (Keychain + AES-128-CBC)
|
||||
*/
|
||||
async function extractCookiesDarwin(): Promise<DesktopCookies> {
|
||||
const dbPath = findCookiesDbPathDarwin()
|
||||
if (!dbPath) {
|
||||
throw new Error(
|
||||
'Claude Desktop Cookies database not found\n' +
|
||||
'Please verify Claude Desktop is installed and has been logged in at least once'
|
||||
)
|
||||
}
|
||||
|
||||
const password = getKeychainPassword()
|
||||
const key = deriveKey(password)
|
||||
|
||||
let db: any
|
||||
try {
|
||||
db = await openReadonlyDb(dbPath)
|
||||
} catch (err: any) {
|
||||
throw new Error(`Failed to open Cookies DB: ${err?.message}`)
|
||||
}
|
||||
|
||||
const result: DesktopCookies = {
|
||||
sessionKey: '',
|
||||
organizationId: '',
|
||||
deviceId: '',
|
||||
cfClearance: '',
|
||||
}
|
||||
|
||||
try {
|
||||
const rows = queryAll(db,
|
||||
`SELECT name, encrypted_value, host_key FROM cookies WHERE host_key LIKE '%claude.ai%'`,
|
||||
)
|
||||
|
||||
for (const row of rows) {
|
||||
const encBuf = row.encrypted_value instanceof Uint8Array
|
||||
? Buffer.from(row.encrypted_value)
|
||||
: Buffer.alloc(0)
|
||||
const value = decryptValue(encBuf, key)
|
||||
if (!value) continue
|
||||
switch (row.name) {
|
||||
case 'sessionKey': result.sessionKey = value; break
|
||||
case 'lastActiveOrg': result.organizationId = value; break
|
||||
case 'anthropic-device-id': result.deviceId = value; break
|
||||
case 'cf_clearance': result.cfClearance = value; break
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
|
||||
if (!result.sessionKey) {
|
||||
throw new Error('sessionKey not found. Please verify Claude Desktop is logged in (able to chat normally).')
|
||||
}
|
||||
if (!result.organizationId) {
|
||||
throw new Error('lastActiveOrg not found. Please verify Claude Desktop is logged in.')
|
||||
}
|
||||
|
||||
if (!result.deviceId) {
|
||||
result.deviceId = findDeviceIdDarwin()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ---- Windows-specific functions ----
|
||||
|
||||
/**
|
||||
* Find Claude Desktop Cookies DB path (Windows)
|
||||
* %APPDATA%\Claude\Cookies or %APPDATA%\Claude\Default\Cookies
|
||||
*/
|
||||
/**
|
||||
* Find Claude Desktop data root directory on Windows.
|
||||
* Checks both traditional Electron installer path (%APPDATA%\Claude)
|
||||
* and Windows Store sandboxed path (%LOCALAPPDATA%\Packages\Claude_*\LocalCache\Roaming\Claude).
|
||||
* Returns the most recently modified one if both exist.
|
||||
*/
|
||||
function findClaudeDataRootWindows(): string | null {
|
||||
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming')
|
||||
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local')
|
||||
|
||||
const candidates: string[] = [
|
||||
path.join(appData, 'Claude'),
|
||||
]
|
||||
|
||||
// Windows Store app: %LOCALAPPDATA%\Packages\Claude_{publisherHash}\LocalCache\Roaming\Claude
|
||||
const packagesDir = path.join(localAppData, 'Packages')
|
||||
try {
|
||||
for (const entry of fs.readdirSync(packagesDir)) {
|
||||
if (entry.startsWith('Claude_')) {
|
||||
candidates.push(path.join(packagesDir, entry, 'LocalCache', 'Roaming', 'Claude'))
|
||||
}
|
||||
}
|
||||
} catch { /* Packages dir not accessible */ }
|
||||
|
||||
// Pick the one with the most recently modified Cookies file
|
||||
let best: string | null = null
|
||||
let bestMtime = 0
|
||||
for (const dir of candidates) {
|
||||
for (const cookiePath of ['Network/Cookies', 'Cookies', 'Default/Cookies']) {
|
||||
const full = path.join(dir, cookiePath)
|
||||
try {
|
||||
const mtime = fs.statSync(full).mtimeMs
|
||||
if (mtime > bestMtime) { bestMtime = mtime; best = dir }
|
||||
} catch { /* not found */ }
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
function findCookiesDbPathWindows(): string | null {
|
||||
const root = findClaudeDataRootWindows()
|
||||
if (!root) return null
|
||||
for (const sub of ['Network/Cookies', 'Cookies', 'Default/Cookies']) {
|
||||
const p = path.join(root, sub)
|
||||
if (fs.existsSync(p)) return p
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Find Local State file for Claude Desktop (Windows)
|
||||
*/
|
||||
function findLocalStatePath(): string | null {
|
||||
const root = findClaudeDataRootWindows()
|
||||
if (root) {
|
||||
const p = path.join(root, 'Local State')
|
||||
if (fs.existsSync(p)) return p
|
||||
}
|
||||
// Fallback: check %LOCALAPPDATA%\Claude directly
|
||||
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local')
|
||||
const fallback = path.join(localAppData, 'Claude', 'Local State')
|
||||
if (fs.existsSync(fallback)) return fallback
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Read device ID from ant-did file (Windows)
|
||||
*/
|
||||
function findDeviceIdWindows(): string {
|
||||
const root = findClaudeDataRootWindows()
|
||||
const candidates = root
|
||||
? [path.join(root, 'ant-did'), path.join(root, 'Default', 'ant-did')]
|
||||
: []
|
||||
for (const p of candidates) {
|
||||
try {
|
||||
if (fs.existsSync(p)) return fs.readFileSync(p, 'utf-8').trim()
|
||||
} catch { /* continue */ }
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract cookies on Windows (DPAPI + AES-256-GCM)
|
||||
*/
|
||||
async function extractCookiesWindows(): Promise<DesktopCookies> {
|
||||
const { getChromiumKeyFromLocalState, decryptChromiumValueWindows } = await import('./dpapi.js')
|
||||
|
||||
const dbPath = findCookiesDbPathWindows()
|
||||
if (!dbPath) {
|
||||
throw new Error(
|
||||
'Claude Desktop Cookies database not found\n' +
|
||||
'Please verify Claude Desktop is installed and has been logged in at least once'
|
||||
)
|
||||
}
|
||||
|
||||
const localStatePath = findLocalStatePath()
|
||||
if (!localStatePath) {
|
||||
throw new Error('Claude Desktop Local State file not found')
|
||||
}
|
||||
|
||||
const key = getChromiumKeyFromLocalState(localStatePath)
|
||||
|
||||
let db: any
|
||||
try {
|
||||
db = await openReadonlyDb(dbPath)
|
||||
} catch (err: any) {
|
||||
throw new Error(`Failed to open Cookies DB: ${err?.message}`)
|
||||
}
|
||||
|
||||
const result: DesktopCookies = {
|
||||
sessionKey: '',
|
||||
organizationId: '',
|
||||
deviceId: '',
|
||||
cfClearance: '',
|
||||
}
|
||||
|
||||
try {
|
||||
const rows = queryAll(db,
|
||||
`SELECT name, encrypted_value, host_key FROM cookies WHERE host_key LIKE '%claude.ai%'`,
|
||||
)
|
||||
|
||||
for (const row of rows) {
|
||||
const encBuf = row.encrypted_value instanceof Uint8Array
|
||||
? Buffer.from(row.encrypted_value)
|
||||
: Buffer.alloc(0)
|
||||
const value = decryptChromiumValueWindows(encBuf, key)
|
||||
if (!value) continue
|
||||
switch (row.name) {
|
||||
case 'sessionKey': result.sessionKey = value; break
|
||||
case 'lastActiveOrg': result.organizationId = value; break
|
||||
case 'anthropic-device-id': result.deviceId = value; break
|
||||
case 'cf_clearance': result.cfClearance = value; break
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
|
||||
if (!result.sessionKey) {
|
||||
throw new Error('sessionKey not found. Please verify Claude Desktop is logged in (able to chat normally).')
|
||||
}
|
||||
if (!result.organizationId) {
|
||||
throw new Error('lastActiveOrg not found. Please verify Claude Desktop is logged in.')
|
||||
}
|
||||
|
||||
if (!result.deviceId) {
|
||||
result.deviceId = findDeviceIdWindows()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ---- Public API ----
|
||||
|
||||
/**
|
||||
* Extract and decrypt all required cookies from Claude Desktop Cookies DB.
|
||||
* Dispatches to platform-specific implementation.
|
||||
*/
|
||||
export async function extractCookies(): Promise<DesktopCookies> {
|
||||
if (process.platform === 'win32') {
|
||||
return extractCookiesWindows()
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
return extractCookiesDarwin()
|
||||
}
|
||||
throw new Error(`Unsupported platform: ${process.platform}`)
|
||||
}
|
||||
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": false,
|
||||
"sourceMap": false
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user