saros-proxy 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +620 -0
- package/dist/cli/opencode-config.d.ts +28 -0
- package/dist/cli/opencode-config.js +126 -0
- package/dist/cli/opencode-config.js.map +1 -0
- package/dist/cli/setup.d.ts +29 -0
- package/dist/cli/setup.js +669 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/cli/ui.d.ts +27 -0
- package/dist/cli/ui.js +116 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/config.d.ts +50 -0
- package/dist/config.js +341 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts +25 -0
- package/dist/constants.js +162 -0
- package/dist/constants.js.map +1 -0
- package/dist/dashboard-parser.d.ts +47 -0
- package/dist/dashboard-parser.js +88 -0
- package/dist/dashboard-parser.js.map +1 -0
- package/dist/firefox-cookies.d.ts +42 -0
- package/dist/firefox-cookies.js +170 -0
- package/dist/firefox-cookies.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +101 -0
- package/dist/index.js.map +1 -0
- package/dist/key-encryption.d.ts +21 -0
- package/dist/key-encryption.js +59 -0
- package/dist/key-encryption.js.map +1 -0
- package/dist/logger.d.ts +11 -0
- package/dist/logger.js +35 -0
- package/dist/logger.js.map +1 -0
- package/dist/proxy-logic.d.ts +108 -0
- package/dist/proxy-logic.js +353 -0
- package/dist/proxy-logic.js.map +1 -0
- package/dist/proxy.d.ts +35 -0
- package/dist/proxy.js +551 -0
- package/dist/proxy.js.map +1 -0
- package/dist/scraper.d.ts +70 -0
- package/dist/scraper.js +239 -0
- package/dist/scraper.js.map +1 -0
- package/dist/validation.d.ts +34 -0
- package/dist/validation.js +64 -0
- package/dist/validation.js.map +1 -0
- package/package.json +73 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Saros Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
# Saros
|
|
2
|
+
|
|
3
|
+
[](https://github.com/vitorvilaca3011/saros-proxy/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/saros-proxy)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://nodejs.org/)
|
|
7
|
+
|
|
8
|
+
Saros — like the eclipse cycle, this proxy predicts exhaustion and rotates accounts before they run out. Built for the OpenCode-Go API.
|
|
9
|
+
|
|
10
|
+
Manage multiple OpenCode-Go API keys behind a single endpoint. When one key is rate-limited, revoked, or returns server errors, the proxy automatically fails over to the next healthy key — with zero downtime.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Contents
|
|
15
|
+
|
|
16
|
+
- [Features](#features)
|
|
17
|
+
- [Architecture](#architecture)
|
|
18
|
+
- [Installation](#installation)
|
|
19
|
+
- [Docker](#docker)
|
|
20
|
+
- [Quick Start](#quick-start)
|
|
21
|
+
- [Configuration Reference](#configuration-reference)
|
|
22
|
+
- [Usage Examples](#usage-examples)
|
|
23
|
+
- [OpenCode Integration](#opencode-integration)
|
|
24
|
+
- [Troubleshooting](#troubleshooting)
|
|
25
|
+
- [Development](#development)
|
|
26
|
+
- [LLM Quick Reference](#llm-quick-reference)
|
|
27
|
+
- [Security](#security)
|
|
28
|
+
- [License](#license)
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- **Multi-key rotation** — Round-robin distribution across API keys.
|
|
35
|
+
- **Automatic failover** — Retry failed requests with the next available key.
|
|
36
|
+
- **Circuit breaker** — Disable failing keys after a configurable threshold, then lazily re-enable them after a cooldown period.
|
|
37
|
+
- **Streaming support** — SSE pass-through with mid-stream error detection.
|
|
38
|
+
- **Request timeout** — Configurable timeout with graceful 504 response.
|
|
39
|
+
- **Graceful shutdown** — Drains active requests before exiting.
|
|
40
|
+
- **Security hardening** — API key masking in logs, SSRF prevention (HTTPS-only upstream), configurable CORS, host binding validation.
|
|
41
|
+
- **Config validation** — CLI args, YAML file, and environment variables with sensible defaults.
|
|
42
|
+
- **Health endpoint** — `GET /health` returns key status, active requests, and circuit-breaker state.
|
|
43
|
+
- **No database** — Stateless proxy, zero external dependencies beyond Node.js.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Architecture
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
┌──────────────────────────────────────────────────────┐
|
|
51
|
+
│ Client / OpenCode │
|
|
52
|
+
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
|
53
|
+
│ │ Request 1│ │ Request 2│ │ Request 3│ ... │
|
|
54
|
+
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
|
|
55
|
+
└───────┼──────────────┼──────────────┼────────────────┘
|
|
56
|
+
│ │ │
|
|
57
|
+
▼ ▼ ▼
|
|
58
|
+
┌──────────────────────────────────────────────────────┐
|
|
59
|
+
│ Saros (:3000) │
|
|
60
|
+
│ │
|
|
61
|
+
│ ┌─────────────┐ ┌────────────┐ ┌─────────────┐ │
|
|
62
|
+
│ │ Key Selector│──▶│ Circuit │──▶│ Request │ │
|
|
63
|
+
│ │ Round-robin │ │ Breaker │ │ Forwarder │ │
|
|
64
|
+
│ │ + Failover │ │ Threshold │ │ + Timeout │ │
|
|
65
|
+
│ └─────────────┘ │ + Cooldown │ └──────┬──────┘ │
|
|
66
|
+
│ └────────────┘ │ │
|
|
67
|
+
│ ┌─────────────────────────────────────────┘ │
|
|
68
|
+
│ │ ┌─────────────────────┐ │
|
|
69
|
+
│ └──▶ Streaming Detector │ SSE passthrough │
|
|
70
|
+
│ └─────────────────────┘ │
|
|
71
|
+
└──────────────────────┬───────────────────────────────┘
|
|
72
|
+
│
|
|
73
|
+
┌────────────┼────────────┐
|
|
74
|
+
▼ ▼ ▼
|
|
75
|
+
┌──────────┐ ┌──────────┐ ┌──────────┐
|
|
76
|
+
│ Key A │ │ Key B │ │ Key C │
|
|
77
|
+
│ sk-abc… │ │ sk-def… │ │ sk-ghi… │
|
|
78
|
+
└────┬─────┘ └────┬─────┘ └────┬─────┘
|
|
79
|
+
│ │ │
|
|
80
|
+
▼ ▼ ▼
|
|
81
|
+
┌───────────────────────────────────────┐
|
|
82
|
+
│ OpenCode-Go API (upstream) │
|
|
83
|
+
│ https://opencode.ai │
|
|
84
|
+
└───────────────────────────────────────┘
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### How Request Distribution Works
|
|
88
|
+
|
|
89
|
+
The proxy uses **two layers** to decide which API key handles each request:
|
|
90
|
+
|
|
91
|
+
#### Layer 1: Round-Robin (always active)
|
|
92
|
+
Every new request cycles to the next key in order:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
Request 1 → Key A
|
|
96
|
+
Request 2 → Key B
|
|
97
|
+
Request 3 → Key C
|
|
98
|
+
Request 4 → Key A (wraps around)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
This gives you **even distribution by default** — no manual odd/even logic needed. If you have 2 accounts, odd requests go to account 1 and even to account 2 automatically.
|
|
102
|
+
|
|
103
|
+
#### Layer 2: Usage-Based Gating (optional)
|
|
104
|
+
If you enable [dashboard scraping](#usage-based-key-selection), the proxy checks each account's quota usage before selecting a key:
|
|
105
|
+
|
|
106
|
+
- If an account's usage is **≥ threshold** (default 70%), that key is skipped
|
|
107
|
+
- If **all** keys are over threshold, the proxy falls back to the **lowest-usage** key
|
|
108
|
+
- If no usage data is available, round-robin continues unchanged
|
|
109
|
+
|
|
110
|
+
| Scenario | Behavior |
|
|
111
|
+
|---|---|
|
|
112
|
+
| Scraping disabled | Pure round-robin across all keys |
|
|
113
|
+
| Scraping enabled + data available | Round-robin, skipping over-quota accounts |
|
|
114
|
+
| Scraping enabled + no data | Falls back to pure round-robin |
|
|
115
|
+
|
|
116
|
+
#### Concurrent Request Safety
|
|
117
|
+
The proxy tracks which keys are **currently in use** by active requests. A key already handling a streaming request won't be assigned to another request until it completes. This prevents double-booking the same account under load.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Installation
|
|
122
|
+
|
|
123
|
+
### From npm (when published)
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npm install -g saros-proxy
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### From source
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
git clone https://github.com/vitorvilaca3011/saros-proxy.git
|
|
133
|
+
cd proxy
|
|
134
|
+
npm install
|
|
135
|
+
npm run build
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
> **Requirements**: Node.js >= 22.0.0
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Docker
|
|
143
|
+
|
|
144
|
+
Run the proxy in a clean Linux container without installing Node.js locally.
|
|
145
|
+
|
|
146
|
+
### Prerequisites
|
|
147
|
+
|
|
148
|
+
- [Docker](https://docs.docker.com/get-docker/) (with Compose v2)
|
|
149
|
+
|
|
150
|
+
### Quick start
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# 1. Create config.yaml from the example (edit with your API keys)
|
|
154
|
+
cp config.example.yaml config.yaml
|
|
155
|
+
|
|
156
|
+
# 2. Build the image
|
|
157
|
+
npm run docker:build
|
|
158
|
+
|
|
159
|
+
# 3. Start the proxy
|
|
160
|
+
npm run docker:run
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The proxy is now available at `http://127.0.0.1:3000`.
|
|
164
|
+
|
|
165
|
+
### Run tests in Docker
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Run the full test suite inside a Linux container
|
|
169
|
+
npm run docker:test
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Manual Docker commands
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
# Build
|
|
176
|
+
docker compose build proxy
|
|
177
|
+
|
|
178
|
+
# Run in foreground
|
|
179
|
+
docker compose up proxy
|
|
180
|
+
|
|
181
|
+
# Run in background (detached)
|
|
182
|
+
docker compose up -d proxy
|
|
183
|
+
|
|
184
|
+
# View logs
|
|
185
|
+
docker compose logs -f proxy
|
|
186
|
+
|
|
187
|
+
# Stop
|
|
188
|
+
docker compose down
|
|
189
|
+
|
|
190
|
+
# Run tests
|
|
191
|
+
docker compose run --rm test
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Configuration with Docker
|
|
195
|
+
|
|
196
|
+
Mount a custom `config.yaml` or use environment variables:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
# Using environment variables (no config.yaml needed)
|
|
200
|
+
docker compose run --rm -e OPENCODE_GO_KEYS="home:sk-xxx,work:sk-yyy" proxy
|
|
201
|
+
|
|
202
|
+
# Or edit docker-compose.yml to set env vars directly, then:
|
|
203
|
+
docker compose up proxy
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Quick Start
|
|
209
|
+
|
|
210
|
+
### Option A: Interactive Setup (Recommended)
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# Run the setup wizard — it will guide you through everything
|
|
214
|
+
npx saros-proxy setup
|
|
215
|
+
|
|
216
|
+
# Or if installed globally:
|
|
217
|
+
saros-proxy setup
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The wizard will:
|
|
221
|
+
1. Ask for your proxy port (default: 3000)
|
|
222
|
+
2. Ask for your API keys (name + key value)
|
|
223
|
+
3. Generate `config.yaml` automatically
|
|
224
|
+
4. Run a smoke test to verify everything works
|
|
225
|
+
|
|
226
|
+
### Option B: Manual Setup
|
|
227
|
+
|
|
228
|
+
#### 1. Create a configuration file
|
|
229
|
+
|
|
230
|
+
```yaml
|
|
231
|
+
# config.yaml
|
|
232
|
+
port: 3000
|
|
233
|
+
host: 127.0.0.1
|
|
234
|
+
upstreamBaseUrl: https://opencode.ai
|
|
235
|
+
keys:
|
|
236
|
+
- label: primary
|
|
237
|
+
key: sk-your-primary-key-here-12345678
|
|
238
|
+
- label: secondary
|
|
239
|
+
key: sk-your-secondary-key-here-87654321
|
|
240
|
+
circuitBreakerThreshold: 3
|
|
241
|
+
circuitBreakerCooldownMs: 60000
|
|
242
|
+
requestTimeoutMs: 30000
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
#### 2. Run the proxy
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
# Using tsx (development)
|
|
249
|
+
npx tsx src/index.ts
|
|
250
|
+
|
|
251
|
+
# Using built JavaScript
|
|
252
|
+
node dist/index.js
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
#### 3. Test it
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
curl http://127.0.0.1:3000/health
|
|
259
|
+
|
|
260
|
+
# Query models
|
|
261
|
+
curl http://127.0.0.1:3000/zen/go/v1/models
|
|
262
|
+
|
|
263
|
+
# Chat completion
|
|
264
|
+
curl http://127.0.0.1:3000/zen/go/v1/chat/completions \
|
|
265
|
+
-H "Content-Type: application/json" \
|
|
266
|
+
-d '{"model":"gpt-4","messages":[{"role":"user","content":"Hello"}]}'
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Configuration Reference
|
|
272
|
+
|
|
273
|
+
All configuration options can be set via YAML file (`config.yaml`), environment variables, or CLI flags. Priority order: CLI > YAML > env vars > defaults.
|
|
274
|
+
|
|
275
|
+
### YAML / Environment Variables
|
|
276
|
+
|
|
277
|
+
| Option | CLI Flag | Env Variable | Default | Description |
|
|
278
|
+
|---|---|---|---|---|
|
|
279
|
+
| `port` | `--port` | `PROXY_PORT` | `3000` | Listening port (1–65535) |
|
|
280
|
+
| `host` | `--host` | `PROXY_HOST` | `127.0.0.1` | Bind address |
|
|
281
|
+
| `upstreamBaseUrl` | — | `UPSTREAM_BASE_URL` | `https://opencode.ai` | Upstream API base URL (HTTPS only) |
|
|
282
|
+
| `circuitBreakerThreshold` | — | `CIRCUIT_BREAKER_THRESHOLD` | `3` | Consecutive failures before disabling a key (1–10) |
|
|
283
|
+
| `circuitBreakerCooldownMs` | — | `CIRCUIT_BREAKER_COOLDOWN_MS` | `60000` | Cooldown in ms before re-enabling a key (1000–3600000) |
|
|
284
|
+
| `requestTimeoutMs` | — | `REQUEST_TIMEOUT_MS` | `30000` | Upstream request timeout in ms (1000–300000) |
|
|
285
|
+
| `allowedOrigins` | — | — | `["http://localhost:*", "http://127.0.0.1:*"]` | CORS allowed origins (use `[]` to allow all) |
|
|
286
|
+
| `keys` | — | `OPENCODE_GO_KEYS` | — | Array of `{label, key}` objects or comma-separated env format |
|
|
287
|
+
| `config` | `--config` | — | `config.yaml` | Path to YAML config file |
|
|
288
|
+
|
|
289
|
+
### API Key Format
|
|
290
|
+
|
|
291
|
+
Keys must start with `sk-` and be at least 20 characters.
|
|
292
|
+
|
|
293
|
+
**YAML:**
|
|
294
|
+
```yaml
|
|
295
|
+
keys:
|
|
296
|
+
- label: my-account
|
|
297
|
+
key: sk-your-key-here...
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Environment variable:**
|
|
301
|
+
```bash
|
|
302
|
+
OPENCODE_GO_KEYS="account1:sk-xxx,account2:sk-yyy"
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### API Key Encryption
|
|
306
|
+
|
|
307
|
+
API keys can be encrypted at rest using AES-256-GCM with scrypt key derivation. When encrypted, keys are stored as `enc:scrypt:...` in `config.yaml` and decrypted at startup using the `OPENCODE_GO_ENCRYPTION_KEY` environment variable.
|
|
308
|
+
|
|
309
|
+
**Setup with encryption:**
|
|
310
|
+
```bash
|
|
311
|
+
npm run setup
|
|
312
|
+
# When prompted, choose to encrypt keys and enter a master key (min 16 characters)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**Running with encrypted keys:**
|
|
316
|
+
```bash
|
|
317
|
+
export OPENCODE_GO_ENCRYPTION_KEY="your-master-key"
|
|
318
|
+
npx tsx src/index.ts
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**Docker with encrypted keys:**
|
|
322
|
+
```yaml
|
|
323
|
+
# docker-compose.yml
|
|
324
|
+
environment:
|
|
325
|
+
- OPENCODE_GO_ENCRYPTION_KEY=your-master-key
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**Notes:**
|
|
329
|
+
- Encrypted keys use AES-256-GCM with scrypt KDF (N=16384, r=8, p=1)
|
|
330
|
+
- Each key is encrypted with a random salt and IV
|
|
331
|
+
- Plaintext keys (starting with `sk-`) are still supported for backward compatibility
|
|
332
|
+
- The `OPENCODE_GO_KEYS` environment variable always uses plaintext keys (for CI/CD)
|
|
333
|
+
- If keys are encrypted but `OPENCODE_GO_ENCRYPTION_KEY` is not set, the proxy will fail to start with a clear error message
|
|
334
|
+
|
|
335
|
+
### CLI Flags
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
tsx src/index.ts --config my-config.yaml --port 4000 --host 0.0.0.0
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Usage Examples
|
|
344
|
+
|
|
345
|
+
### Basic: Run with YAML config
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
# Create config.yaml (see Quick Start), then:
|
|
349
|
+
npx tsx src/index.ts
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Advanced: Run with environment variables only
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
export PROXY_PORT=4000
|
|
356
|
+
export PROXY_HOST=127.0.0.1
|
|
357
|
+
export OPENCODE_GO_KEYS="home:sk-abc123...,work:sk-def456..."
|
|
358
|
+
export CIRCUIT_BREAKER_THRESHOLD=5
|
|
359
|
+
export CIRCUIT_BREAKER_COOLDOWN_MS=120000
|
|
360
|
+
export REQUEST_TIMEOUT_MS=45000
|
|
361
|
+
export LOG_LEVEL=debug
|
|
362
|
+
export NODE_ENV=development
|
|
363
|
+
|
|
364
|
+
npx tsx src/index.ts
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Custom config path
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
npx tsx src/index.ts --config /etc/saros/config.yaml
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Run as a background service (Linux)
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
nohup npx tsx src/index.ts > proxy.log 2>&1 &
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## OpenCode Integration
|
|
382
|
+
|
|
383
|
+
Configure OpenCode to route API requests through the proxy. Create or edit your OpenCode config file:
|
|
384
|
+
|
|
385
|
+
**Location:** `~/.config/opencode/opencode.json` (global) or `opencode.json` in your project root
|
|
386
|
+
|
|
387
|
+
```jsonc
|
|
388
|
+
{
|
|
389
|
+
"$schema": "https://opencode.ai/config.json",
|
|
390
|
+
"provider": {
|
|
391
|
+
"saros-proxy": {
|
|
392
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
393
|
+
"name": "Saros",
|
|
394
|
+
"options": {
|
|
395
|
+
"baseURL": "http://127.0.0.1:3000/zen/go/v1",
|
|
396
|
+
"apiKey": "not-used"
|
|
397
|
+
},
|
|
398
|
+
"models": {
|
|
399
|
+
"glm-5": { "name": "GLM-5" },
|
|
400
|
+
"kimi-k2.5": { "name": "Kimi K2.5" },
|
|
401
|
+
"qwen3.7-plus": { "name": "Qwen 3.7 Plus" }
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
**Key points:**
|
|
409
|
+
- `baseURL` must end at `/v1` — OpenCode appends route paths internally
|
|
410
|
+
- `apiKey` can be any placeholder — the proxy handles real key selection
|
|
411
|
+
- Model IDs must match what the upstream API expects (e.g., `glm-5`, not `opencode-go/glm-5`)
|
|
412
|
+
- After editing the config, restart OpenCode
|
|
413
|
+
|
|
414
|
+
**Alternative: Environment variable substitution**
|
|
415
|
+
|
|
416
|
+
```jsonc
|
|
417
|
+
{
|
|
418
|
+
"provider": {
|
|
419
|
+
"saros-proxy": {
|
|
420
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
421
|
+
"name": "Saros",
|
|
422
|
+
"options": {
|
|
423
|
+
"baseURL": "{env:OPENCODE_PROXY_URL}",
|
|
424
|
+
"apiKey": "{env:OPENCODE_PROXY_KEY}"
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Then set: `export OPENCODE_PROXY_URL=http://127.0.0.1:3000/zen/go/v1`
|
|
432
|
+
|
|
433
|
+
**Using the proxy in OpenCode:**
|
|
434
|
+
|
|
435
|
+
Once configured, select the proxy provider in OpenCode's model picker, or set it as default:
|
|
436
|
+
|
|
437
|
+
```jsonc
|
|
438
|
+
{
|
|
439
|
+
"provider": { /* ... as above ... */ },
|
|
440
|
+
"model": "saros-proxy/glm-5"
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Troubleshooting
|
|
447
|
+
|
|
448
|
+
### Check proxy health
|
|
449
|
+
|
|
450
|
+
```bash
|
|
451
|
+
curl http://127.0.0.1:3000/health
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
Returns JSON with key counts, active requests, and circuit-breaker state.
|
|
455
|
+
|
|
456
|
+
### View logs
|
|
457
|
+
|
|
458
|
+
```bash
|
|
459
|
+
# Development (pretty-printed, colorized)
|
|
460
|
+
NODE_ENV=development npx tsx src/index.ts
|
|
461
|
+
|
|
462
|
+
# Production (structured JSON)
|
|
463
|
+
NODE_ENV=production npx tsx src/index.ts
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Verify which key served a request
|
|
467
|
+
|
|
468
|
+
The proxy returns two debug headers on every successful response:
|
|
469
|
+
|
|
470
|
+
| Header | Example | Meaning |
|
|
471
|
+
|---|---|---|
|
|
472
|
+
| `X-Proxy-Key-Label` | `primary` | Which configured key was used |
|
|
473
|
+
| `X-Proxy-Request-Id` | `550e8400-e29b-41d4-a716-446655440000` | Unique request ID for log correlation |
|
|
474
|
+
|
|
475
|
+
```bash
|
|
476
|
+
# Check which key handled your request
|
|
477
|
+
curl -s -D - http://127.0.0.1:3000/zen/go/v1/models | grep -i x-proxy
|
|
478
|
+
# X-Proxy-Key-Label: primary
|
|
479
|
+
# X-Proxy-Request-Id: 550e8400-e29b-41d4-a716-446655440000
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### Common issues
|
|
483
|
+
|
|
484
|
+
| Symptom | Likely cause | Fix |
|
|
485
|
+
|---|---|---|
|
|
486
|
+
| `No valid API keys configured` | Missing or invalid keys | Check `keys` in config.yaml or `OPENCODE_GO_KEYS` env var |
|
|
487
|
+
| `ECONNREFUSED` on startup | Port already in use | Change `port` or kill the existing process |
|
|
488
|
+
| `504 Gateway Timeout` | Upstream request exceeded timeout | Increase `requestTimeoutMs` or check upstream latency |
|
|
489
|
+
| `All API keys are temporarily unavailable` | All keys disabled by circuit breaker | Wait for cooldown or check upstream status |
|
|
490
|
+
| `CORS violation` | Client origin not in `allowedOrigins` | Add the origin to `allowedOrigins` in config |
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
## Development
|
|
495
|
+
|
|
496
|
+
### Run tests
|
|
497
|
+
|
|
498
|
+
```bash
|
|
499
|
+
# Unit tests
|
|
500
|
+
npm test
|
|
501
|
+
|
|
502
|
+
# E2E tests (requires network)
|
|
503
|
+
npx vitest run test/e2e.test.ts
|
|
504
|
+
|
|
505
|
+
# Watch mode
|
|
506
|
+
npm test -- --watch
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### Type check
|
|
510
|
+
|
|
511
|
+
```bash
|
|
512
|
+
npm run lint
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### Build
|
|
516
|
+
|
|
517
|
+
```bash
|
|
518
|
+
npm run build
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
Output goes to `dist/`.
|
|
522
|
+
|
|
523
|
+
### Project structure
|
|
524
|
+
|
|
525
|
+
```
|
|
526
|
+
src/
|
|
527
|
+
index.ts — Entry point, server startup, graceful shutdown
|
|
528
|
+
proxy.ts — Hono HTTP app, routing, streaming, failover
|
|
529
|
+
proxy-logic.ts — Pure logic: key selection, circuit breaker, error classification
|
|
530
|
+
proxy-logic.test.ts — Unit tests for proxy logic
|
|
531
|
+
config.ts — Configuration loading from YAML, env vars, CLI
|
|
532
|
+
logger.ts — Structured logging with Pino + key masking
|
|
533
|
+
cli/
|
|
534
|
+
setup.ts — Interactive setup wizard
|
|
535
|
+
test/
|
|
536
|
+
e2e.test.ts — End-to-end tests with mock upstream
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Contributing
|
|
540
|
+
|
|
541
|
+
1. Fork the repository.
|
|
542
|
+
2. Create a feature branch: `git checkout -b feat/my-feature`
|
|
543
|
+
3. Commit changes: `git commit -am 'Add my feature'`
|
|
544
|
+
4. Push: `git push origin feat/my-feature`
|
|
545
|
+
5. Open a pull request.
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
## LLM Quick Reference
|
|
550
|
+
|
|
551
|
+
If you're an AI assistant helping a user install this proxy, use these exact commands:
|
|
552
|
+
|
|
553
|
+
**Prerequisites check:**
|
|
554
|
+
```bash
|
|
555
|
+
node --version # Must be >= 22.0.0
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
**Install from source:**
|
|
559
|
+
```bash
|
|
560
|
+
git clone https://github.com/vitorvilaca3011/saros-proxy.git
|
|
561
|
+
cd proxy
|
|
562
|
+
npm install
|
|
563
|
+
npm run build
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
**Configure:**
|
|
567
|
+
```bash
|
|
568
|
+
npm run setup # Interactive wizard
|
|
569
|
+
# OR manually: cp config.example.yaml config.yaml && edit config.yaml
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
**Run:**
|
|
573
|
+
```bash
|
|
574
|
+
npm run dev # Development
|
|
575
|
+
node dist/index.js # Production
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
**Verify:**
|
|
579
|
+
```bash
|
|
580
|
+
curl http://127.0.0.1:3000/health
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
**Docker quick start:**
|
|
584
|
+
```bash
|
|
585
|
+
cp config.example.yaml config.yaml # Edit with your keys
|
|
586
|
+
npm run docker:build
|
|
587
|
+
npm run docker:run
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
**Add to OpenCode config:**
|
|
591
|
+
```bash
|
|
592
|
+
# Run setup wizard and choose "Update OpenCode config automatically"
|
|
593
|
+
npm run setup
|
|
594
|
+
|
|
595
|
+
# Or manually edit ~/.config/opencode/opencode.json and add:
|
|
596
|
+
{
|
|
597
|
+
"provider": {
|
|
598
|
+
"saros-proxy": {
|
|
599
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
600
|
+
"name": "Saros",
|
|
601
|
+
"options": {
|
|
602
|
+
"baseURL": "http://127.0.0.1:3000/zen/go/v1",
|
|
603
|
+
"apiKey": "not-used"
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
## Security
|
|
613
|
+
|
|
614
|
+
For vulnerability reporting, see [SECURITY.md](SECURITY.md).
|
|
615
|
+
|
|
616
|
+
---
|
|
617
|
+
|
|
618
|
+
## License
|
|
619
|
+
|
|
620
|
+
[MIT](LICENSE) © 2026 Saros Contributors
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencode-config.ts — OpenCode client configuration management.
|
|
3
|
+
*
|
|
4
|
+
* Handles reading, merging, and writing the user's opencode.json
|
|
5
|
+
* to add or update the proxy provider configuration.
|
|
6
|
+
*/
|
|
7
|
+
/** Return the default opencode.json path for the current platform. */
|
|
8
|
+
export declare function getDefaultOpencodeConfigPath(): string;
|
|
9
|
+
export interface OpencodeConfigResult {
|
|
10
|
+
success: boolean;
|
|
11
|
+
path?: string;
|
|
12
|
+
created?: boolean;
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Update or create the user's opencode.json with the proxy provider.
|
|
17
|
+
*
|
|
18
|
+
* @param port — The proxy port to use in baseURL
|
|
19
|
+
* @param options.configPath — Explicit path to opencode.json (optional)
|
|
20
|
+
* @returns Result object with success status and path
|
|
21
|
+
*/
|
|
22
|
+
export declare function updateOpencodeConfig(port: number, options?: {
|
|
23
|
+
configPath?: string;
|
|
24
|
+
}): OpencodeConfigResult;
|
|
25
|
+
/**
|
|
26
|
+
* Generate a manual configuration snippet that users can paste.
|
|
27
|
+
*/
|
|
28
|
+
export declare function generateManualConfigSnippet(port: number): string;
|