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
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ API_KEY=
# bare-source installs so accounts.json / stats.json / logs don't scatter there.
DATA_DIR=

# On the Cascade transport, after GetCascadeModelConfigs returns usable
# per-account catalogs, pool model lists show their union while routing enforces
# the selected account's catalog. DEVIN_CONNECT uses its separate selector
# catalog and is not filtered by this switch.
# Missing, empty, or failed catalog fetches fail open. Set this to 1 only if you
# need the legacy behavior that exposes and routes the full static catalog.
# WINDSURFAPI_IGNORE_CLOUD_FILTER=1

# ========== Reverse proxy / load balancer ==========
# Only honour X-Forwarded-For when a trusted proxy sits in front (e.g. the
# bundled docker-compose nginx LB, or openresty/Caddy). Without this every
Expand Down
3 changes: 2 additions & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ In your client's settings for **Custom OpenAI Compatible**:
| `DEFAULT_MODEL` | `claude-4.5-sonnet-thinking` | The model to use if `model` is not specified. |
| `MAX_TOKENS` | `8192` | Default maximum number of response tokens. |
| `LOG_LEVEL` | `info` | debug / info / warn / error |
| `WINDSURFAPI_IGNORE_CLOUD_FILTER` | `0` | On the Cascade transport, after per-account cloud catalogs sync, pool listings show their union and routing enforces each selected account's catalog. Set to `1` to restore the full static catalog. Missing, empty, or failed catalog syncs fail open. `DEVIN_CONNECT` uses its separate selector catalog. |
| `LS_BINARY_PATH` | `/opt/windsurf/language_server_linux_x64` | Path to the LS binary. |
| `LS_PORT` | `42100` | LS gRPC port. |
| `LS_DATA_DIR` | Linux: `/opt/windsurf/data`; macOS: `~/.windsurf/data` | Per-proxy LS data directory root. |
Expand Down Expand Up @@ -332,7 +333,7 @@ Open `http://YOUR_IP:3003/dashboard`:

## Supported Models

100+ static models in the main catalog plus dynamic cloud-side models added at startup via `mergeCloudModels`. Full list: `GET /v1/models`, or browse the [GitHub Pages model catalog](https://dwgx.github.io/WindsurfAPI/#models) (auto-generated from `src/models.js`).
100+ static models in the main catalog plus dynamic cloud-side models added at startup via `mergeCloudModels`. On the Cascade transport, after per-account cloud catalogs sync, `GET /v1/models` and the Dashboard show the union available across active accounts, while routing applies the selected account's own catalog. `DEVIN_CONNECT` remains governed by its separate selector catalog. The full static catalog remains available on the [GitHub Pages model catalog](https://dwgx.github.io/WindsurfAPI/#models) (auto-generated from `src/models.js`).

<details>
<summary><b>Claude (Anthropic)</b> — 21 models</summary>
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ curl http://localhost:3003/v1/messages \
| `DEFAULT_MODEL` | `claude-4.5-sonnet-thinking` | 不传 model 用哪个 |
| `MAX_TOKENS` | `8192` | 默认最大回复 token 数 |
| `LOG_LEVEL` | `info` | debug / info / warn / error |
| `WINDSURFAPI_IGNORE_CLOUD_FILTER` | `0` | Cascade 路径下,各账号云端 catalog 同步后,账号池列表展示活跃账号目录的并集,路由则校验所选账号自己的目录;设为 `1` 恢复完整静态 catalog。目录缺失、为空或同步失败时保持 fail-open;`DEVIN_CONNECT` 使用独立 selector catalog |
| `LS_BINARY_PATH` | `/opt/windsurf/language_server_linux_x64` | LS 二进制位置 |
| `LS_DATA_DIR` | Linux: `/opt/windsurf/data`;macOS: `~/.windsurf/data` | 每个 proxy 独立的 LS 数据根目录 |
| `LS_PORT` | `42100` | LS gRPC 端口 |
Expand Down Expand Up @@ -325,7 +326,7 @@ curl http://localhost:3003/v1/messages \

## 支持的模型

主线 100+ 个静态模型 + Windsurf 雲端動態下發(`mergeCloudModels` 啟動時拉取最新)。完整列表查 `GET /v1/models`,或看 [GitHub Pages 模型清单](https://dwgx.github.io/WindsurfAPI/#models)(同步生成於 `src/models.js`)。
主线 100+ 个静态模型 + Windsurf 雲端動態下發(`mergeCloudModels` 啟動時拉取最新)。Cascade 路径下,各账号云端 catalog 同步后,`GET /v1/models` 和 Dashboard 展示活跃账号目录的并集,路由则校验所选账号自己的目录;`DEVIN_CONNECT` 继续使用独立 selector catalog;静态完整列表仍可查看 [GitHub Pages 模型清单](https://dwgx.github.io/WindsurfAPI/#models)(同步生成於 `src/models.js`)。

<details>
<summary><b>Claude(Anthropic)</b> — 21 个</summary>
Expand Down
304 changes: 250 additions & 54 deletions src/auth.js

Large diffs are not rendered by default.

17 changes: 11 additions & 6 deletions src/dashboard/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { getClineCompatStats } from '../handlers/cline-compat.js';
import { getCcCompatStats } from '../handlers/cc-compat.js';
import { getLogs, subscribeToLogs, unsubscribeFromLogs } from './logger.js';
import { getProxyConfig, getProxyConfigMasked, setGlobalProxy, setAccountProxy, removeProxy, getEffectiveProxy } from './proxy-config.js';
import { MODELS, MODEL_TIER_ACCESS as _TIER_TABLE, getTierModels as _getTierModels } from '../models.js';
import { MODELS, MODEL_TIER_ACCESS as _TIER_TABLE, getTierModels as _getTierModels, filterModelKeysByCloudCatalog } from '../models.js';
import { windsurfLogin, refreshFirebaseToken, reRegisterWithCodeium } from './windsurf-login.js';
import { getModelAccessConfig, setModelAccessMode, setModelAccessList, addModelToList, removeModelFromList, setDefaultModel } from './model-access.js';
import { checkMessageRateLimit } from '../windsurf-api.js';
Expand Down Expand Up @@ -1185,7 +1185,7 @@ export async function handleDashboardApi(method, subpath, body, req, res) {
pro: _TIER_TABLE.pro,
unknown: _TIER_TABLE.unknown,
expired: _TIER_TABLE.expired,
allModels: Object.keys(MODELS),
allModels: filterModelKeysByCloudCatalog(),
});
}

Expand Down Expand Up @@ -1759,10 +1759,15 @@ export async function handleDashboardApi(method, subpath, body, req, res) {

// ─── Models list ──────────────────────────────────────
if (subpath === '/models' && method === 'GET') {
const models = Object.entries(MODELS).map(([id, info]) => ({
id, name: info.name, provider: info.provider,
credit: typeof info.credit === 'number' ? info.credit : null,
}));
const models = filterModelKeysByCloudCatalog().map((id) => {
const info = MODELS[id];
return {
id,
name: info.name,
provider: info.provider,
credit: typeof info.credit === 'number' ? info.credit : null,
};
});
return json(res, 200, { models });
}

Expand Down
1 change: 1 addition & 0 deletions src/dashboard/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,7 @@
"drought": {
"title": "Quota drought:",
"body": "Every active account is below the weekly quota threshold — add accounts or wait for reset",
"restrictionFailOpen": "No free model is available for the active accounts, so low-quota protection will not block paid models; models unsupported by an account are still hidden and blocked",
"detail": "min weekly={{weekly}}% · min daily={{daily}}% · {{known}}/{{active}} accounts known"
},
"tooltip": {
Expand Down
1 change: 1 addition & 0 deletions src/dashboard/i18n/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,7 @@
"drought": {
"title": "配额预警:",
"body": "所有账号本周配额都低于阈值,建议补账号或等待重置",
"restrictionFailOpen": "当前可用账号中没有免费模型,因此低配额保护不会阻止付费模型;账号本身不支持的模型仍会被隐藏和拦截",
"detail": "最低周额度={{weekly}}% · 最低日额度={{daily}}% · 已知 {{known}}/{{active}} 个账号"
},
"tooltip": {
Expand Down
7 changes: 7 additions & 0 deletions src/dashboard/index-sketch.html
Original file line number Diff line number Diff line change
Expand Up @@ -1746,6 +1746,7 @@ <h1 class="page-title"><em class="em">Account</em> Manager</h1>
<div id="drought-banner" style="display:none;margin:10px 0 16px;padding:14px 18px;border:1.5px dashed #d97706;background:#fff7e6;border-radius:8px;color:#874d00">
<strong data-zh="配额预警 ·" data-en="Quota drought ·">配额预警 ·</strong>
<span id="drought-message" data-zh="所有账号本周配额都低于阈值,建议补账号或等待重置" data-en="every account's weekly quota is under the threshold; add accounts or wait for reset">所有账号本周配额都低于阈值,建议补账号或等待重置</span>
<span id="drought-fail-open-message" style="display:none" data-zh="当前可用账号中没有免费模型,因此低配额保护不会阻止付费模型;账号本身不支持的模型仍会被隐藏和拦截" data-en="no free model is available for the active accounts, so low-quota protection will not block paid models; models unsupported by an account are still hidden and blocked">当前可用账号中没有免费模型,因此低配额保护不会阻止付费模型;账号本身不支持的模型仍会被隐藏和拦截</span>
<span id="drought-detail" class="text-mute" style="margin-left:8px;font-size:12px"></span>
</div>

Expand Down Expand Up @@ -2696,12 +2697,18 @@ <h1 class="page-title"><em class="em">Cre</em>dits</h1>

async loadDrought() {
const banner = document.getElementById('drought-banner');
const msg = document.getElementById('drought-message');
const failOpenMsg = document.getElementById('drought-fail-open-message');
const detail = document.getElementById('drought-detail');
if (!banner) return;
try {
const d = await this.api('GET', '/drought');
if (!d?.drought) { banner.style.display = 'none'; return; }
banner.style.display = '';
if (msg && failOpenMsg) {
msg.style.display = d.restrictionFailOpen ? 'none' : '';
failOpenMsg.style.display = d.restrictionFailOpen ? '' : 'none';
}
if (detail) {
const lw = d.lowestWeeklyPercent;
const ld = d.lowestDailyPercent;
Expand Down
4 changes: 3 additions & 1 deletion src/dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9127,7 +9127,9 @@ <h3 data-i18n="login.title">控制台登录</h3>
known: d.knownAccounts,
active: d.activeAccounts,
});
msg.textContent = I18n.t('drought.body') || msg.textContent;
msg.textContent = d.restrictionFailOpen
? I18n.t('drought.restrictionFailOpen')
: (I18n.t('drought.body') || msg.textContent);
detail.textContent = summary;
} catch (e) {
// no /drought endpoint on older backends — silently hide.
Expand Down
8 changes: 6 additions & 2 deletions src/handlers/models.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ import { getBackendSwitch } from '../runtime-config.js';
// The MODELS table stays full for the Cascade transport; this is a per-transport
// view, not a catalog edit. Non-connect deployments see the full list unchanged.
export function handleModels(env = process.env) {
let data = listModels();
if (getBackendSwitch('devinConnect', env)) {
const effectiveEnv = env === process.env ? env : { ...process.env, ...env };
// listModels receives the same effective environment used for transport
// selection so a DEVIN_CONNECT request is never pre-filtered by the
// unrelated Cascade cloud catalog.
let data = listModels({ env: effectiveEnv });
if (getBackendSwitch('devinConnect', effectiveEnv)) {
// Existence = snapshot ∪ live (same source of truth as resolveConnectSelector,
// audit 2026-07-12). Before this, the filter only consulted the frozen
// CATALOG_SELECTORS snapshot, so live-synced selectors were dropped here even
Expand Down
Loading
Loading