Skip to content

Commit b0bca38

Browse files
committed
fix(native): honor requested model in bridge gates
1 parent 02e4a6b commit b0bca38

10 files changed

Lines changed: 70 additions & 6 deletions

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,8 @@ curl http://localhost:3003/v1/messages \
276276
| `LS_PREWARM_DEFAULT` | `1` | 设为 `0` 可跳过启动时 default LS 预热,低内存/全 proxy 池改为首个真实请求再懒启动 |
277277
| `LS_PREWARM_PROXIES` | `0` | 设为 `1` 才在启动时预热所有 proxy LS;默认按需启动。后台 scheduled probe / 预测 prewarm 只复用空闲常驻 LS,不会为了探测新开/等待/驱逐 LS |
278278
| `LS_PREWARM_ON_ACCOUNT_ADD` | `0` | 设为 `1` 才在 Dashboard/批量导入/OAuth 添加账号后立即预热对应 LS;默认避免批量录入打爆内存 |
279-
| `WINDSURFAPI_NATIVE_TOOL_BRIDGE` || `all_mapped` 仅在 Read/Bash/Grep/Glob 及其别名全部可映射时走 native bridge;`1` 为混合工具 partition 模式。WebSearch/WebFetch 默认仍走 prompt emulation,需显式加入工具 allowlist |
280-
| `WINDSURFAPI_NATIVE_TOOL_BRIDGE_TOOLS` | `Read,Bash,Grep,Glob` 语义族 | native bridge 工具 allowlist。默认包含 `Read/read_file/view_file``Bash/shell_command/run_command``Grep/grep_search_v2``Glob/find`,不含 WebSearch/WebFetch |
279+
| `WINDSURFAPI_NATIVE_TOOL_BRIDGE` || `all_mapped` 仅在已 allowlist 的工具全部可映射时走 native bridge;`1` 为混合工具 partition 模式。WebSearch/WebFetch 默认仍走 prompt emulation,需显式加入工具 allowlist |
280+
| `WINDSURFAPI_NATIVE_TOOL_BRIDGE_TOOLS` | `Bash/shell_command/run_command` 语义族 | native bridge 工具 allowlist。默认只包含成熟的 Bash/run_command 路径;Read/Grep/GlobWebSearch/WebFetch 需显式加入 allowlist 后再用灰度账号/API key 实测 |
281281
| `WINDSURFAPI_NATIVE_TOOL_BRIDGE_MODELS` / `PROVIDERS` / `ROUTES` / `CALLERS` / `ACCOUNTS` / `API_KEYS` || native bridge 灰度门。为空表示不限;设置后必须匹配才启用。`ACCOUNTS` 可填账号 id/email,`API_KEYS` 匹配调用方 API key 但不会把明文 key 传进 chat 逻辑 |
282282
| `WINDSURFAPI_NATIVE_TOOL_BRIDGE_OFF` || 设为 `1` 强制关闭 native tool bridge,优先级高于上面的开关 |
283283
| `WINDSURFAPI_SPECIAL_AGENT_BACKEND` || 可选 special-agent 后端。设为 `devin-cli` 后,`swe-1.6` / `swe-1.6-fast` / `adaptive` / `arena-*` 不再走 direct Cascade,而是走 Devin CLI PoC |
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
## v2.0.129 - Native bridge gray-gate model alias fix
2+
3+
- Fixed the native bridge model gray gate so it checks both the internal routing
4+
model key and the caller's requested model alias. A real v2.0.128 canary with
5+
`WINDSURFAPI_NATIVE_TOOL_BRIDGE_MODELS=claude-haiku-4.5` was incorrectly
6+
rejected after chat routing normalized the request to `claude-4.5-haiku`.
7+
- Native bridge decision telemetry now includes `requestedModel` alongside
8+
`modelKey`, making dashboard/health output explain whether a gray gate matched
9+
the request alias or the internal routing key.
10+
- README native-bridge defaults now match the code: production default allowlist
11+
is still the mature Bash/run_command path only. Read/Grep/Glob and
12+
WebSearch/WebFetch remain explicit gray-canary tools until their live protocol
13+
matrix is proven.
14+
15+
Verification:
16+
17+
- `node --check src\cascade-native-bridge.js`
18+
- `node --check src\native-bridge-stats.js`
19+
- `node --check src\handlers\chat.js`
20+
- `node --test test\native-tool-routing.test.js test\native-bridge-stats.test.js test\cascade-native-bridge.test.js test\dashboard-api.test.js`
21+
- `node --test --test-timeout=120000 --test-force-exit test\*.test.js`

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "windsurf-api",
3-
"version": "2.0.128",
3+
"version": "2.0.129",
44
"description": "Windsurf to OpenAI + Anthropic compatible API proxy. Turns Windsurf's 107 AI models (Claude, GPT, Gemini, DeepSeek, Grok, Qwen, Kimi, GLM, SWE) into dual-protocol API endpoints. Zero npm deps.",
55
"type": "module",
66
"main": "src/index.js",

src/cascade-native-bridge.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1350,6 +1350,7 @@ function nativeBridgeDecision({
13501350
explicitOn,
13511351
allMappedOnly,
13521352
modelKey,
1353+
model,
13531354
provider,
13541355
route,
13551356
} = {}) {
@@ -1363,6 +1364,7 @@ function nativeBridgeDecision({
13631364
allMappedOnly: !!allMappedOnly,
13641365
useCascade: !!useCascade,
13651366
modelKey: String(modelKey || ''),
1367+
requestedModel: String(model || ''),
13661368
provider: String(provider || ''),
13671369
route: String(route || ''),
13681370
hasTools: Array.isArray(tools) && tools.length > 0,
@@ -1394,6 +1396,7 @@ export function getNativeBridgeDecision(tools, {
13941396
explicitOn,
13951397
allMappedOnly,
13961398
modelKey,
1399+
model,
13971400
provider,
13981401
route,
13991402
};
@@ -1423,9 +1426,10 @@ export function getNativeBridgeDecision(tools, {
14231426
return nativeBridgeDecision({ ...base, enabled: true, reason: 'native_bridge_enabled' });
14241427
}
14251428

1426-
export function shouldUseNativeBridge(tools, { modelKey = '', provider = '', route = '', callerKey = '' } = {}) {
1429+
export function shouldUseNativeBridge(tools, { modelKey = '', model = '', provider = '', route = '', callerKey = '' } = {}) {
14271430
return getNativeBridgeDecision(tools, {
14281431
modelKey,
1432+
model,
14291433
provider,
14301434
route,
14311435
callerKey,

src/handlers/chat.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,12 @@ function upstreamTransientErrorMessage(model, triedCount, reason = 'internal_err
120120
return `${model} 上游 Windsurf Cascade 服务瞬态故障:已在 ${triedCount} 个账号上重试都收到 ${detail}。这是上游或本地语言服务器会话的瞬时问题,建议 30-60 秒后重试;若连续出现,请重启语言服务器。`;
121121
}
122122

123-
export function buildToolRoutingPlan(tools, { useCascade = false, modelKey = '', provider = null, route = 'chat', callerKey = '' } = {}) {
123+
export function buildToolRoutingPlan(tools, { useCascade = false, modelKey = '', model = '', provider = null, route = 'chat', callerKey = '' } = {}) {
124124
const hasTools = Array.isArray(tools) && tools.length > 0;
125125
const nativeDecision = getNativeBridgeDecision(tools || [], {
126126
useCascade,
127127
modelKey,
128+
model,
128129
provider,
129130
route,
130131
callerKey,
@@ -1582,6 +1583,7 @@ async function _handleChatCompletionsInner(body, context = {}) {
15821583
const toolRouting = buildToolRoutingPlan(effectiveTools, {
15831584
useCascade,
15841585
modelKey: routingModelKey,
1586+
model: reqModel || body.model || modelKey,
15851587
provider: modelInfo?.provider || null,
15861588
route: body.__route || 'chat',
15871589
callerKey: nativeBridgeCallerKey,

src/native-bridge-stats.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ function sanitizeDecision(decision = {}) {
6565
mode: safeString(decision.mode, 40),
6666
useCascade: decision.useCascade !== false,
6767
modelKey: safeString(decision.modelKey || decision.model, 120),
68+
requestedModel: safeString(decision.requestedModel || decision.model, 120),
6869
provider: safeString(decision.provider, 80),
6970
route: safeString(decision.route, 80),
7071
toolChoiceFiltered: !!decision.toolChoiceFiltered,

src/runtime-config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ export function getRuntimeConfig() {
129129
return structuredClone(_state);
130130
}
131131

132+
export function _resetRuntimeConfigForTests(patch = {}) {
133+
_state = deepMerge(structuredClone(DEFAULTS), patch);
134+
return getRuntimeConfig();
135+
}
136+
132137
export function getExperimental() {
133138
return { ...(_state.experimental || {}) };
134139
}

test/dashboard-api.test.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
import { afterEach, describe, it } from 'node:test';
22
import assert from 'node:assert/strict';
33
import { config } from '../src/config.js';
4-
import { configureBindHost } from '../src/auth.js';
4+
import { configureBindHost, _resetLockoutForTests } from '../src/auth.js';
55
import { buildBatchProxyBinding, handleDashboardApi } from '../src/dashboard/api.js';
66
import {
77
recordNativeBridgeDecision,
88
resetNativeBridgeStats,
99
} from '../src/native-bridge-stats.js';
10+
import { _resetRuntimeConfigForTests } from '../src/runtime-config.js';
1011

1112
const originalDashboardPassword = config.dashboardPassword;
1213
const originalApiKey = config.apiKey;
1314
const originalNativeBridgeApiKeys = process.env.WINDSURFAPI_NATIVE_TOOL_BRIDGE_API_KEYS;
1415
const originalNativeBridgeAccounts = process.env.WINDSURFAPI_NATIVE_TOOL_BRIDGE_ACCOUNTS;
1516

1617
afterEach(() => {
18+
_resetRuntimeConfigForTests();
19+
_resetLockoutForTests();
1720
config.dashboardPassword = originalDashboardPassword;
1821
config.apiKey = originalApiKey;
1922
if (originalNativeBridgeApiKeys === undefined) delete process.env.WINDSURFAPI_NATIVE_TOOL_BRIDGE_API_KEYS;
@@ -51,6 +54,7 @@ describe('dashboard batch import proxy binding', () => {
5154
});
5255

5356
it('fails closed for dashboard write APIs without auth on non-localhost binds', async () => {
57+
_resetRuntimeConfigForTests();
5458
config.dashboardPassword = '';
5559
config.apiKey = '';
5660
configureBindHost('0.0.0.0');
@@ -63,6 +67,7 @@ describe('dashboard batch import proxy binding', () => {
6367
});
6468

6569
it('allows unauthenticated dashboard writes only on localhost binds', async () => {
70+
_resetRuntimeConfigForTests();
6671
config.dashboardPassword = '';
6772
config.apiKey = '';
6873
configureBindHost('127.0.0.1');
@@ -74,6 +79,7 @@ describe('dashboard batch import proxy binding', () => {
7479
});
7580

7681
it('accepts dashboard auth headers with timing-safe configured secrets', async () => {
82+
_resetRuntimeConfigForTests();
7783
config.dashboardPassword = 'dash-secret';
7884
config.apiKey = '';
7985
configureBindHost('0.0.0.0');
@@ -85,6 +91,7 @@ describe('dashboard batch import proxy binding', () => {
8591
});
8692

8793
it('includes sanitized native bridge telemetry in authenticated overview', async () => {
94+
_resetRuntimeConfigForTests();
8895
config.dashboardPassword = 'dash-secret';
8996
config.apiKey = '';
9097
configureBindHost('0.0.0.0');

test/native-bridge-stats.test.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ describe('native bridge runtime stats', () => {
6060
mode: 'all_mapped',
6161
useCascade: true,
6262
modelKey: 'gpt-5.5-medium',
63+
requestedModel: 'gpt-5.5',
6364
provider: 'openai',
6465
route: 'chat',
6566
callerKey: 'api:secret-hash',
@@ -76,6 +77,7 @@ describe('native bridge runtime stats', () => {
7677
mode: '1',
7778
useCascade: true,
7879
modelKey: 'claude-sonnet-4.6',
80+
requestedModel: 'claude-sonnet-4.6',
7981
provider: 'anthropic',
8082
route: 'chat',
8183
hasTools: true,
@@ -91,6 +93,8 @@ describe('native bridge runtime stats', () => {
9193
assert.equal(stats.decisionReasons.native_bridge_model_not_allowed, 1);
9294
assert.equal(stats.decisionReasons.native_bridge_enabled, 1);
9395
assert.equal(stats.lastDecision.reason, 'native_bridge_enabled');
96+
assert.equal(stats.recentDecisions[0].modelKey, 'gpt-5.5-medium');
97+
assert.equal(stats.recentDecisions[0].requestedModel, 'gpt-5.5');
9498
assert.equal(stats.recentDecisions.length, 2);
9599
assert.equal(JSON.stringify(stats).includes('secret-hash'), false);
96100
});

test/native-tool-routing.test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,26 @@ describe('native mapped-tool routing', () => {
436436
assert.equal(allowed.nativeDecision.reason, 'native_bridge_enabled');
437437
});
438438

439+
it('honors requested model aliases in native bridge model gray gates', () => {
440+
process.env.WINDSURFAPI_NATIVE_TOOL_BRIDGE = 'all_mapped';
441+
process.env.WINDSURFAPI_NATIVE_TOOL_BRIDGE_TOOLS = 'Read';
442+
process.env.WINDSURFAPI_NATIVE_TOOL_BRIDGE_MODELS = 'claude-haiku-4.5';
443+
delete process.env.WINDSURFAPI_NATIVE_TOOL_BRIDGE_OFF;
444+
445+
const plan = buildToolRoutingPlan([fnTool('Read')], {
446+
useCascade: true,
447+
modelKey: 'claude-4.5-haiku',
448+
model: 'claude-haiku-4.5',
449+
provider: 'anthropic',
450+
route: 'chat',
451+
});
452+
453+
assert.equal(plan.nativeBridgeOn, true);
454+
assert.equal(plan.nativeDecision.reason, 'native_bridge_enabled');
455+
assert.equal(plan.nativeDecision.modelKey, 'claude-4.5-haiku');
456+
assert.equal(plan.nativeDecision.requestedModel, 'claude-haiku-4.5');
457+
});
458+
439459
it('explains native bridge disabled decisions for operators', () => {
440460
delete process.env.WINDSURFAPI_NATIVE_TOOL_BRIDGE;
441461
delete process.env.WINDSURFAPI_NATIVE_TOOL_BRIDGE_TOOLS;

0 commit comments

Comments
 (0)