bingocode 1.0.20 → 1.0.21
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/package.json +1 -1
- package/src/entrypoints/init.ts +1 -9
- package/src/server/cli/providerManager.ts +18 -67
- package/src/server/cli/providersMenu.tsx +44 -74
- package/src/server/proxy/handler.ts +289 -302
- package/src/server/services/providerService.ts +24 -10
- package/src/utils/config.ts +5 -11
- package/src/utils/preflightChecks.tsx +3 -3
- package/src/utils/proxy.ts +1 -13
- package/src/server/proxy/streaming/anthropicStreamLabeler.ts +0 -56
package/package.json
CHANGED
package/src/entrypoints/init.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { mkdirSync } from 'node:fs'
|
|
2
1
|
import { profileCheckpoint } from '../utils/startupProfiler.js'
|
|
3
2
|
import '../bootstrap/state.js'
|
|
4
3
|
import '../utils/config.js'
|
|
@@ -26,7 +25,7 @@ import { logForDebugging } from '../utils/debug.js'
|
|
|
26
25
|
import { detectCurrentRepository } from '../utils/detectRepository.js'
|
|
27
26
|
import { logForDiagnosticsNoPII } from '../utils/diagLogs.js'
|
|
28
27
|
import { initJetBrainsDetection } from '../utils/envDynamic.js'
|
|
29
|
-
import {
|
|
28
|
+
import { isEnvTruthy } from '../utils/envUtils.js'
|
|
30
29
|
import { ConfigParseError, errorMessage } from '../utils/errors.js'
|
|
31
30
|
// showInvalidConfigDialog is dynamically imported in the error path to avoid loading React at init
|
|
32
31
|
import {
|
|
@@ -56,13 +55,6 @@ import { setShellIfWindows } from '../utils/windowsPaths.js'
|
|
|
56
55
|
let telemetryInitialized = false
|
|
57
56
|
|
|
58
57
|
export const init = memoize(async (): Promise<void> => {
|
|
59
|
-
// Ensure config directory exists for first-time use
|
|
60
|
-
try {
|
|
61
|
-
mkdirSync(getClaudeConfigHomeDir(), { recursive: true });
|
|
62
|
-
} catch (e) {
|
|
63
|
-
// Ignore error if it already exists
|
|
64
|
-
}
|
|
65
|
-
|
|
66
58
|
const initStartTime = Date.now()
|
|
67
59
|
logForDiagnosticsNoPII('info', 'init_started')
|
|
68
60
|
profileCheckpoint('init_function_start')
|
|
@@ -121,13 +121,14 @@ export class ProviderManager {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
//
|
|
124
|
+
// 预设列表(可选,providerPresets 不存在则返回空)
|
|
125
125
|
static async listPresets(): Promise<any[]> {
|
|
126
126
|
try {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
127
|
+
// 相对 src/server/cli 到 config 的路径
|
|
128
|
+
const mod = await import('../config/providerPresets.ts');
|
|
129
|
+
const presets = (mod as any).presets || (mod as any).providerPresets || [];
|
|
130
|
+
return presets;
|
|
131
|
+
} catch {
|
|
131
132
|
return [];
|
|
132
133
|
}
|
|
133
134
|
}
|
|
@@ -144,83 +145,33 @@ export class ProviderManager {
|
|
|
144
145
|
baseUrl: overrides?.baseUrl ?? p.baseUrl ?? '',
|
|
145
146
|
apiFormat: overrides?.apiFormat ?? p.apiFormat ?? 'openai_chat',
|
|
146
147
|
models: overrides?.models ?? { main: p.defaultModel || '', haiku: '', sonnet: '', opus: '' },
|
|
147
|
-
notes: overrides?.notes ?? p.notes ?? ''
|
|
148
|
-
extra: { ...p, ...overrides?.extra }
|
|
148
|
+
notes: overrides?.notes ?? p.notes ?? ''
|
|
149
149
|
};
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
//
|
|
153
|
-
static async fetchModels(target: SavedProvider | any, apiKeyOverride?: string): Promise<string[]> {
|
|
154
|
-
const p = target;
|
|
155
|
-
const apiKey = apiKeyOverride || p.apiKey;
|
|
156
|
-
const baseUrl = (p.baseUrl || '').replace(/\/+$/, '');
|
|
157
|
-
const modelsUrl = p.modelsUrl || p.extra?.modelsUrl;
|
|
158
|
-
const modelsDataPath = p.modelsDataPath || p.extra?.modelsDataPath || 'data';
|
|
159
|
-
const authStyle = p.modelsAuthStyle || p.extra?.modelsAuthStyle || 'bearer';
|
|
160
|
-
|
|
161
|
-
if (!modelsUrl) return [];
|
|
162
|
-
|
|
163
|
-
const url = modelsUrl.startsWith('http') ? modelsUrl : (baseUrl + modelsUrl);
|
|
164
|
-
|
|
165
|
-
try {
|
|
166
|
-
const headers: any = {};
|
|
167
|
-
if (apiKey) {
|
|
168
|
-
if (authStyle === 'bearer') {
|
|
169
|
-
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
170
|
-
} else if (authStyle === 'x-api-key') {
|
|
171
|
-
headers['x-api-key'] = apiKey;
|
|
172
|
-
headers['anthropic-version'] = '2023-06-01';
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const res = await axios.get(url, { headers, timeout: 5000 });
|
|
177
|
-
const data = res.data;
|
|
178
|
-
const list = modelsDataPath.split('.').reduce((acc: any, part: string) => acc && acc[part], data);
|
|
179
|
-
|
|
180
|
-
if (Array.isArray(list)) {
|
|
181
|
-
return list.map((m: any) => typeof m === 'string' ? m : (m.id || m.name)).filter(Boolean);
|
|
182
|
-
}
|
|
183
|
-
return [];
|
|
184
|
-
} catch (e) {
|
|
185
|
-
console.error('Failed to fetch models:', e);
|
|
186
|
-
return [];
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// 连通性测试:优先使用配置的 modelsUrl
|
|
152
|
+
// 连通性测试:按 apiFormat 选择探测路径
|
|
191
153
|
static async testProvider(target: SavedProvider | string): Promise<{ ok: boolean; latencyMs?: number; message?: string }> {
|
|
192
154
|
const p = typeof target === 'string' ? await this.getProvider(target) : target;
|
|
193
155
|
if (!p) return { ok: false, message: 'Provider not found' };
|
|
194
|
-
|
|
195
|
-
const baseUrl = (p.baseUrl || '').replace(/\/+$/, '');
|
|
196
|
-
const modelsUrl = p.modelsUrl || p.extra?.modelsUrl || (p.apiFormat?.includes('openai') ? '/v1/models' : '/v1/models');
|
|
197
|
-
const authStyle = p.modelsAuthStyle || p.extra?.modelsAuthStyle || 'bearer';
|
|
198
|
-
const apiKey = p.apiKey;
|
|
199
|
-
|
|
200
|
-
const url = modelsUrl.startsWith('http') ? modelsUrl : (baseUrl + modelsUrl);
|
|
156
|
+
const base = (p.baseUrl || '').replace(/\/+$/,'');
|
|
201
157
|
const start = Date.now();
|
|
202
|
-
|
|
158
|
+
// 简单路径推断
|
|
159
|
+
let url = base;
|
|
160
|
+
if ((p.apiFormat || '').includes('openai')) url = base + '/v1/models';
|
|
161
|
+
else if ((p.apiFormat || '').includes('anthropic')) url = base + '/v1/models';
|
|
203
162
|
try {
|
|
204
|
-
const headers: any = {};
|
|
205
|
-
if (apiKey) {
|
|
206
|
-
if (authStyle === 'bearer') {
|
|
207
|
-
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
208
|
-
} else if (authStyle === 'x-api-key') {
|
|
209
|
-
headers['x-api-key'] = apiKey;
|
|
210
|
-
headers['anthropic-version'] = '2023-06-01';
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
163
|
const res = await axios.get(url, {
|
|
215
164
|
timeout: 8000,
|
|
216
|
-
headers
|
|
165
|
+
headers: {
|
|
166
|
+
...(p.apiKey ? { Authorization: `Bearer ${p.apiKey}` } : {}),
|
|
167
|
+
},
|
|
217
168
|
validateStatus: () => true
|
|
218
169
|
});
|
|
219
|
-
|
|
170
|
+
// 2xx/3xx 都视为连通
|
|
220
171
|
if (res.status >= 200 && res.status < 400) {
|
|
221
172
|
return { ok: true, latencyMs: Date.now() - start };
|
|
222
173
|
}
|
|
223
|
-
return { ok: false, message: `HTTP ${res.status}
|
|
174
|
+
return { ok: false, message: `HTTP ${res.status}` };
|
|
224
175
|
} catch (e: any) {
|
|
225
176
|
return { ok: false, message: e?.message || 'network error' };
|
|
226
177
|
}
|
|
@@ -30,9 +30,8 @@ const FIXED_APIFMT = 'openai_chat';
|
|
|
30
30
|
|
|
31
31
|
const ProvidersMenu: React.FC = () => {
|
|
32
32
|
const { exit } = useApp();
|
|
33
|
-
const [modelOptions, setModelOptions] = useState<string[]>([]);
|
|
34
33
|
const [modelSelectIdx, setModelSelectIdx] = useState(0);
|
|
35
|
-
const [addStep, setAddStep] = useState(0); // 0
|
|
34
|
+
const [addStep, setAddStep] = useState(0); // 0:选模型, 1:填key
|
|
36
35
|
const [inputKey, setInputKey] = useState('');
|
|
37
36
|
const [addError, setAddError] = useState('');
|
|
38
37
|
const [mode, setMode] = useState<UiMode>('list');
|
|
@@ -45,9 +44,6 @@ const ProvidersMenu: React.FC = () => {
|
|
|
45
44
|
const [msg, setMsg] = useState<string>('');
|
|
46
45
|
const [detail, setDetail] = useState<SavedProvider | null>(null);
|
|
47
46
|
const [presets, setPresets] = useState<any[]>([]);
|
|
48
|
-
// 临时存储当前正在配置的 Provider 草案
|
|
49
|
-
const [pendingProvider, setPendingProvider] = useState<CreateProviderInput | null>(null);
|
|
50
|
-
|
|
51
47
|
useEffect(() => { ProviderManager.listPresets().then(setPresets).catch(()=>setPresets([])); }, []);
|
|
52
48
|
|
|
53
49
|
const refresh = async () => {
|
|
@@ -58,14 +54,7 @@ const ProvidersMenu: React.FC = () => {
|
|
|
58
54
|
};
|
|
59
55
|
useEffect(() => { refresh(); }, []);
|
|
60
56
|
useEffect(() => {
|
|
61
|
-
if (mode !== 'add') {
|
|
62
|
-
setAddStep(0);
|
|
63
|
-
setInputKey('');
|
|
64
|
-
setAddError('');
|
|
65
|
-
setModelSelectIdx(0);
|
|
66
|
-
setModelOptions([]);
|
|
67
|
-
setPendingProvider(null);
|
|
68
|
-
}
|
|
57
|
+
if (mode !== 'add') { setAddStep(0); setInputKey(''); setAddError(''); setModelSelectIdx(0); }
|
|
69
58
|
}, [mode]);
|
|
70
59
|
|
|
71
60
|
// 主界面 LIST 模式
|
|
@@ -107,55 +96,33 @@ const ProvidersMenu: React.FC = () => {
|
|
|
107
96
|
setMode('list'); setAddError(''); setInputKey(''); setAddStep(0);
|
|
108
97
|
return;
|
|
109
98
|
}
|
|
110
|
-
// 步骤
|
|
111
|
-
if (addStep ===
|
|
112
|
-
if (key.downArrow) setModelSelectIdx(idx => Math.min(
|
|
99
|
+
// 步骤0: 主模型选择
|
|
100
|
+
if (addStep === 0) {
|
|
101
|
+
if (key.downArrow) setModelSelectIdx(idx => Math.min(MODEL_OPTIONS.length - 1, idx + 1));
|
|
113
102
|
else if (key.upArrow) setModelSelectIdx(idx => Math.max(0, idx - 1));
|
|
114
|
-
else if (key.return) {
|
|
115
|
-
completeAdd(modelOptions[modelSelectIdx]);
|
|
116
|
-
}
|
|
103
|
+
else if (key.return) { setAddStep(1); }
|
|
117
104
|
}
|
|
118
105
|
});
|
|
119
106
|
|
|
120
|
-
//
|
|
121
|
-
const
|
|
107
|
+
// 新增表单
|
|
108
|
+
const addSubmit = async (keyInput: string) => {
|
|
109
|
+
const selectedModel = MODEL_OPTIONS[modelSelectIdx];
|
|
110
|
+
const presetId = selectedModel.replace(/[^a-zA-Z0-9]/g, '') + '-preset';
|
|
111
|
+
const name = `${selectedModel} Provider`;
|
|
122
112
|
if (!keyInput.trim()) { setAddError('API Key 不能为空'); return; }
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
baseUrl: FIXED_BASEURL,
|
|
130
|
-
apiFormat: FIXED_APIFMT,
|
|
131
|
-
apiKey: keyInput.trim(),
|
|
132
|
-
models: { main: '', haiku: '', sonnet: '', opus: '' }
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const models = await ProviderManager.fetchModels(draft, keyInput.trim());
|
|
136
|
-
if (models.length === 0) {
|
|
137
|
-
setAddError('未获取到可用模型,请检查 API Key 或网络连通性。');
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
setModelOptions(models);
|
|
141
|
-
setPendingProvider({ ...draft, apiKey: keyInput.trim() });
|
|
142
|
-
setAddStep(1);
|
|
143
|
-
setAddError('');
|
|
144
|
-
} catch (e: any) {
|
|
145
|
-
setAddError('获取模型失败: ' + (e.message || '未知错误'));
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
const completeAdd = async (selectedModel: string) => {
|
|
150
|
-
if (!pendingProvider) return;
|
|
151
|
-
const name = pendingProvider.name || `${selectedModel} Provider`;
|
|
152
|
-
|
|
113
|
+
const exists = (await ProviderManager.listProviders()).some(p => p.id === presetId);
|
|
114
|
+
if (exists) { setAddError('该模型已添加过,如要更换Key请在列表编辑。'); return; }
|
|
115
|
+
setAddError('');
|
|
116
|
+
// 支持预设覆盖
|
|
117
|
+
const overrideBase = (global as any).__PM_BASEURL_OVERRIDE__ || FIXED_BASEURL;
|
|
118
|
+
const overrideFmt = (global as any).__PM_APIFMT_OVERRIDE__ || FIXED_APIFMT;
|
|
153
119
|
await ProviderManager.addProvider({
|
|
154
|
-
|
|
155
|
-
|
|
120
|
+
presetId, name, apiKey: keyInput.trim(),
|
|
121
|
+
baseUrl: overrideBase, apiFormat: overrideFmt,
|
|
156
122
|
models: { main: selectedModel, haiku: '', sonnet: '', opus: '' }
|
|
157
123
|
});
|
|
158
|
-
|
|
124
|
+
delete (global as any).__PM_BASEURL_OVERRIDE__;
|
|
125
|
+
delete (global as any).__PM_APIFMT_OVERRIDE__;
|
|
159
126
|
setMode('list');
|
|
160
127
|
setAddStep(0);
|
|
161
128
|
setInputKey('');
|
|
@@ -186,25 +153,25 @@ const ProvidersMenu: React.FC = () => {
|
|
|
186
153
|
{/* 新增 */}
|
|
187
154
|
{mode === 'add' && (
|
|
188
155
|
<Box flexDirection="column">
|
|
156
|
+
<Text>选择模型与输入API Key:</Text>
|
|
189
157
|
{addStep === 0
|
|
190
158
|
? <>
|
|
191
|
-
|
|
159
|
+
{MODEL_OPTIONS.map((m, idx) => (
|
|
160
|
+
<Text key={m} color={idx === modelSelectIdx ? 'yellow' : undefined}>
|
|
161
|
+
{idx === modelSelectIdx ? '> ' : ' '}{m}
|
|
162
|
+
</Text>
|
|
163
|
+
))}
|
|
164
|
+
<Text color="gray">↑↓选择,回车下一步,q返回</Text>
|
|
165
|
+
</>
|
|
166
|
+
: <>
|
|
167
|
+
<Text>已选模型:{MODEL_OPTIONS[modelSelectIdx]}</Text>
|
|
192
168
|
<TextInput
|
|
193
169
|
value={inputKey}
|
|
194
170
|
onChange={setInputKey}
|
|
195
|
-
onSubmit={
|
|
171
|
+
onSubmit={addSubmit}
|
|
196
172
|
placeholder="请输入API Key"
|
|
197
173
|
/>
|
|
198
|
-
<Text color="gray"
|
|
199
|
-
</>
|
|
200
|
-
: <>
|
|
201
|
-
<Text>获取成功!请选择主模型:</Text>
|
|
202
|
-
{modelOptions.map((m, idx) => (
|
|
203
|
-
<Text key={m} color={idx === modelSelectIdx ? 'yellow' : undefined}>
|
|
204
|
-
{idx === modelSelectIdx ? '> ' : ' '}{m}
|
|
205
|
-
</Text>
|
|
206
|
-
))}
|
|
207
|
-
<Text color="gray">↑↓选择,回车保存,q返回</Text>
|
|
174
|
+
<Text color="gray">输入后回车提交,q返回</Text>
|
|
208
175
|
</>
|
|
209
176
|
}
|
|
210
177
|
{addError && <Text color="red">{addError}</Text>}
|
|
@@ -292,14 +259,17 @@ const ProvidersMenu: React.FC = () => {
|
|
|
292
259
|
<SelectInput
|
|
293
260
|
items={presets.map((p: any) => ({ label: `${p.name || p.id} ${p.baseUrl || ''}`, value: p.id }))}
|
|
294
261
|
onSelect={async it => {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
262
|
+
const p = presets.find((x: any) => x.id === it.value);
|
|
263
|
+
if (!p) return;
|
|
264
|
+
const model = (p.defaultModel || MODEL_OPTIONS[0]);
|
|
265
|
+
const presetBase = p.baseUrl || FIXED_BASEURL;
|
|
266
|
+
const presetFmt = p.apiFormat || FIXED_APIFMT;
|
|
267
|
+
const idx = Math.max(0, MODEL_OPTIONS.findIndex(m => m === model));
|
|
268
|
+
setModelSelectIdx(idx);
|
|
269
|
+
(global as any).__PM_BASEURL_OVERRIDE__ = presetBase;
|
|
270
|
+
(global as any).__PM_APIFMT_OVERRIDE__ = presetFmt;
|
|
271
|
+
setMode('add');
|
|
272
|
+
setAddStep(1); // 直接跳到 Key 输入
|
|
303
273
|
}}
|
|
304
274
|
/>
|
|
305
275
|
<Text color="gray">回车选择预设,q 返回</Text>
|
|
@@ -1,302 +1,289 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Proxy Handler — protocol-translating reverse proxy for OpenAI-compatible APIs.
|
|
3
|
-
*
|
|
4
|
-
* Receives Anthropic Messages API requests from the CLI, transforms them to
|
|
5
|
-
* OpenAI Chat Completions or Responses API format, forwards to the upstream
|
|
6
|
-
* provider, and transforms the response back to Anthropic format.
|
|
7
|
-
*
|
|
8
|
-
* Supports slot-based routing: each request's model field is mapped to a slot
|
|
9
|
-
* (main/haiku/sonnet/opus), and the corresponding configured provider is used.
|
|
10
|
-
*
|
|
11
|
-
* Derived from cc-switch (https://github.com/farion1231/cc-switch)
|
|
12
|
-
* Original work by Jason Young, MIT License
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { ProviderService } from '../services/providerService.js'
|
|
16
|
-
import { anthropicToOpenaiChat } from './transform/anthropicToOpenaiChat.js'
|
|
17
|
-
import { anthropicToOpenaiResponses } from './transform/anthropicToOpenaiResponses.js'
|
|
18
|
-
import { openaiChatToAnthropic } from './transform/openaiChatToAnthropic.js'
|
|
19
|
-
import { openaiResponsesToAnthropic } from './transform/openaiResponsesToAnthropic.js'
|
|
20
|
-
import { openaiChatStreamToAnthropic } from './streaming/openaiChatStreamToAnthropic.js'
|
|
21
|
-
import { openaiResponsesStreamToAnthropic } from './streaming/openaiResponsesStreamToAnthropic.js'
|
|
22
|
-
import {
|
|
23
|
-
import type {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
*
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (m.includes('
|
|
52
|
-
if (m.includes('
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
{
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
},
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
{
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
},
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
*
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
'
|
|
173
|
-
'
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
type: 'error',
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
headers: {
|
|
291
|
-
'Content-Type': 'text/event-stream',
|
|
292
|
-
'Cache-Control': 'no-cache',
|
|
293
|
-
Connection: 'keep-alive',
|
|
294
|
-
},
|
|
295
|
-
})
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Non-streaming
|
|
299
|
-
const responseBody = await upstream.json()
|
|
300
|
-
const anthropicResponse = openaiResponsesToAnthropic(responseBody, uiLabel || body.model)
|
|
301
|
-
return Response.json(anthropicResponse)
|
|
302
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Proxy Handler — protocol-translating reverse proxy for OpenAI-compatible APIs.
|
|
3
|
+
*
|
|
4
|
+
* Receives Anthropic Messages API requests from the CLI, transforms them to
|
|
5
|
+
* OpenAI Chat Completions or Responses API format, forwards to the upstream
|
|
6
|
+
* provider, and transforms the response back to Anthropic format.
|
|
7
|
+
*
|
|
8
|
+
* Supports slot-based routing: each request's model field is mapped to a slot
|
|
9
|
+
* (main/haiku/sonnet/opus), and the corresponding configured provider is used.
|
|
10
|
+
*
|
|
11
|
+
* Derived from cc-switch (https://github.com/farion1231/cc-switch)
|
|
12
|
+
* Original work by Jason Young, MIT License
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { ProviderService } from '../services/providerService.js'
|
|
16
|
+
import { anthropicToOpenaiChat } from './transform/anthropicToOpenaiChat.js'
|
|
17
|
+
import { anthropicToOpenaiResponses } from './transform/anthropicToOpenaiResponses.js'
|
|
18
|
+
import { openaiChatToAnthropic } from './transform/openaiChatToAnthropic.js'
|
|
19
|
+
import { openaiResponsesToAnthropic } from './transform/openaiResponsesToAnthropic.js'
|
|
20
|
+
import { openaiChatStreamToAnthropic } from './streaming/openaiChatStreamToAnthropic.js'
|
|
21
|
+
import { openaiResponsesStreamToAnthropic } from './streaming/openaiResponsesStreamToAnthropic.js'
|
|
22
|
+
import type { AnthropicRequest } from './transform/types.js'
|
|
23
|
+
import type { SlotName } from '../types/provider.js'
|
|
24
|
+
|
|
25
|
+
const providerService = new ProviderService()
|
|
26
|
+
|
|
27
|
+
function buildUpstreamHeaders(apiKey: string): Record<string, string> {
|
|
28
|
+
const headers: Record<string, string> = {
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
Authorization: `Bearer ${apiKey}`,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (process.env.X_API_KEY) {
|
|
34
|
+
headers['x-api-key'] = process.env.X_API_KEY
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (process.env.APPLICATION_NAME) {
|
|
38
|
+
headers['x-application-name'] = process.env.APPLICATION_NAME
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return headers
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Identify which slot a model name belongs to.
|
|
46
|
+
* Claude Code sends model names like "claude-3-5-haiku-20241022".
|
|
47
|
+
*/
|
|
48
|
+
function identifySlot(modelName: string): SlotName {
|
|
49
|
+
const m = modelName.toLowerCase()
|
|
50
|
+
if (m.includes('haiku')) return 'haiku'
|
|
51
|
+
if (m.includes('sonnet')) return 'sonnet'
|
|
52
|
+
if (m.includes('opus')) return 'opus'
|
|
53
|
+
return 'main'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function handleProxyRequest(req: Request, url: URL): Promise<Response> {
|
|
57
|
+
// Only handle POST /proxy/v1/messages
|
|
58
|
+
if (req.method !== 'POST' || url.pathname !== '/proxy/v1/messages') {
|
|
59
|
+
return Response.json(
|
|
60
|
+
{ error: 'Not Found', message: 'Proxy only handles POST /proxy/v1/messages' },
|
|
61
|
+
{ status: 404 },
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Parse request body
|
|
66
|
+
let body: AnthropicRequest
|
|
67
|
+
try {
|
|
68
|
+
body = (await req.json()) as AnthropicRequest
|
|
69
|
+
} catch {
|
|
70
|
+
return Response.json(
|
|
71
|
+
{ type: 'error', error: { type: 'invalid_request_error', message: 'Invalid JSON in request body' } },
|
|
72
|
+
{ status: 400 },
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const isStream = body.stream === true
|
|
77
|
+
|
|
78
|
+
// --- Slot-based routing ---
|
|
79
|
+
const slot = identifySlot(body.model ?? '')
|
|
80
|
+
const slotConfig = await providerService.getProviderForSlot(slot)
|
|
81
|
+
|
|
82
|
+
if (slotConfig) {
|
|
83
|
+
// Use the slot's configured modelId instead of the original Claude model name
|
|
84
|
+
const proxiedBody: AnthropicRequest = { ...body, model: slotConfig.modelId }
|
|
85
|
+
const baseUrl = slotConfig.baseUrl.replace(/\/+$/, '')
|
|
86
|
+
const uiLabel = slotConfig.label || null
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
if (slotConfig.apiFormat === 'anthropic') {
|
|
90
|
+
return await handleAnthropicPassthrough(proxiedBody, baseUrl, slotConfig.apiKey, isStream, uiLabel)
|
|
91
|
+
} else if (slotConfig.apiFormat === 'openai_chat') {
|
|
92
|
+
return await handleOpenaiChat(proxiedBody, baseUrl, slotConfig.apiKey, isStream, uiLabel)
|
|
93
|
+
} else {
|
|
94
|
+
return await handleOpenaiResponses(proxiedBody, baseUrl, slotConfig.apiKey, isStream, uiLabel)
|
|
95
|
+
}
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error(`[Proxy] Slot "${slot}" upstream request failed:`, err)
|
|
98
|
+
return Response.json(
|
|
99
|
+
{
|
|
100
|
+
type: 'error',
|
|
101
|
+
error: {
|
|
102
|
+
type: 'api_error',
|
|
103
|
+
message: err instanceof Error ? err.message : String(err),
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{ status: 502 },
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --- Fallback: legacy single-activeId routing ---
|
|
112
|
+
const config = await providerService.getActiveProviderForProxy()
|
|
113
|
+
if (!config) {
|
|
114
|
+
return Response.json(
|
|
115
|
+
{
|
|
116
|
+
type: 'error',
|
|
117
|
+
error: {
|
|
118
|
+
type: 'invalid_request_error',
|
|
119
|
+
message: `No provider configured for slot "${slot}". Please configure slots in the Provider panel.`,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{ status: 400 },
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (config.apiFormat === 'anthropic') {
|
|
127
|
+
return Response.json(
|
|
128
|
+
{ type: 'error', error: { type: 'invalid_request_error', message: 'Active provider uses anthropic format — proxy not needed' } },
|
|
129
|
+
{ status: 400 },
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const baseUrl = config.baseUrl.replace(/\/+$/, '')
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
if (config.apiFormat === 'openai_chat') {
|
|
137
|
+
return await handleOpenaiChat(body, baseUrl, config.apiKey, isStream)
|
|
138
|
+
} else {
|
|
139
|
+
return await handleOpenaiResponses(body, baseUrl, config.apiKey, isStream)
|
|
140
|
+
}
|
|
141
|
+
} catch (err) {
|
|
142
|
+
console.error('[Proxy] Upstream request failed:', err)
|
|
143
|
+
return Response.json(
|
|
144
|
+
{
|
|
145
|
+
type: 'error',
|
|
146
|
+
error: {
|
|
147
|
+
type: 'api_error',
|
|
148
|
+
message: err instanceof Error ? err.message : String(err),
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
{ status: 502 },
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Pass through to an Anthropic-compatible upstream without format transformation.
|
|
158
|
+
* Used when the slot provider uses apiFormat 'anthropic'.
|
|
159
|
+
*/
|
|
160
|
+
async function handleAnthropicPassthrough(
|
|
161
|
+
body: AnthropicRequest,
|
|
162
|
+
baseUrl: string,
|
|
163
|
+
apiKey: string,
|
|
164
|
+
isStream: boolean,
|
|
165
|
+
uiLabel: string | null = null,
|
|
166
|
+
): Promise<Response> {
|
|
167
|
+
const url = `${baseUrl}/v1/messages`
|
|
168
|
+
const upstream = await fetch(url, {
|
|
169
|
+
method: 'POST',
|
|
170
|
+
headers: {
|
|
171
|
+
'Content-Type': 'application/json',
|
|
172
|
+
'x-api-key': apiKey,
|
|
173
|
+
'anthropic-version': '2023-06-01',
|
|
174
|
+
},
|
|
175
|
+
body: JSON.stringify(body),
|
|
176
|
+
signal: isStream ? AbortSignal.timeout(30_000) : AbortSignal.timeout(300_000),
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// ... (existing error checks)
|
|
180
|
+
|
|
181
|
+
if (isStream) {
|
|
182
|
+
// Anthropic pass-through doesn't easily support label injection without parsing SSE
|
|
183
|
+
// So for native anthropic format, we might just pass original stream
|
|
184
|
+
return new Response(upstream.body, { /* ... */ })
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const responseBody = await upstream.json()
|
|
188
|
+
if (uiLabel) {
|
|
189
|
+
(responseBody as any).model = uiLabel
|
|
190
|
+
}
|
|
191
|
+
return Response.json(responseBody)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function handleOpenaiChat(
|
|
195
|
+
body: AnthropicRequest,
|
|
196
|
+
baseUrl: string,
|
|
197
|
+
apiKey: string,
|
|
198
|
+
isStream: boolean,
|
|
199
|
+
uiLabel: string | null = null,
|
|
200
|
+
): Promise<Response> {
|
|
201
|
+
const transformed = anthropicToOpenaiChat(body)
|
|
202
|
+
const url = `${baseUrl}/v1/chat/completions`
|
|
203
|
+
|
|
204
|
+
const upstream = await fetch(url, {
|
|
205
|
+
method: 'POST',
|
|
206
|
+
headers: buildUpstreamHeaders(apiKey),
|
|
207
|
+
body: JSON.stringify(transformed),
|
|
208
|
+
signal: isStream ? AbortSignal.timeout(30_000) : AbortSignal.timeout(300_000),
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
if (!upstream.ok) {
|
|
212
|
+
// ... error handling
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (isStream) {
|
|
216
|
+
if (!upstream.body) {
|
|
217
|
+
return Response.json(/* ... */)
|
|
218
|
+
}
|
|
219
|
+
const anthropicStream = openaiChatStreamToAnthropic(upstream.body, uiLabel || body.model)
|
|
220
|
+
return new Response(anthropicStream, {
|
|
221
|
+
status: 200,
|
|
222
|
+
headers: {
|
|
223
|
+
'Content-Type': 'text/event-stream',
|
|
224
|
+
'Cache-Control': 'no-cache',
|
|
225
|
+
Connection: 'keep-alive',
|
|
226
|
+
},
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Non-streaming
|
|
231
|
+
const responseBody = await upstream.json()
|
|
232
|
+
const anthropicResponse = openaiChatToAnthropic(responseBody, uiLabel || body.model)
|
|
233
|
+
return Response.json(anthropicResponse)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function handleOpenaiResponses(
|
|
237
|
+
body: AnthropicRequest,
|
|
238
|
+
baseUrl: string,
|
|
239
|
+
apiKey: string,
|
|
240
|
+
isStream: boolean,
|
|
241
|
+
uiLabel: string | null = null,
|
|
242
|
+
): Promise<Response> {
|
|
243
|
+
const transformed = anthropicToOpenaiResponses(body)
|
|
244
|
+
const url = `${baseUrl}/v1/responses`
|
|
245
|
+
|
|
246
|
+
const upstream = await fetch(url, {
|
|
247
|
+
method: 'POST',
|
|
248
|
+
headers: buildUpstreamHeaders(apiKey),
|
|
249
|
+
body: JSON.stringify(transformed),
|
|
250
|
+
signal: isStream ? AbortSignal.timeout(30_000) : AbortSignal.timeout(300_000),
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
if (!upstream.ok) {
|
|
254
|
+
const errText = await upstream.text().catch(() => '')
|
|
255
|
+
return Response.json(
|
|
256
|
+
{
|
|
257
|
+
type: 'error',
|
|
258
|
+
error: {
|
|
259
|
+
type: 'api_error',
|
|
260
|
+
message: `Upstream returned HTTP ${upstream.status}: ${errText.slice(0, 500)}`,
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{ status: upstream.status },
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (isStream) {
|
|
268
|
+
if (!upstream.body) {
|
|
269
|
+
return Response.json(
|
|
270
|
+
{ type: 'error', error: { type: 'api_error', message: 'Upstream returned no body for stream' } },
|
|
271
|
+
{ status: 502 },
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
const anthropicStream = openaiResponsesStreamToAnthropic(upstream.body, uiLabel || body.model)
|
|
275
|
+
return new Response(anthropicStream, {
|
|
276
|
+
status: 200,
|
|
277
|
+
headers: {
|
|
278
|
+
'Content-Type': 'text/event-stream',
|
|
279
|
+
'Cache-Control': 'no-cache',
|
|
280
|
+
Connection: 'keep-alive',
|
|
281
|
+
},
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Non-streaming
|
|
286
|
+
const responseBody = await upstream.json()
|
|
287
|
+
const anthropicResponse = openaiResponsesToAnthropic(responseBody, uiLabel || body.model)
|
|
288
|
+
return Response.json(anthropicResponse)
|
|
289
|
+
}
|
|
@@ -409,8 +409,8 @@ export class ProviderService {
|
|
|
409
409
|
}
|
|
410
410
|
|
|
411
411
|
try {
|
|
412
|
-
const
|
|
413
|
-
const res = await fetch(url, { headers, signal: AbortSignal.timeout(10000), ...
|
|
412
|
+
const directOpts = getDirectFetchOptions()
|
|
413
|
+
const res = await fetch(url, { headers, signal: AbortSignal.timeout(10000), ...directOpts })
|
|
414
414
|
if (!res.ok) {
|
|
415
415
|
console.error(`[ProviderService] Failed to fetch models from ${url}: ${res.status}`)
|
|
416
416
|
return []
|
|
@@ -438,11 +438,26 @@ export class ProviderService {
|
|
|
438
438
|
|
|
439
439
|
// If no modelId provided, try to fetch from provider or use preset default
|
|
440
440
|
let modelId = overrides?.modelId || provider.models.main
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
441
|
+
const needsAutoDetect =
|
|
442
|
+
!modelId ||
|
|
443
|
+
modelId === 'auto' ||
|
|
444
|
+
(apiFormat !== 'anthropic' && modelId.startsWith('claude-'))
|
|
445
|
+
if (needsAutoDetect) {
|
|
446
|
+
const fetched = await this.fetchProviderModels(id).catch(() => [])
|
|
447
|
+
if (fetched.length > 0) {
|
|
448
|
+
modelId = fetched[0] // Use first available model for testing
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// 兜底:如果仍然没有有效的 modelId,直接返回有意义的错误
|
|
453
|
+
if (!modelId) {
|
|
454
|
+
return {
|
|
455
|
+
connectivity: {
|
|
456
|
+
success: false,
|
|
457
|
+
latencyMs: 0,
|
|
458
|
+
error: '无法确定测试用模型:models.main 为空且自动拉取模型列表失败。请先在槽位配置中选择模型,或检查 API Key 和网络连接。',
|
|
459
|
+
},
|
|
460
|
+
}
|
|
446
461
|
}
|
|
447
462
|
|
|
448
463
|
if (!baseUrl || !provider.apiKey) {
|
|
@@ -491,7 +506,6 @@ export class ProviderService {
|
|
|
491
506
|
const start = Date.now()
|
|
492
507
|
try {
|
|
493
508
|
const { url, headers, body } = buildDirectTestRequest(base, apiKey, modelId, format)
|
|
494
|
-
// 使用 getDirectFetchOptions 以绕开系统代理,测试直接连接
|
|
495
509
|
const directOpts = getDirectFetchOptions()
|
|
496
510
|
const response = await fetch(url, {
|
|
497
511
|
method: 'POST',
|
|
@@ -556,13 +570,13 @@ export class ProviderService {
|
|
|
556
570
|
}
|
|
557
571
|
|
|
558
572
|
// Call upstream with transformed request
|
|
559
|
-
const
|
|
573
|
+
const directOpts = getDirectFetchOptions()
|
|
560
574
|
const response = await fetch(upstreamUrl, {
|
|
561
575
|
method: 'POST',
|
|
562
576
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
563
577
|
body: JSON.stringify(transformedBody),
|
|
564
578
|
signal: AbortSignal.timeout(30000),
|
|
565
|
-
...
|
|
579
|
+
...directOpts,
|
|
566
580
|
})
|
|
567
581
|
|
|
568
582
|
if (!response.ok) {
|
package/src/utils/config.ts
CHANGED
|
@@ -1344,17 +1344,11 @@ export function enableConfigs(): void {
|
|
|
1344
1344
|
// to prevent us from adding config reading during module initialization
|
|
1345
1345
|
configReadingAllowed = true
|
|
1346
1346
|
// We only check the global config because currently all the configs share a file
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
)
|
|
1353
|
-
} catch (e) {
|
|
1354
|
-
logForDebugging(`Failed to load config during enableConfigs: ${e}`, { level: 'error' })
|
|
1355
|
-
// If it's a corrupted file, we allow the boostrap to continue with defaults
|
|
1356
|
-
// instead of hard-crashing the process.
|
|
1357
|
-
}
|
|
1347
|
+
getConfig(
|
|
1348
|
+
getGlobalClaudeFile(),
|
|
1349
|
+
createDefaultGlobalConfig,
|
|
1350
|
+
true /* throw on invalid */,
|
|
1351
|
+
)
|
|
1358
1352
|
|
|
1359
1353
|
logForDiagnosticsNoPII('info', 'enable_configs_completed', {
|
|
1360
1354
|
duration_ms: Date.now() - startTime,
|
|
@@ -202,7 +202,7 @@ export function PreflightStep(t0) {
|
|
|
202
202
|
|
|
203
203
|
//@C:ID=F.PC._temp;K=F;V=1.0;P=Helper function for process exit;D=UI;M=Connectivity;S=Utility;In=void;Out=void
|
|
204
204
|
function _temp() {
|
|
205
|
-
console.log("F.PC._temp
|
|
206
|
-
|
|
207
|
-
|
|
205
|
+
console.log("F.PC._temp");
|
|
206
|
+
|
|
207
|
+
return process.exit(1);
|
|
208
208
|
}
|
package/src/utils/proxy.ts
CHANGED
|
@@ -334,26 +334,14 @@ export function getDirectFetchOptions(): {
|
|
|
334
334
|
return { ...base, proxy: undefined, ...getTLSFetchOptions() }
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
// Check if system proxy exists
|
|
338
|
-
const proxyUrl = getProxyUrl()
|
|
339
|
-
if (!proxyUrl) {
|
|
340
|
-
// No proxy configured, just return normal fetch options
|
|
341
|
-
return { ...base, ...getTLSFetchOptions() }
|
|
342
|
-
}
|
|
343
|
-
|
|
344
337
|
// In Node.js/undici, a fresh Agent with no proxy settings bypasses system defaults
|
|
345
338
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
346
339
|
const undiciMod = require('undici') as typeof undici
|
|
347
340
|
const tlsOpts = getTLSFetchOptions()
|
|
348
341
|
|
|
349
|
-
// Use the global dispatcher's options if possible, or fresh default options
|
|
350
|
-
const agentOptions = tlsOpts.dispatcher && 'options' in (tlsOpts.dispatcher as any)
|
|
351
|
-
? (tlsOpts.dispatcher as any).options
|
|
352
|
-
: {}
|
|
353
|
-
|
|
354
342
|
return {
|
|
355
343
|
...base,
|
|
356
|
-
dispatcher: new undiciMod.Agent(
|
|
344
|
+
dispatcher: new undiciMod.Agent(tlsOpts.dispatcher ? (tlsOpts.dispatcher as any).options : {}),
|
|
357
345
|
}
|
|
358
346
|
}
|
|
359
347
|
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Anthropic-to-Anthropic SSE stream labeler.
|
|
3
|
-
*
|
|
4
|
-
* Intercepts an Anthropic Messages API stream and replaces the 'model' field
|
|
5
|
-
* in the 'message_start' event with a custom label.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export function anthropicStreamLabeler(
|
|
9
|
-
upstream: ReadableStream<Uint8Array>,
|
|
10
|
-
label: string,
|
|
11
|
-
): ReadableStream<Uint8Array> {
|
|
12
|
-
const encoder = new TextEncoder()
|
|
13
|
-
const decoder = new TextDecoder()
|
|
14
|
-
let buffer = ''
|
|
15
|
-
|
|
16
|
-
return new ReadableStream({
|
|
17
|
-
async start(controller) {
|
|
18
|
-
const reader = upstream.getReader()
|
|
19
|
-
try {
|
|
20
|
-
while (true) {
|
|
21
|
-
const { done, value } = await reader.read()
|
|
22
|
-
if (done) break
|
|
23
|
-
|
|
24
|
-
buffer += decoder.decode(value, { stream: true })
|
|
25
|
-
const lines = buffer.split('\n')
|
|
26
|
-
buffer = lines.pop() || ''
|
|
27
|
-
|
|
28
|
-
for (const line of lines) {
|
|
29
|
-
const trimmed = line.trim()
|
|
30
|
-
if (!trimmed || !trimmed.startsWith('data: ')) {
|
|
31
|
-
controller.enqueue(encoder.encode(line + '\n'))
|
|
32
|
-
continue
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const jsonStr = trimmed.slice(6)
|
|
36
|
-
try {
|
|
37
|
-
const data = JSON.parse(jsonStr)
|
|
38
|
-
if (data.type === 'message_start' && data.message) {
|
|
39
|
-
data.message.model = label
|
|
40
|
-
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n`))
|
|
41
|
-
} else {
|
|
42
|
-
controller.enqueue(encoder.encode(line + '\n'))
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
controller.enqueue(encoder.encode(line + '\n'))
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
} catch (err) {
|
|
50
|
-
controller.error(err)
|
|
51
|
-
} finally {
|
|
52
|
-
controller.close()
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
})
|
|
56
|
-
}
|