Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Copy this file to .env before running docker compose.

API_KEY=change-me
# Multi-key auth (A): comma-separated; any entry passes. Empty = no auth.
# Canonical list name first, legacy alias second (both merge, empty never blocks).
# OPENCODE_API_KEYS=key-alpha,key-beta
# API_KEYS=key-alpha,key-beta,key-gamma
OPENCODE_SERVER_PASSWORD=change-me-too

# Safer default. Keep true unless you really want model tool access.
Expand Down Expand Up @@ -41,6 +45,12 @@ OPENCODE_PROXY_CLEANUP_MAX_AGE_MS=86400000
# OPENCODE_INTERNAL_ALLOWED_TOOLS=
# OPENCODE_INTERNAL_TOOL_METRICS_ENABLED=true
# OPENCODE_TOOL_DISCOVERY_FIXTURE=
# Fallback proxy pool (P3): direct-only until a free-limit 429 engages it.
# Socks5 first-class; loopback targets always bypass.
# OPENCODE_UPSTREAM_PROXIES=socks5://user:pass@10.0.0.1:1080,http://user:pass@10.0.0.2:8080
# OPENCODE_UPSTREAM_PROXY_STRATEGY=failover-rr
# OPENCODE_UPSTREAM_PROXY_COOLDOWN_MS=300000
# OPENCODE_UPSTREAM_PROXY_NO_PROXY=localhost,127.0.0.1,::1
# Diagnostics
# OPENCODE_HEALTH_DETAILS_ENABLED=true
# OPENCODE_HEALTH_DETAILS_REQUIRE_AUTH=true
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ jobs:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20
# Must satisfy package.json engines (undici 8 needs >=22.19).
node-version: 24
cache: npm
- run: npm ci
- run: npm run typecheck
Expand Down
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

## 2. Tech stack & commands (single source of truth)

- `node >= 18`, TypeScript (`tsconfig.json`: `module/moduleResolution NodeNext`, `strict`, `noEmitOnError`), Express, Jest 30 + Supertest via `@swc/jest`, `tsx` for dev.
- `node >= 22.19` (undici 8 `engines` floor), TypeScript (`tsconfig.json`: `module/moduleResolution NodeNext`, `strict`, `noEmitOnError`), Express, Jest 30 + Supertest via `@swc/jest`, `tsx` for dev.
- Run in this order:
```bash
npm run typecheck # tsc --noEmit (covers index.ts + src/**/*.ts only; tests excluded by design)
Expand Down Expand Up @@ -69,6 +69,8 @@ config.json.example / .env.example # examples only, never real secrets
| Binary / zen | `OPENCODE_PATH` / `OPENCODE_ZEN_API_KEY` | `OPENCODE_PATH` / `ZEN_API_KEY` | `opencode` / `''` |
| Backend mgmt | `OPENCODE_PROXY_MANAGE_BACKEND` | `MANAGE_BACKEND` | see known drift below |
| Auth | `API_KEY` (env+file same name) | `API_KEY` | `''` (= no auth) |
| Auth multi-key (A) | `OPENCODE_API_KEYS` canonical, `API_KEYS` legacy alias (merge; empty never blocks) | `API_KEYS` | `(none, merges with API_KEY)` |
| Fallback proxies | `OPENCODE_UPSTREAM_PROXIES` / `OPENCODE_UPSTREAM_PROXY_STRATEGY` / `OPENCODE_UPSTREAM_PROXY_COOLDOWN_MS` / `OPENCODE_UPSTREAM_PROXY_NO_PROXY` | `UPSTREAM_*` short forms | `(none)` / `failover-rr` / `300000` / `localhost,127.0.0.1,::1` |

- `DISABLE_TOOLS` resolution: `OPENCODE_DISABLE_TOOLS > DISABLE_TOOLS > file > true` via `resolveDisableTools` (`src/config/proxy-config.ts:47-57`); covered by `tests/env-alias.test.js`. Never `??`-chain booleans by hand; call the helper.
- `RETRY`: raw `env ?? file ?? 3`, normalized by `resolveMaxRetries` (`src/retry/policy.ts:51-58`, clamp 0–5, total attempts `1+n`, `2s×2ⁿ⁻¹` + 25% jitter, `retry-after` clamped to 30s).
Expand Down
6 changes: 6 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ EXPOSE 10000 10001

ENV OPENCODE_SERVER_PASSWORD=
ENV API_KEY=
ENV API_KEYS=
ENV OPENCODE_API_KEYS=
ENV BIND_HOST=0.0.0.0
ENV DISABLE_TOOLS=true
ENV OPENCODE_DISABLE_TOOLS=
Expand All @@ -74,6 +76,10 @@ ENV OPENCODE_PROXY_CLEANUP_INTERVAL_MS=43200000
ENV OPENCODE_PROXY_CLEANUP_MAX_AGE_MS=86400000
ENV OPENCODE_PROXY_REQUEST_TIMEOUT_MS=180000
ENV OPENCODE_PROXY_RETRY_MAX_RETRIES=3
ENV OPENCODE_UPSTREAM_PROXIES=
ENV OPENCODE_UPSTREAM_PROXY_STRATEGY=failover-rr
ENV OPENCODE_UPSTREAM_PROXY_COOLDOWN_MS=300000
ENV OPENCODE_UPSTREAM_PROXY_NO_PROXY=localhost,127.0.0.1,::1

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["opencode", "serve", "--hostname", "0.0.0.0", "--port", "10001"]
7 changes: 6 additions & 1 deletion config.json.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"PORT": 10000,
"API_KEY": "your-secret-api-key",
"API_KEYS": [],
"BIND_HOST": "0.0.0.0",
"DISABLE_TOOLS": true,
"EXTERNAL_TOOLS_MODE": "proxy-bridge",
Expand All @@ -26,5 +27,9 @@
"ZEN_API_KEY": "",
"MANAGE_BACKEND": false,
"REQUEST_TIMEOUT_MS": 180000,
"RETRY_MAX_RETRIES": 3
"RETRY_MAX_RETRIES": 3,
"UPSTREAM_PROXIES": [],
"UPSTREAM_PROXY_STRATEGY": "failover-rr",
"UPSTREAM_PROXY_COOLDOWN_MS": 300000,
"UPSTREAM_PROXY_NO_PROXY": ["localhost", "127.0.0.1", "::1"]
}
6 changes: 6 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ services:
- OPENCODE_SERVER_PASSWORD=${OPENCODE_SERVER_PASSWORD:-}
- OPENCODE_ZEN_API_KEY=${OPENCODE_ZEN_API_KEY:-}
- API_KEY=${API_KEY:-}
- API_KEYS=${API_KEYS:-}
- OPENCODE_API_KEYS=${OPENCODE_API_KEYS:-}
- BIND_HOST=${BIND_HOST:-${OPENCODE_PROXY_BIND_HOST:-0.0.0.0}}
- DISABLE_TOOLS=${DISABLE_TOOLS:-true}
- OPENCODE_DISABLE_TOOLS=${OPENCODE_DISABLE_TOOLS:-}
Expand All @@ -25,6 +27,10 @@ services:
- OPENCODE_PROXY_CLEANUP_MAX_AGE_MS=${OPENCODE_PROXY_CLEANUP_MAX_AGE_MS:-86400000}
- OPENCODE_PROXY_REQUEST_TIMEOUT_MS=${OPENCODE_PROXY_REQUEST_TIMEOUT_MS:-180000}
- OPENCODE_PROXY_RETRY_MAX_RETRIES=${OPENCODE_PROXY_RETRY_MAX_RETRIES:-3}
- OPENCODE_UPSTREAM_PROXIES=${OPENCODE_UPSTREAM_PROXIES:-}
- OPENCODE_UPSTREAM_PROXY_STRATEGY=${OPENCODE_UPSTREAM_PROXY_STRATEGY:-failover-rr}
- OPENCODE_UPSTREAM_PROXY_COOLDOWN_MS=${OPENCODE_UPSTREAM_PROXY_COOLDOWN_MS:-300000}
- OPENCODE_UPSTREAM_PROXY_NO_PROXY=${OPENCODE_UPSTREAM_PROXY_NO_PROXY:-localhost,127.0.0.1,::1}
- OPENCODE_PROXY_PORT=${OPENCODE_PROXY_PORT:-10000}
- OPENCODE_SERVER_PORT=${OPENCODE_SERVER_PORT:-10001}
- OPENCODE_SERVER_URL=${OPENCODE_SERVER_URL:-}
Expand Down
34 changes: 34 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,40 @@ curl -X POST http://127.0.0.1:10000/v1/responses \

非流式 `responses` 响应会在 `response.output` 中返回 `type: "function_call"` 项;流式模式会发送 function_call 生命周期和参数增量事件。

**服务端联网搜索(`web_search`):**

```bash
curl -X POST http://127.0.0.1:10000/v1/responses \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "opencode-go/kimi-k2.5-free",
"input": "PostgreSQL 最新版本是多少?",
"tools": [{"type": "web_search"}]
}'
```

`tools: [{"type": "web_search"}]`(亦接受 `web_search_preview` / `google_search`)是显式授权:代理将其从外部 function 注册表剔除,转驱动 opencode 内置 `websearch`(需模型 provider 为 `opencode`/`opencode-go`,否则需 `OPENCODE_ENABLE_EXA=1`),并在输出中返回 `type: "web_search_call"` 项(含真实执行的 `action.query`)与 `url_citation` 引用(仅标注答案中实际出现的来源 URL,无编造)。流式模式会发送 `response.web_search_call.searching/completed` 事件,最终 `response.completed` 携带完整引用。

### 🔁 Interactions API(Gemini 兼容薄层)

```http
POST /v1beta/interactions
POST /v1/interactions
```

| 参数 | 类型 | 必填 | 说明 |
|:-----|:-----|:-----|:-----|
| `model` / `agent` | string | ✅(二选一) | 模型 ID(`agent` 暂仅占位) |
| `input` | string \| array | ✅ | 输入文本/消息 |
| `system_instruction` | string | - | 系统指令 |
| `tools` | array | - | 仅支持 `{type: "google_search"}`(等价 `web_search`),其余 function 工具返回 400 |
| `previous_interaction_id` | string | - | 续写上轮会话(等价 responses 的 `previous_response_id`) |
| `stream` | boolean | - | SSE:`interaction.created` / `step.delta` / `interaction.completed`(无 `[DONE]`,错误为 `{type:'error'}` 事件;15s heartbeat) |
| `store` | boolean | - | `false` 则不持久化;仅删除本请求新建的会话,复用的父会话保留(注意:带 `previous_interaction_id` 的 `store:false` 轮次仍会追加到父会话历史,并非完全无痕) |

响应为 `Interaction` 资源:`{id, status, model, output_text, steps[], usage: {grounding_tool_count}}`,其中 `steps` 含 `google_search_call{queries}`、`google_search_result{sources}`(标注为 opencode websearch 代理结果,非 Google 原生)、`model_output{text, annotations}`。文本往返与 `web_search` 接地逻辑复用 Responses 管线;限流错误的重试与代理 fallback 和其余路由一致(engage + 换 session,最多 `maxAttempts` 次),普通错误直接返回。

### 🧭 推荐提示模板(OpenClaw / Claude Code)

在真实运行环境中,如果你希望第一跳**稳定先产出 tool call**,推荐把“调用工具”和“基于工具结果继续回答”拆成两步,而不是混在同一句里。
Expand Down
22 changes: 20 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
| `PORT` / `OPENCODE_PROXY_PORT` | `10000` | 代理服务端口 |
| `OPENCODE_SERVER_PORT` | `10001` | OpenCode 后端服务端口 |
| `API_KEY` | - | Bearer Token 认证密钥 |
| `API_KEYS` / `OPENCODE_API_KEYS` | - | 多 client keys(逗号分隔,任一通过;与 `API_KEY` 合并;为空回退免认证) |
| `BIND_HOST` | `0.0.0.0` | 绑定地址(`BIND_HOST` 优先,`OPENCODE_PROXY_BIND_HOST` 为后备) |
| `OPENCODE_SERVER_URL` | `http://127.0.0.1:10001` | OpenCode 后端地址 |
| `OPENCODE_SERVER_PASSWORD` | - | OpenCode 后端密码 |
Expand All @@ -33,7 +34,7 @@
| `OPENCODE_EXTERNAL_TOOLS_MODE` | `proxy-bridge` | 外部工具桥接模式;当前仅支持 `proxy-bridge` |
| `OPENCODE_EXTERNAL_TOOLS_CONFLICT_POLICY` | `namespace` | 外部工具冲突隔离策略;当前仅支持 `namespace` |
| `OPENCODE_INTERNAL_WEB_FETCH_ENABLED` | `false` | 兼容旧开关;未显式配置 allowlist 时,启用后默认放行 `web_fetch` |
| `OPENCODE_INTERNAL_ALLOWED_TOOLS` | `(none)` | 当请求未传入 `tools` 时允许使用的 OpenCode 内置工具列表,逗号分隔 |
| `OPENCODE_INTERNAL_ALLOWED_TOOLS` | `(none)` | 当请求未传入 `tools` 时允许使用的 OpenCode 内置工具列表,逗号分隔(例 `websearch,webfetch`;`web_fetch` 等旧写法仍可匹配,大小写/分隔符不敏感) |
| `OPENCODE_INTERNAL_TOOL_METRICS_ENABLED` | `true` | 输出 internal allowlist 模式的调试/指标日志 |
| `OPENCODE_TOOL_DISCOVERY_FIXTURE` | `(none)` | 集成测试/本地调试用的固定后端工具 ID 列表,逗号分隔 |
| `OPENCODE_HEALTH_DETAILS_ENABLED` | `true` | 控制 `/health/details` 是否暴露 |
Expand All @@ -51,6 +52,21 @@

> 重试退避移植自上游 `session/retry.ts`(`2s×2ⁿ⁻¹` +25% jitter),但 `retry-after` 等待 clamp 在 30s(上游近无界;网关面对自带超时的客户端不宜久睡)。旧部署注意:默认总尝试由 3 次变为 1+3=4 次,如需接近旧次数可设 `2`。

### 免费额度 fallback 代理

平时直连、零开销;仅当上游返回免费/Go 配额耗尽(`429 + FreeUsageLimitError/GoUsageLimitError`)时自动切到代理并按冷却粘滞,普通 5xx 不触发。

> 两跳模型:代理作用于网关 → OpenCode 后端这一跳。若后端是远端地址(`OPENCODE_SERVER_URL` 非 loopback),切换出口 IP 可命中新的匿名/IP 配额;若是默认本地托管后端(`127.0.0.1`,恒直连 bypass),fallback 退化为同后端直接重试 + engaged 状态标记——此时如需改变后端 → Zen 的出口 IP,需给后端进程配 `HTTP(S)_PROXY`(`spawn` 会继承环境)。

| 变量 | 默认值 | 说明 |
|:-----|:-------|:-----|
| `OPENCODE_UPSTREAM_PROXIES` | `(none)` | 逗号分隔的代理 URL(`socks5://` 优先,亦支持 `http(s)://`;`config.json` 中用短键 `UPSTREAM_PROXIES` 数组) |
| `OPENCODE_UPSTREAM_PROXY_STRATEGY` | `failover-rr` | `failover-rr` / `round-robin` / `random`(连续限流即轮换下一个) |
| `OPENCODE_UPSTREAM_PROXY_COOLDOWN_MS` | `300000` | engaged 粘滞时长(毫秒),到期回直连 |
| `OPENCODE_UPSTREAM_PROXY_NO_PROXY` | `localhost,127.0.0.1,::1` | 永不走代理的目标 host(默认后端 `127.0.0.1` 恒直连) |

> 注意:`GET /health/details` 的 `internal_tools.fallback_proxies` 可观察 engaged 状态;`/metrics` 有 `opencode_fallback_proxy_engaged` gauge。流式 SSE 不走自定义 fetch(上游 SDK 缺口),fallback 自动降级为轮询。

### 调试配置

| 变量 | 默认值 | 说明 |
Expand All @@ -67,6 +83,7 @@
{
"PORT": 10000,
"API_KEY": "your-secret-api-key",
"API_KEYS": ["key-alpha", "key-beta"],
"BIND_HOST": "0.0.0.0",
"DISABLE_TOOLS": true,
"EXTERNAL_TOOLS_MODE": "proxy-bridge",
Expand Down Expand Up @@ -113,7 +130,8 @@ OpenCode2API 现在支持把外部客户端传入的 OpenAI-compatible `tools`

- 当请求 **未传入** `tools` 时,代理会进入 internal allowlist 模式,只允许 `OPENCODE_INTERNAL_ALLOWED_TOOLS` 中声明的 OpenCode 内置工具。
- `OPENCODE_INTERNAL_WEB_FETCH_ENABLED=true` 仅用于兼容旧配置:如果未显式配置 allowlist,则默认把 allowlist 视为 `web_fetch`。
- 代理会读取后端工具列表,并通过精确匹配或 `.<tool>` / `/<tool>` 后缀匹配解析最终可用工具。
- 代理会读取后端工具列表,并通过精确匹配、`.<tool>` / `/<tool>` 后缀匹配或大小写/分隔符不敏感匹配(如 `web_fetch` ↔ `webfetch`)解析最终可用工具。
- 要启用上游搜索(`opencode`/`opencode-go` provider 自带 `websearch`,免额外 key):`OPENCODE_INTERNAL_ALLOWED_TOOLS=websearch,webfetch`,模型用 `opencode-go/<model>`,并确保后端 `permission.websearch=allow`(默认 agent 已放行,无人值守勿设 `ask`)。
- 如果配置的 allowlist 在后端工具列表中一个也没有匹配到,代理会自动回退到“全部内置工具禁用”的安全模式。
- `OPENCODE_INTERNAL_TOOL_METRICS_ENABLED=true` 时,会输出 internal allowlist 模式的调试/指标日志,记录模式选择、后端工具发现、allowlist 命中情况和降级原因,但不会记录工具输出内容。
- `OPENCODE_TOOL_DISCOVERY_FIXTURE` 可在集成测试或本地调试时绕过真实 `client.tool.ids()`,直接提供固定工具 ID 列表。
Expand Down
53 changes: 50 additions & 3 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// P4 TS: root entrypoint (ported from index.js, behavior identical).
import { startProxy, normalizeBool, resolveDisableTools } from './src/proxy.js';
import { resolveMaxRetries } from './src/retry/policy.js';
import { mergeApiKeySources } from './src/auth/keys.js';
import { DEFAULT_PROXY_COOLDOWN_MS, normalizeProxyCooldownMs, normalizeProxyStrategy, parseProxyList, parseProxyNoProxyList } from './src/upstream-proxy/pool.js';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
Expand Down Expand Up @@ -40,10 +42,16 @@ function parsePort(value: unknown, fallback: number): number {
return Number.isFinite(n) && n > 0 ? n : fallback;
}

/** Resolve the multi-key list for one layer: canonical list + legacy aliases merge (empty never blocks). */
function parseApiKeys(...sources: unknown[]): string[] {
return mergeApiKeySources(...sources);
}

// Default configuration
const defaultConfig = {
PORT: parsePort(process.env['OPENCODE_PROXY_PORT'], 10000),
API_KEY: '',
API_KEYS: [] as string[],
OPENCODE_SERVER_URL: `http://127.0.0.1:${process.env['OPENCODE_SERVER_PORT'] || 10001}`,
OPENCODE_SERVER_PASSWORD: process.env['OPENCODE_SERVER_PASSWORD'] || '',
MANAGE_BACKEND: parseBool(process.env['OPENCODE_PROXY_MANAGE_BACKEND'], false),
Expand All @@ -65,6 +73,13 @@ const defaultConfig = {
AUTO_CLEANUP_CONVERSATIONS: parseBool(process.env['OPENCODE_PROXY_AUTO_CLEANUP_CONVERSATIONS'], false),
CLEANUP_INTERVAL_MS: parsePort(process.env['OPENCODE_PROXY_CLEANUP_INTERVAL_MS'], 43200000),
CLEANUP_MAX_AGE_MS: parsePort(process.env['OPENCODE_PROXY_CLEANUP_MAX_AGE_MS'], 86400000),
UPSTREAM_PROXIES: parseProxyList([process.env['OPENCODE_UPSTREAM_PROXIES'], process.env['UPSTREAM_PROXIES']]),
UPSTREAM_PROXY_STRATEGY: 'failover-rr',
UPSTREAM_PROXY_COOLDOWN_MS: DEFAULT_PROXY_COOLDOWN_MS,
UPSTREAM_PROXY_NO_PROXY: parseProxyNoProxyList(
process.env['OPENCODE_UPSTREAM_PROXY_NO_PROXY'] ?? process.env['UPSTREAM_PROXY_NO_PROXY'],
['localhost', '127.0.0.1', '::1'],
),
};

// Load config from file.
Expand Down Expand Up @@ -110,12 +125,18 @@ function readFileNumber(key: string, fallback: number): number {
}

// Merge configs: env > file > default
const finalConfig: ProxyConfig = {
PORT:
// Auth layers resolve independently; the env layer wins as a whole so a stale
// file single-key can never pollute (or resurrect after rotation of) an
// explicitly configured env list. Empty strings never block lower sources.
const envApiKeys = parseApiKeys(process.env['OPENCODE_API_KEYS'], process.env['API_KEYS'], process.env['API_KEY']);
const fileApiKeys = parseApiKeys(fileConfig['API_KEYS'], fileConfig['API_KEY']);
const resolvedApiKeys = envApiKeys.length > 0 ? envApiKeys : fileApiKeys;
const finalConfig: ProxyConfig = { PORT:
parsePort(process.env['OPENCODE_PROXY_PORT'], NaN) ||
parsePort(process.env['PORT'], NaN) ||
readFileNumber('PORT', defaultConfig.PORT),
API_KEY: process.env['API_KEY'] || readFileString('API_KEY', defaultConfig.API_KEY),
API_KEY: resolvedApiKeys[0] ?? '',
API_KEYS: resolvedApiKeys,
OPENCODE_SERVER_URL: process.env['OPENCODE_SERVER_URL'] || readFileString('OPENCODE_SERVER_URL', defaultConfig.OPENCODE_SERVER_URL),
OPENCODE_SERVER_PASSWORD:
process.env['OPENCODE_SERVER_PASSWORD'] || readFileString('OPENCODE_SERVER_PASSWORD', defaultConfig.OPENCODE_SERVER_PASSWORD),
Expand Down Expand Up @@ -196,6 +217,26 @@ const finalConfig: ProxyConfig = {
parsePort(process.env['OPENCODE_PROXY_CLEANUP_MAX_AGE_MS'], 0) ||
readFileNumber('CLEANUP_MAX_AGE_MS', defaultConfig.CLEANUP_MAX_AGE_MS),
OPENCODE_HOME_BASE: null,
// Fallback proxy pool (P3): env layer wins as a whole, then file, then default.
// Empty = direct-only (default, zero overhead; engagable only on free-limit).
UPSTREAM_PROXIES: (() => {
const env = parseProxyList([process.env['OPENCODE_UPSTREAM_PROXIES'], process.env['UPSTREAM_PROXIES']]);
if (env.length > 0) return env;
const file = parseProxyList(fileConfig['UPSTREAM_PROXIES']);
return file.length > 0 ? file : [...defaultConfig.UPSTREAM_PROXIES];
})(),
UPSTREAM_PROXY_STRATEGY: normalizeProxyStrategy(
process.env['OPENCODE_UPSTREAM_PROXY_STRATEGY'] ||
readFileString('UPSTREAM_PROXY_STRATEGY', defaultConfig.UPSTREAM_PROXY_STRATEGY),
),
UPSTREAM_PROXY_COOLDOWN_MS: normalizeProxyCooldownMs(
parsePort(process.env['OPENCODE_UPSTREAM_PROXY_COOLDOWN_MS'], 0) ||
readFileNumber('UPSTREAM_PROXY_COOLDOWN_MS', defaultConfig.UPSTREAM_PROXY_COOLDOWN_MS),
),
UPSTREAM_PROXY_NO_PROXY: parseProxyNoProxyList(
process.env['OPENCODE_UPSTREAM_PROXY_NO_PROXY'] ?? process.env['UPSTREAM_PROXY_NO_PROXY'] ?? fileConfig['UPSTREAM_PROXY_NO_PROXY'],
defaultConfig.UPSTREAM_PROXY_NO_PROXY,
),
};

// Validate required configuration
Expand Down Expand Up @@ -223,6 +264,12 @@ console.log(` - Backend: ${finalConfig.OPENCODE_SERVER_URL}`);
console.log(` - Backend Password: ${finalConfig.OPENCODE_SERVER_PASSWORD ? 'Configured' : 'Not configured'}`);
console.log(` - OpenCode Path: ${finalConfig.OPENCODE_PATH}`);
console.log(` - API Key: ${finalConfig.API_KEY ? 'Configured' : 'Not configured (no auth)'}`);
console.log(
` - API Keys: ${finalConfig.API_KEYS.length > 0 ? `Configured (n=${finalConfig.API_KEYS.length})` : 'Not configured'}`,
);
console.log(
` - Fallback Proxies: ${finalConfig.UPSTREAM_PROXIES.length > 0 ? `Configured (n=${finalConfig.UPSTREAM_PROXIES.length}, ${finalConfig.UPSTREAM_PROXY_STRATEGY})` : 'Direct-only'}`,
);
console.log(` - Zen API Key: ${finalConfig.ZEN_API_KEY ? 'Configured' : 'Not configured'}`);
console.log(` - Disable Tools: ${finalConfig.DISABLE_TOOLS ? 'Yes' : 'No'}`);
console.log(` - External Tools Mode: ${finalConfig.EXTERNAL_TOOLS_MODE}`);
Expand Down
Loading
Loading