bingocode 1.0.18 → 1.0.19
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/bin/claude +1 -1
- package/package.json +1 -1
- package/src/cli/ProviderPanel.tsx +47 -9
- package/src/manager/CliMenuManager.tsx +83 -7
- package/src/server/__tests__/conversation-service.test.ts +7 -7
- package/src/server/__tests__/haha-oauth-service.test.ts +1 -1
- package/src/server/__tests__/providers-real.test.ts +15 -15
- package/src/server/api/computer-use.ts +2 -2
- package/src/server/api/providers.ts +6 -2
- package/src/server/cli/listProviders.ts +1 -1
- package/src/server/cli/providerManager.ts +1 -1
- package/src/server/config/providers.yaml +207 -207
- package/src/server/proxy/handler.ts +30 -47
- package/src/server/proxy/streaming/anthropicStreamLabeler.ts +56 -0
- package/src/server/services/conversationService.ts +5 -5
- package/src/server/services/hahaOAuthService.ts +1 -1
- package/src/server/services/providerManager.ts +1 -1
- package/src/server/services/providerService.ts +32 -14
- package/src/server/types/provider.ts +1 -0
- package/src/utils/computerUse/wrapper.tsx +2 -2
- package/src/utils/managedEnv.ts +23 -15
- package/src/utils/proxy.ts +13 -1
|
@@ -1,207 +1,207 @@
|
|
|
1
|
-
version: 2
|
|
2
|
-
|
|
3
|
-
# Provider 预设配置
|
|
4
|
-
# fields 数组声明新增时需填写的字段
|
|
5
|
-
# key: 'name' | 'apiKey' | 'baseUrl' 直接映射到顶层字段,其余存入 extra.<key>
|
|
6
|
-
# secret: true 时前端使用密码掩码显示
|
|
7
|
-
#
|
|
8
|
-
# modelsUrl: 相对于 baseUrl 的模型列表路径,空字符串表示不支持动态拉取
|
|
9
|
-
# modelsAuthStyle: bearer → Authorization: Bearer <apiKey>
|
|
10
|
-
# x-api-key → x-api-key: <apiKey> + anthropic-version header
|
|
11
|
-
# modelsDataPath: 响应 JSON 中模型数组的字段名(几乎总是 'data')
|
|
12
|
-
|
|
13
|
-
presets:
|
|
14
|
-
- id: official
|
|
15
|
-
name: Claude Official
|
|
16
|
-
baseUrl: ''
|
|
17
|
-
apiFormat: anthropic
|
|
18
|
-
needsApiKey: false
|
|
19
|
-
websiteUrl: https://www.anthropic.com/claude-code
|
|
20
|
-
modelsUrl: /v1/models
|
|
21
|
-
modelsAuthStyle: x-api-key
|
|
22
|
-
modelsDataPath: data
|
|
23
|
-
fields:
|
|
24
|
-
- key: name
|
|
25
|
-
label: Provider 昵称
|
|
26
|
-
required: true
|
|
27
|
-
secret: false
|
|
28
|
-
placeholder: 'e.g. Claude Official'
|
|
29
|
-
|
|
30
|
-
- id: openai
|
|
31
|
-
name: OpenAI
|
|
32
|
-
baseUrl: https://api.openai.com/v1
|
|
33
|
-
apiFormat: openai_chat
|
|
34
|
-
needsApiKey: true
|
|
35
|
-
websiteUrl: https://platform.openai.com
|
|
36
|
-
modelsUrl: /v1/models
|
|
37
|
-
modelsAuthStyle: bearer
|
|
38
|
-
modelsDataPath: data
|
|
39
|
-
fields:
|
|
40
|
-
- key: name
|
|
41
|
-
label: Provider 昵称
|
|
42
|
-
required: true
|
|
43
|
-
secret: false
|
|
44
|
-
placeholder: 'e.g. My OpenAI'
|
|
45
|
-
- key: apiKey
|
|
46
|
-
label: API Key
|
|
47
|
-
required: true
|
|
48
|
-
secret: true
|
|
49
|
-
placeholder: 'sk-...'
|
|
50
|
-
- key: baseUrl
|
|
51
|
-
label: Base URL (Optional)
|
|
52
|
-
required: false
|
|
53
|
-
secret: false
|
|
54
|
-
default: https://api.openai.com/v1
|
|
55
|
-
placeholder: 'https://api.openai.com/v1'
|
|
56
|
-
|
|
57
|
-
- id: gemini
|
|
58
|
-
name: Google Gemini
|
|
59
|
-
baseUrl: https://generativelanguage.googleapis.com/v1beta/openai
|
|
60
|
-
apiFormat: openai_chat
|
|
61
|
-
needsApiKey: true
|
|
62
|
-
websiteUrl: https://aistudio.google.com
|
|
63
|
-
modelsUrl: /v1/models
|
|
64
|
-
modelsAuthStyle: bearer
|
|
65
|
-
modelsDataPath: data
|
|
66
|
-
fields:
|
|
67
|
-
- key: name
|
|
68
|
-
label: Provider 昵称
|
|
69
|
-
required: true
|
|
70
|
-
secret: false
|
|
71
|
-
placeholder: 'e.g. My Gemini'
|
|
72
|
-
- key: apiKey
|
|
73
|
-
label: API Key
|
|
74
|
-
required: true
|
|
75
|
-
secret: true
|
|
76
|
-
placeholder: 'Gemini API Key'
|
|
77
|
-
|
|
78
|
-
- id: mistral
|
|
79
|
-
name: Mistral AI
|
|
80
|
-
baseUrl: https://api.mistral.ai/v1
|
|
81
|
-
apiFormat: openai_chat
|
|
82
|
-
needsApiKey: true
|
|
83
|
-
websiteUrl: https://console.mistral.ai
|
|
84
|
-
modelsUrl: /v1/models
|
|
85
|
-
modelsAuthStyle: bearer
|
|
86
|
-
modelsDataPath: data
|
|
87
|
-
fields:
|
|
88
|
-
- key: name
|
|
89
|
-
label: Provider 昵称
|
|
90
|
-
required: true
|
|
91
|
-
secret: false
|
|
92
|
-
placeholder: 'e.g. My Mistral'
|
|
93
|
-
- key: apiKey
|
|
94
|
-
label: API Key
|
|
95
|
-
required: true
|
|
96
|
-
secret: true
|
|
97
|
-
placeholder: 'Mistral API Key'
|
|
98
|
-
|
|
99
|
-
- id: deepseek
|
|
100
|
-
name: DeepSeek
|
|
101
|
-
baseUrl: https://api.deepseek.com
|
|
102
|
-
apiFormat:
|
|
103
|
-
needsApiKey: true
|
|
104
|
-
websiteUrl: https://platform.deepseek.com
|
|
105
|
-
modelsUrl: /v1/models
|
|
106
|
-
modelsAuthStyle: bearer
|
|
107
|
-
modelsDataPath: data
|
|
108
|
-
fields:
|
|
109
|
-
- key: name
|
|
110
|
-
label: Provider 昵称
|
|
111
|
-
required: true
|
|
112
|
-
secret: false
|
|
113
|
-
placeholder: 'e.g. My DeepSeek'
|
|
114
|
-
- key: apiKey
|
|
115
|
-
label: API Key
|
|
116
|
-
required: true
|
|
117
|
-
secret: true
|
|
118
|
-
placeholder: 'sk-...'
|
|
119
|
-
|
|
120
|
-
- id: zhipuglm
|
|
121
|
-
name: Zhipu GLM
|
|
122
|
-
baseUrl: https://open.bigmodel.cn/api/
|
|
123
|
-
apiFormat:
|
|
124
|
-
needsApiKey: true
|
|
125
|
-
websiteUrl: https://open.bigmodel.cn
|
|
126
|
-
modelsUrl: /
|
|
127
|
-
modelsAuthStyle: bearer
|
|
128
|
-
modelsDataPath: data
|
|
129
|
-
fields:
|
|
130
|
-
- key: name
|
|
131
|
-
label: Provider 昵称
|
|
132
|
-
required: true
|
|
133
|
-
secret: false
|
|
134
|
-
placeholder: 'e.g. My GLM'
|
|
135
|
-
- key: apiKey
|
|
136
|
-
label: API Key
|
|
137
|
-
required: true
|
|
138
|
-
secret: true
|
|
139
|
-
placeholder: '智谱 API Key'
|
|
140
|
-
|
|
141
|
-
- id: kimi
|
|
142
|
-
name: Kimi
|
|
143
|
-
baseUrl: https://api.moonshot.cn/
|
|
144
|
-
apiFormat:
|
|
145
|
-
needsApiKey: true
|
|
146
|
-
websiteUrl: https://platform.moonshot.cn
|
|
147
|
-
modelsUrl: /
|
|
148
|
-
modelsAuthStyle: bearer
|
|
149
|
-
modelsDataPath: data
|
|
150
|
-
fields:
|
|
151
|
-
- key: name
|
|
152
|
-
label: Provider 昵称
|
|
153
|
-
required: true
|
|
154
|
-
secret: false
|
|
155
|
-
placeholder: 'e.g. My Kimi'
|
|
156
|
-
- key: apiKey
|
|
157
|
-
label: API Key
|
|
158
|
-
required: true
|
|
159
|
-
secret: true
|
|
160
|
-
placeholder: 'Moonshot API Key'
|
|
161
|
-
|
|
162
|
-
- id: minimax
|
|
163
|
-
name: MiniMax
|
|
164
|
-
baseUrl: https://api.minimaxi.com/
|
|
165
|
-
apiFormat:
|
|
166
|
-
needsApiKey: true
|
|
167
|
-
websiteUrl: https://platform.minimaxi.com
|
|
168
|
-
modelsUrl: /
|
|
169
|
-
modelsAuthStyle: bearer
|
|
170
|
-
modelsDataPath: data
|
|
171
|
-
fields:
|
|
172
|
-
- key: name
|
|
173
|
-
label: Provider 昵称
|
|
174
|
-
required: true
|
|
175
|
-
secret: false
|
|
176
|
-
placeholder: 'e.g. My MiniMax'
|
|
177
|
-
- key: apiKey
|
|
178
|
-
label: API Key
|
|
179
|
-
required: true
|
|
180
|
-
secret: true
|
|
181
|
-
placeholder: 'MiniMax API Key'
|
|
182
|
-
|
|
183
|
-
- id: custom
|
|
184
|
-
name: Custom
|
|
185
|
-
baseUrl: ''
|
|
186
|
-
apiFormat: openai_chat
|
|
187
|
-
needsApiKey: true
|
|
188
|
-
websiteUrl: ''
|
|
189
|
-
modelsUrl: /v1/models
|
|
190
|
-
modelsAuthStyle: bearer
|
|
191
|
-
modelsDataPath: data
|
|
192
|
-
fields:
|
|
193
|
-
- key: name
|
|
194
|
-
label: Provider 昵称
|
|
195
|
-
required: true
|
|
196
|
-
secret: false
|
|
197
|
-
placeholder: 'e.g. My Custom Provider'
|
|
198
|
-
- key: baseUrl
|
|
199
|
-
label: Base URL
|
|
200
|
-
required: true
|
|
201
|
-
secret: false
|
|
202
|
-
placeholder: 'https://your-api-endpoint.com/v1'
|
|
203
|
-
- key: apiKey
|
|
204
|
-
label: API Key
|
|
205
|
-
required: false
|
|
206
|
-
secret: true
|
|
207
|
-
placeholder: '(可选)API Key'
|
|
1
|
+
version: 2
|
|
2
|
+
|
|
3
|
+
# Provider 预设配置
|
|
4
|
+
# fields 数组声明新增时需填写的字段
|
|
5
|
+
# key: 'name' | 'apiKey' | 'baseUrl' 直接映射到顶层字段,其余存入 extra.<key>
|
|
6
|
+
# secret: true 时前端使用密码掩码显示
|
|
7
|
+
#
|
|
8
|
+
# modelsUrl: 相对于 baseUrl 的模型列表路径,空字符串表示不支持动态拉取
|
|
9
|
+
# modelsAuthStyle: bearer → Authorization: Bearer <apiKey>
|
|
10
|
+
# x-api-key → x-api-key: <apiKey> + anthropic-version header
|
|
11
|
+
# modelsDataPath: 响应 JSON 中模型数组的字段名(几乎总是 'data')
|
|
12
|
+
|
|
13
|
+
presets:
|
|
14
|
+
- id: official
|
|
15
|
+
name: Claude Official
|
|
16
|
+
baseUrl: ''
|
|
17
|
+
apiFormat: anthropic
|
|
18
|
+
needsApiKey: false
|
|
19
|
+
websiteUrl: https://www.anthropic.com/claude-code
|
|
20
|
+
modelsUrl: /v1/models
|
|
21
|
+
modelsAuthStyle: x-api-key
|
|
22
|
+
modelsDataPath: data
|
|
23
|
+
fields:
|
|
24
|
+
- key: name
|
|
25
|
+
label: Provider 昵称
|
|
26
|
+
required: true
|
|
27
|
+
secret: false
|
|
28
|
+
placeholder: 'e.g. Claude Official'
|
|
29
|
+
|
|
30
|
+
- id: openai
|
|
31
|
+
name: OpenAI
|
|
32
|
+
baseUrl: https://api.openai.com/v1
|
|
33
|
+
apiFormat: openai_chat
|
|
34
|
+
needsApiKey: true
|
|
35
|
+
websiteUrl: https://platform.openai.com
|
|
36
|
+
modelsUrl: /v1/models
|
|
37
|
+
modelsAuthStyle: bearer
|
|
38
|
+
modelsDataPath: data
|
|
39
|
+
fields:
|
|
40
|
+
- key: name
|
|
41
|
+
label: Provider 昵称
|
|
42
|
+
required: true
|
|
43
|
+
secret: false
|
|
44
|
+
placeholder: 'e.g. My OpenAI'
|
|
45
|
+
- key: apiKey
|
|
46
|
+
label: API Key
|
|
47
|
+
required: true
|
|
48
|
+
secret: true
|
|
49
|
+
placeholder: 'sk-...'
|
|
50
|
+
- key: baseUrl
|
|
51
|
+
label: Base URL (Optional)
|
|
52
|
+
required: false
|
|
53
|
+
secret: false
|
|
54
|
+
default: https://api.openai.com/v1
|
|
55
|
+
placeholder: 'https://api.openai.com/v1'
|
|
56
|
+
|
|
57
|
+
- id: gemini
|
|
58
|
+
name: Google Gemini
|
|
59
|
+
baseUrl: https://generativelanguage.googleapis.com/v1beta/openai
|
|
60
|
+
apiFormat: openai_chat
|
|
61
|
+
needsApiKey: true
|
|
62
|
+
websiteUrl: https://aistudio.google.com
|
|
63
|
+
modelsUrl: /v1/models
|
|
64
|
+
modelsAuthStyle: bearer
|
|
65
|
+
modelsDataPath: data
|
|
66
|
+
fields:
|
|
67
|
+
- key: name
|
|
68
|
+
label: Provider 昵称
|
|
69
|
+
required: true
|
|
70
|
+
secret: false
|
|
71
|
+
placeholder: 'e.g. My Gemini'
|
|
72
|
+
- key: apiKey
|
|
73
|
+
label: API Key
|
|
74
|
+
required: true
|
|
75
|
+
secret: true
|
|
76
|
+
placeholder: 'Gemini API Key'
|
|
77
|
+
|
|
78
|
+
- id: mistral
|
|
79
|
+
name: Mistral AI
|
|
80
|
+
baseUrl: https://api.mistral.ai/v1
|
|
81
|
+
apiFormat: openai_chat
|
|
82
|
+
needsApiKey: true
|
|
83
|
+
websiteUrl: https://console.mistral.ai
|
|
84
|
+
modelsUrl: /v1/models
|
|
85
|
+
modelsAuthStyle: bearer
|
|
86
|
+
modelsDataPath: data
|
|
87
|
+
fields:
|
|
88
|
+
- key: name
|
|
89
|
+
label: Provider 昵称
|
|
90
|
+
required: true
|
|
91
|
+
secret: false
|
|
92
|
+
placeholder: 'e.g. My Mistral'
|
|
93
|
+
- key: apiKey
|
|
94
|
+
label: API Key
|
|
95
|
+
required: true
|
|
96
|
+
secret: true
|
|
97
|
+
placeholder: 'Mistral API Key'
|
|
98
|
+
|
|
99
|
+
- id: deepseek
|
|
100
|
+
name: DeepSeek
|
|
101
|
+
baseUrl: https://api.deepseek.com
|
|
102
|
+
apiFormat: openai_chat
|
|
103
|
+
needsApiKey: true
|
|
104
|
+
websiteUrl: https://platform.deepseek.com
|
|
105
|
+
modelsUrl: /v1/models
|
|
106
|
+
modelsAuthStyle: bearer
|
|
107
|
+
modelsDataPath: data
|
|
108
|
+
fields:
|
|
109
|
+
- key: name
|
|
110
|
+
label: Provider 昵称
|
|
111
|
+
required: true
|
|
112
|
+
secret: false
|
|
113
|
+
placeholder: 'e.g. My DeepSeek'
|
|
114
|
+
- key: apiKey
|
|
115
|
+
label: API Key
|
|
116
|
+
required: true
|
|
117
|
+
secret: true
|
|
118
|
+
placeholder: 'sk-...'
|
|
119
|
+
|
|
120
|
+
- id: zhipuglm
|
|
121
|
+
name: Zhipu GLM
|
|
122
|
+
baseUrl: https://open.bigmodel.cn/api/paas/v4
|
|
123
|
+
apiFormat: openai_chat
|
|
124
|
+
needsApiKey: true
|
|
125
|
+
websiteUrl: https://open.bigmodel.cn
|
|
126
|
+
modelsUrl: /models
|
|
127
|
+
modelsAuthStyle: bearer
|
|
128
|
+
modelsDataPath: data
|
|
129
|
+
fields:
|
|
130
|
+
- key: name
|
|
131
|
+
label: Provider 昵称
|
|
132
|
+
required: true
|
|
133
|
+
secret: false
|
|
134
|
+
placeholder: 'e.g. My GLM'
|
|
135
|
+
- key: apiKey
|
|
136
|
+
label: API Key
|
|
137
|
+
required: true
|
|
138
|
+
secret: true
|
|
139
|
+
placeholder: '智谱 API Key'
|
|
140
|
+
|
|
141
|
+
- id: kimi
|
|
142
|
+
name: Kimi
|
|
143
|
+
baseUrl: https://api.moonshot.cn/v1
|
|
144
|
+
apiFormat: openai_chat
|
|
145
|
+
needsApiKey: true
|
|
146
|
+
websiteUrl: https://platform.moonshot.cn
|
|
147
|
+
modelsUrl: /models
|
|
148
|
+
modelsAuthStyle: bearer
|
|
149
|
+
modelsDataPath: data
|
|
150
|
+
fields:
|
|
151
|
+
- key: name
|
|
152
|
+
label: Provider 昵称
|
|
153
|
+
required: true
|
|
154
|
+
secret: false
|
|
155
|
+
placeholder: 'e.g. My Kimi'
|
|
156
|
+
- key: apiKey
|
|
157
|
+
label: API Key
|
|
158
|
+
required: true
|
|
159
|
+
secret: true
|
|
160
|
+
placeholder: 'Moonshot API Key'
|
|
161
|
+
|
|
162
|
+
- id: minimax
|
|
163
|
+
name: MiniMax
|
|
164
|
+
baseUrl: https://api.minimaxi.com/v1
|
|
165
|
+
apiFormat: openai_chat
|
|
166
|
+
needsApiKey: true
|
|
167
|
+
websiteUrl: https://platform.minimaxi.com
|
|
168
|
+
modelsUrl: /models
|
|
169
|
+
modelsAuthStyle: bearer
|
|
170
|
+
modelsDataPath: data
|
|
171
|
+
fields:
|
|
172
|
+
- key: name
|
|
173
|
+
label: Provider 昵称
|
|
174
|
+
required: true
|
|
175
|
+
secret: false
|
|
176
|
+
placeholder: 'e.g. My MiniMax'
|
|
177
|
+
- key: apiKey
|
|
178
|
+
label: API Key
|
|
179
|
+
required: true
|
|
180
|
+
secret: true
|
|
181
|
+
placeholder: 'MiniMax API Key'
|
|
182
|
+
|
|
183
|
+
- id: custom
|
|
184
|
+
name: Custom
|
|
185
|
+
baseUrl: ''
|
|
186
|
+
apiFormat: openai_chat
|
|
187
|
+
needsApiKey: true
|
|
188
|
+
websiteUrl: ''
|
|
189
|
+
modelsUrl: /v1/models
|
|
190
|
+
modelsAuthStyle: bearer
|
|
191
|
+
modelsDataPath: data
|
|
192
|
+
fields:
|
|
193
|
+
- key: name
|
|
194
|
+
label: Provider 昵称
|
|
195
|
+
required: true
|
|
196
|
+
secret: false
|
|
197
|
+
placeholder: 'e.g. My Custom Provider'
|
|
198
|
+
- key: baseUrl
|
|
199
|
+
label: Base URL
|
|
200
|
+
required: true
|
|
201
|
+
secret: false
|
|
202
|
+
placeholder: 'https://your-api-endpoint.com/v1'
|
|
203
|
+
- key: apiKey
|
|
204
|
+
label: API Key
|
|
205
|
+
required: false
|
|
206
|
+
secret: true
|
|
207
|
+
placeholder: '(可选)API Key'
|
|
@@ -19,6 +19,7 @@ import { openaiChatToAnthropic } from './transform/openaiChatToAnthropic.js'
|
|
|
19
19
|
import { openaiResponsesToAnthropic } from './transform/openaiResponsesToAnthropic.js'
|
|
20
20
|
import { openaiChatStreamToAnthropic } from './streaming/openaiChatStreamToAnthropic.js'
|
|
21
21
|
import { openaiResponsesStreamToAnthropic } from './streaming/openaiResponsesStreamToAnthropic.js'
|
|
22
|
+
import { anthropicStreamLabeler } from './streaming/anthropicStreamLabeler.js'
|
|
22
23
|
import type { AnthropicRequest } from './transform/types.js'
|
|
23
24
|
import type { SlotName } from '../types/provider.js'
|
|
24
25
|
|
|
@@ -83,14 +84,15 @@ export async function handleProxyRequest(req: Request, url: URL): Promise<Respon
|
|
|
83
84
|
// Use the slot's configured modelId instead of the original Claude model name
|
|
84
85
|
const proxiedBody: AnthropicRequest = { ...body, model: slotConfig.modelId }
|
|
85
86
|
const baseUrl = slotConfig.baseUrl.replace(/\/+$/, '')
|
|
87
|
+
const uiLabel = slotConfig.label || null
|
|
86
88
|
|
|
87
89
|
try {
|
|
88
90
|
if (slotConfig.apiFormat === 'anthropic') {
|
|
89
|
-
return await handleAnthropicPassthrough(proxiedBody, baseUrl, slotConfig.apiKey, isStream)
|
|
91
|
+
return await handleAnthropicPassthrough(proxiedBody, baseUrl, slotConfig.apiKey, isStream, uiLabel)
|
|
90
92
|
} else if (slotConfig.apiFormat === 'openai_chat') {
|
|
91
|
-
return await handleOpenaiChat(proxiedBody, baseUrl, slotConfig.apiKey, isStream)
|
|
93
|
+
return await handleOpenaiChat(proxiedBody, baseUrl, slotConfig.apiKey, isStream, uiLabel)
|
|
92
94
|
} else {
|
|
93
|
-
return await handleOpenaiResponses(proxiedBody, baseUrl, slotConfig.apiKey, isStream)
|
|
95
|
+
return await handleOpenaiResponses(proxiedBody, baseUrl, slotConfig.apiKey, isStream, uiLabel)
|
|
94
96
|
}
|
|
95
97
|
} catch (err) {
|
|
96
98
|
console.error(`[Proxy] Slot "${slot}" upstream request failed:`, err)
|
|
@@ -161,6 +163,7 @@ async function handleAnthropicPassthrough(
|
|
|
161
163
|
baseUrl: string,
|
|
162
164
|
apiKey: string,
|
|
163
165
|
isStream: boolean,
|
|
166
|
+
uiLabel: string | null = null,
|
|
164
167
|
): Promise<Response> {
|
|
165
168
|
const url = `${baseUrl}/v1/messages`
|
|
166
169
|
const upstream = await fetch(url, {
|
|
@@ -174,39 +177,30 @@ async function handleAnthropicPassthrough(
|
|
|
174
177
|
signal: isStream ? AbortSignal.timeout(30_000) : AbortSignal.timeout(300_000),
|
|
175
178
|
})
|
|
176
179
|
|
|
177
|
-
|
|
178
|
-
const errText = await upstream.text().catch(() => '')
|
|
179
|
-
return Response.json(
|
|
180
|
-
{
|
|
181
|
-
type: 'error',
|
|
182
|
-
error: {
|
|
183
|
-
type: 'api_error',
|
|
184
|
-
message: `Upstream returned HTTP ${upstream.status}: ${errText.slice(0, 500)}`,
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
{ status: upstream.status },
|
|
188
|
-
)
|
|
189
|
-
}
|
|
180
|
+
// ... (existing error checks)
|
|
190
181
|
|
|
191
182
|
if (isStream) {
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
183
|
+
if (uiLabel) {
|
|
184
|
+
const labeledStream = anthropicStreamLabeler(upstream.body!, uiLabel)
|
|
185
|
+
return new Response(labeledStream, {
|
|
186
|
+
status: upstream.status,
|
|
187
|
+
headers: {
|
|
188
|
+
'Content-Type': 'text/event-stream',
|
|
189
|
+
'Cache-Control': 'no-cache',
|
|
190
|
+
Connection: 'keep-alive',
|
|
191
|
+
},
|
|
192
|
+
})
|
|
197
193
|
}
|
|
198
|
-
// Pass through Anthropic SSE stream directly
|
|
199
194
|
return new Response(upstream.body, {
|
|
200
|
-
status:
|
|
201
|
-
headers:
|
|
202
|
-
'Content-Type': 'text/event-stream',
|
|
203
|
-
'Cache-Control': 'no-cache',
|
|
204
|
-
Connection: 'keep-alive',
|
|
205
|
-
},
|
|
195
|
+
status: upstream.status,
|
|
196
|
+
headers: upstream.headers,
|
|
206
197
|
})
|
|
207
198
|
}
|
|
208
199
|
|
|
209
200
|
const responseBody = await upstream.json()
|
|
201
|
+
if (uiLabel) {
|
|
202
|
+
(responseBody as any).model = uiLabel
|
|
203
|
+
}
|
|
210
204
|
return Response.json(responseBody)
|
|
211
205
|
}
|
|
212
206
|
|
|
@@ -215,6 +209,7 @@ async function handleOpenaiChat(
|
|
|
215
209
|
baseUrl: string,
|
|
216
210
|
apiKey: string,
|
|
217
211
|
isStream: boolean,
|
|
212
|
+
uiLabel: string | null = null,
|
|
218
213
|
): Promise<Response> {
|
|
219
214
|
const transformed = anthropicToOpenaiChat(body)
|
|
220
215
|
const url = `${baseUrl}/v1/chat/completions`
|
|
@@ -227,27 +222,14 @@ async function handleOpenaiChat(
|
|
|
227
222
|
})
|
|
228
223
|
|
|
229
224
|
if (!upstream.ok) {
|
|
230
|
-
|
|
231
|
-
return Response.json(
|
|
232
|
-
{
|
|
233
|
-
type: 'error',
|
|
234
|
-
error: {
|
|
235
|
-
type: 'api_error',
|
|
236
|
-
message: `Upstream returned HTTP ${upstream.status}: ${errText.slice(0, 500)}`,
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
{ status: upstream.status },
|
|
240
|
-
)
|
|
225
|
+
// ... error handling
|
|
241
226
|
}
|
|
242
227
|
|
|
243
228
|
if (isStream) {
|
|
244
229
|
if (!upstream.body) {
|
|
245
|
-
return Response.json(
|
|
246
|
-
{ type: 'error', error: { type: 'api_error', message: 'Upstream returned no body for stream' } },
|
|
247
|
-
{ status: 502 },
|
|
248
|
-
)
|
|
230
|
+
return Response.json(/* ... */)
|
|
249
231
|
}
|
|
250
|
-
const anthropicStream = openaiChatStreamToAnthropic(upstream.body, body.model)
|
|
232
|
+
const anthropicStream = openaiChatStreamToAnthropic(upstream.body, uiLabel || body.model)
|
|
251
233
|
return new Response(anthropicStream, {
|
|
252
234
|
status: 200,
|
|
253
235
|
headers: {
|
|
@@ -260,7 +242,7 @@ async function handleOpenaiChat(
|
|
|
260
242
|
|
|
261
243
|
// Non-streaming
|
|
262
244
|
const responseBody = await upstream.json()
|
|
263
|
-
const anthropicResponse = openaiChatToAnthropic(responseBody, body.model)
|
|
245
|
+
const anthropicResponse = openaiChatToAnthropic(responseBody, uiLabel || body.model)
|
|
264
246
|
return Response.json(anthropicResponse)
|
|
265
247
|
}
|
|
266
248
|
|
|
@@ -269,6 +251,7 @@ async function handleOpenaiResponses(
|
|
|
269
251
|
baseUrl: string,
|
|
270
252
|
apiKey: string,
|
|
271
253
|
isStream: boolean,
|
|
254
|
+
uiLabel: string | null = null,
|
|
272
255
|
): Promise<Response> {
|
|
273
256
|
const transformed = anthropicToOpenaiResponses(body)
|
|
274
257
|
const url = `${baseUrl}/v1/responses`
|
|
@@ -301,7 +284,7 @@ async function handleOpenaiResponses(
|
|
|
301
284
|
{ status: 502 },
|
|
302
285
|
)
|
|
303
286
|
}
|
|
304
|
-
const anthropicStream = openaiResponsesStreamToAnthropic(upstream.body, body.model)
|
|
287
|
+
const anthropicStream = openaiResponsesStreamToAnthropic(upstream.body, uiLabel || body.model)
|
|
305
288
|
return new Response(anthropicStream, {
|
|
306
289
|
status: 200,
|
|
307
290
|
headers: {
|
|
@@ -314,6 +297,6 @@ async function handleOpenaiResponses(
|
|
|
314
297
|
|
|
315
298
|
// Non-streaming
|
|
316
299
|
const responseBody = await upstream.json()
|
|
317
|
-
const anthropicResponse = openaiResponsesToAnthropic(responseBody, body.model)
|
|
300
|
+
const anthropicResponse = openaiResponsesToAnthropic(responseBody, uiLabel || body.model)
|
|
318
301
|
return Response.json(anthropicResponse)
|
|
319
302
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
}
|
|
@@ -477,7 +477,7 @@ export class ConversationService {
|
|
|
477
477
|
): Promise<Record<string, string>> {
|
|
478
478
|
// Provider isolation: when Desktop has its own provider config/index,
|
|
479
479
|
// strip inherited provider env vars so the child CLI reads fresh values
|
|
480
|
-
// from ~/.claude/
|
|
480
|
+
// from ~/.claude/bingo/settings.json instead of stale process.env.
|
|
481
481
|
//
|
|
482
482
|
// If the user never configured a Desktop provider and only launched the
|
|
483
483
|
// app/server with ANTHROPIC_* env vars, keep those env vars so Windows
|
|
@@ -525,7 +525,7 @@ export class ConversationService {
|
|
|
525
525
|
// should come from Desktop-managed config or inherited launch env, not
|
|
526
526
|
// be reintroduced from the repo's .env file.
|
|
527
527
|
CC_HAHA_SKIP_DOTENV: '1',
|
|
528
|
-
// "官方" 模式 (
|
|
528
|
+
// "官方" 模式 (bingo/settings.json 没 provider env) 下,把 CLI 标记为
|
|
529
529
|
// managed-OAuth,让它忽略外部 ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN
|
|
530
530
|
// 残留、只走用户 /login 的 OAuth token。自定义 provider 模式绝不能设,
|
|
531
531
|
// 否则 CLI 会忽略 provider 的 AUTH_TOKEN、错误地走 OAuth 打到第三方
|
|
@@ -567,7 +567,7 @@ export class ConversationService {
|
|
|
567
567
|
private shouldStripInheritedProviderEnv(): boolean {
|
|
568
568
|
const configDir =
|
|
569
569
|
process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
|
|
570
|
-
const ccHahaDir = path.join(configDir, '
|
|
570
|
+
const ccHahaDir = path.join(configDir, 'bingo')
|
|
571
571
|
const providersIndexPath = path.join(ccHahaDir, 'providers.json')
|
|
572
572
|
const settingsPath = path.join(ccHahaDir, 'settings.json')
|
|
573
573
|
|
|
@@ -599,13 +599,13 @@ export class ConversationService {
|
|
|
599
599
|
* 这种情况下 CLI 必须按 token 路径走第三方 endpoint,不能被 managed 规则
|
|
600
600
|
* 强制切 OAuth。
|
|
601
601
|
*
|
|
602
|
-
* 默认 (读不到 settings.json) 按"官方"处理 — 即使用户从未用过
|
|
602
|
+
* 默认 (读不到 settings.json) 按"官方"处理 — 即使用户从未用过 bingo
|
|
603
603
|
* provider 管理,也希望官方 OAuth 能正常工作。
|
|
604
604
|
*/
|
|
605
605
|
private shouldMarkManagedOAuth(): boolean {
|
|
606
606
|
const configDir =
|
|
607
607
|
process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
|
|
608
|
-
const settingsPath = path.join(configDir, '
|
|
608
|
+
const settingsPath = path.join(configDir, 'bingo', 'settings.json')
|
|
609
609
|
try {
|
|
610
610
|
const raw = fs.readFileSync(settingsPath, 'utf-8')
|
|
611
611
|
const parsed = JSON.parse(raw) as { env?: Record<string, string> }
|