bingocode 1.1.200-beta.16 → 1.1.200-beta.18
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/server/api/providers.ts +1 -6
- package/src/server/config/providers.yaml +229 -208
- package/src/server/proxy/handler.ts +16 -3
- package/src/server/services/providerService.ts +14 -5
- package/src/tools/AgentTool/agentToolUtils.ts +2 -2
- package/src/utils/tokens.ts +23 -0
package/package.json
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from '../types/provider.js'
|
|
25
25
|
import type { SlotName } from '../types/provider.js'
|
|
26
26
|
import { ApiError, errorResponse } from '../middleware/errorHandler.js'
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
|
|
29
29
|
const providerService = new ProviderService()
|
|
30
30
|
providerService.init().catch((err) =>
|
|
@@ -105,11 +105,6 @@ export async function handleProvidersApi(
|
|
|
105
105
|
{ status: 403 },
|
|
106
106
|
)
|
|
107
107
|
}
|
|
108
|
-
// Also enforce Bearer-token auth (mirrors the global /api/ guard,
|
|
109
|
-
// but active even when the server is bound to localhost)
|
|
110
|
-
const authError = requireAuth(req)
|
|
111
|
-
if (authError) return authError
|
|
112
|
-
|
|
113
108
|
if (req.method === 'GET') {
|
|
114
109
|
const status = await providerService.getVscodeStatus()
|
|
115
110
|
return Response.json(status)
|
|
@@ -1,208 +1,229 @@
|
|
|
1
|
-
version: 2
|
|
2
|
-
|
|
3
|
-
# Provider Preset Configuration
|
|
4
|
-
# fields array declares fields to be filled when adding
|
|
5
|
-
# key: 'name' | 'apiKey' | 'baseUrl' mapped directly to top-level fields, others saved in extra.<key>
|
|
6
|
-
# secret: true uses mask display in frontend
|
|
7
|
-
#
|
|
8
|
-
# modelsUrl: model list path relative to baseUrl, empty string means dynamic pulling not supported
|
|
9
|
-
# modelsAuthStyle: bearer → Authorization: Bearer <apiKey>
|
|
10
|
-
# x-api-key → x-api-key: <apiKey> + anthropic-version header
|
|
11
|
-
# modelsDataPath: record field name for models array in JSON response (almost always '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 Nickname
|
|
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 Nickname
|
|
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 Nickname
|
|
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 Nickname
|
|
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/anthropic
|
|
102
|
-
apiFormat: anthropic
|
|
103
|
-
needsApiKey: true
|
|
104
|
-
websiteUrl: https://platform.deepseek.com
|
|
105
|
-
# Models list follows OpenAI endpoint (no /v1/models under /anthropic)
|
|
106
|
-
modelsUrl: https://api.deepseek.com/v1/models
|
|
107
|
-
modelsAuthStyle: bearer
|
|
108
|
-
modelsDataPath: data
|
|
109
|
-
fields:
|
|
110
|
-
- key: name
|
|
111
|
-
label: Provider Nickname
|
|
112
|
-
required: true
|
|
113
|
-
secret: false
|
|
114
|
-
placeholder: 'e.g. My DeepSeek'
|
|
115
|
-
- key: apiKey
|
|
116
|
-
label: API Key
|
|
117
|
-
required: true
|
|
118
|
-
secret: true
|
|
119
|
-
placeholder: 'sk-...'
|
|
120
|
-
|
|
121
|
-
- id:
|
|
122
|
-
name:
|
|
123
|
-
baseUrl: https://
|
|
124
|
-
apiFormat: openai_chat
|
|
125
|
-
needsApiKey: true
|
|
126
|
-
websiteUrl: https://
|
|
127
|
-
modelsUrl: /
|
|
128
|
-
modelsAuthStyle: bearer
|
|
129
|
-
modelsDataPath: data
|
|
130
|
-
fields:
|
|
131
|
-
- key: name
|
|
132
|
-
label: Provider Nickname
|
|
133
|
-
required: true
|
|
134
|
-
secret: false
|
|
135
|
-
placeholder: 'e.g. My
|
|
136
|
-
- key: apiKey
|
|
137
|
-
label: API Key
|
|
138
|
-
required: true
|
|
139
|
-
secret: true
|
|
140
|
-
placeholder: '
|
|
141
|
-
|
|
142
|
-
- id:
|
|
143
|
-
name:
|
|
144
|
-
baseUrl: https://
|
|
145
|
-
apiFormat: openai_chat
|
|
146
|
-
needsApiKey: true
|
|
147
|
-
websiteUrl: https://
|
|
148
|
-
modelsUrl: /models
|
|
149
|
-
modelsAuthStyle: bearer
|
|
150
|
-
modelsDataPath: data
|
|
151
|
-
fields:
|
|
152
|
-
- key: name
|
|
153
|
-
label: Provider Nickname
|
|
154
|
-
required: true
|
|
155
|
-
secret: false
|
|
156
|
-
placeholder: 'e.g. My
|
|
157
|
-
- key: apiKey
|
|
158
|
-
label: API Key
|
|
159
|
-
required: true
|
|
160
|
-
secret: true
|
|
161
|
-
placeholder: '
|
|
162
|
-
|
|
163
|
-
- id:
|
|
164
|
-
name:
|
|
165
|
-
baseUrl: https://api.
|
|
166
|
-
apiFormat: openai_chat
|
|
167
|
-
needsApiKey: true
|
|
168
|
-
websiteUrl: https://platform.
|
|
169
|
-
modelsUrl: /models
|
|
170
|
-
modelsAuthStyle: bearer
|
|
171
|
-
modelsDataPath: data
|
|
172
|
-
fields:
|
|
173
|
-
- key: name
|
|
174
|
-
label: Provider Nickname
|
|
175
|
-
required: true
|
|
176
|
-
secret: false
|
|
177
|
-
placeholder: 'e.g. My
|
|
178
|
-
- key: apiKey
|
|
179
|
-
label: API Key
|
|
180
|
-
required: true
|
|
181
|
-
secret: true
|
|
182
|
-
placeholder: '
|
|
183
|
-
|
|
184
|
-
- id:
|
|
185
|
-
name:
|
|
186
|
-
baseUrl:
|
|
187
|
-
apiFormat: openai_chat
|
|
188
|
-
needsApiKey: true
|
|
189
|
-
websiteUrl:
|
|
190
|
-
modelsUrl: /
|
|
191
|
-
modelsAuthStyle: bearer
|
|
192
|
-
modelsDataPath: data
|
|
193
|
-
fields:
|
|
194
|
-
- key: name
|
|
195
|
-
label: Provider Nickname
|
|
196
|
-
required: true
|
|
197
|
-
secret: false
|
|
198
|
-
placeholder: 'e.g. My
|
|
199
|
-
- key:
|
|
200
|
-
label:
|
|
201
|
-
required: true
|
|
202
|
-
secret:
|
|
203
|
-
placeholder: '
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
1
|
+
version: 2
|
|
2
|
+
|
|
3
|
+
# Provider Preset Configuration
|
|
4
|
+
# fields array declares fields to be filled when adding
|
|
5
|
+
# key: 'name' | 'apiKey' | 'baseUrl' mapped directly to top-level fields, others saved in extra.<key>
|
|
6
|
+
# secret: true uses mask display in frontend
|
|
7
|
+
#
|
|
8
|
+
# modelsUrl: model list path relative to baseUrl, empty string means dynamic pulling not supported
|
|
9
|
+
# modelsAuthStyle: bearer → Authorization: Bearer <apiKey>
|
|
10
|
+
# x-api-key → x-api-key: <apiKey> + anthropic-version header
|
|
11
|
+
# modelsDataPath: record field name for models array in JSON response (almost always '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 Nickname
|
|
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 Nickname
|
|
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 Nickname
|
|
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 Nickname
|
|
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/anthropic
|
|
102
|
+
apiFormat: anthropic
|
|
103
|
+
needsApiKey: true
|
|
104
|
+
websiteUrl: https://platform.deepseek.com
|
|
105
|
+
# Models list follows OpenAI endpoint (no /v1/models under /anthropic)
|
|
106
|
+
modelsUrl: https://api.deepseek.com/v1/models
|
|
107
|
+
modelsAuthStyle: bearer
|
|
108
|
+
modelsDataPath: data
|
|
109
|
+
fields:
|
|
110
|
+
- key: name
|
|
111
|
+
label: Provider Nickname
|
|
112
|
+
required: true
|
|
113
|
+
secret: false
|
|
114
|
+
placeholder: 'e.g. My DeepSeek'
|
|
115
|
+
- key: apiKey
|
|
116
|
+
label: API Key
|
|
117
|
+
required: true
|
|
118
|
+
secret: true
|
|
119
|
+
placeholder: 'sk-...'
|
|
120
|
+
|
|
121
|
+
- id: nvidia
|
|
122
|
+
name: NVIDIA API
|
|
123
|
+
baseUrl: https://integrate.api.nvidia.com/v1
|
|
124
|
+
apiFormat: openai_chat
|
|
125
|
+
needsApiKey: true
|
|
126
|
+
websiteUrl: https://build.nvidia.com/explore/discover
|
|
127
|
+
modelsUrl: /models
|
|
128
|
+
modelsAuthStyle: bearer
|
|
129
|
+
modelsDataPath: data
|
|
130
|
+
fields:
|
|
131
|
+
- key: name
|
|
132
|
+
label: Provider Nickname
|
|
133
|
+
required: true
|
|
134
|
+
secret: false
|
|
135
|
+
placeholder: 'e.g. My NVIDIA'
|
|
136
|
+
- key: apiKey
|
|
137
|
+
label: API Key
|
|
138
|
+
required: true
|
|
139
|
+
secret: true
|
|
140
|
+
placeholder: 'nvapi-...'
|
|
141
|
+
|
|
142
|
+
- id: zhipuglm
|
|
143
|
+
name: Zhipu GLM
|
|
144
|
+
baseUrl: https://open.bigmodel.cn/api/paas/v4
|
|
145
|
+
apiFormat: openai_chat
|
|
146
|
+
needsApiKey: true
|
|
147
|
+
websiteUrl: https://open.bigmodel.cn
|
|
148
|
+
modelsUrl: /v4/models
|
|
149
|
+
modelsAuthStyle: bearer
|
|
150
|
+
modelsDataPath: data
|
|
151
|
+
fields:
|
|
152
|
+
- key: name
|
|
153
|
+
label: Provider Nickname
|
|
154
|
+
required: true
|
|
155
|
+
secret: false
|
|
156
|
+
placeholder: 'e.g. My GLM'
|
|
157
|
+
- key: apiKey
|
|
158
|
+
label: API Key
|
|
159
|
+
required: true
|
|
160
|
+
secret: true
|
|
161
|
+
placeholder: 'Zhipu API Key (glm-5.1)'
|
|
162
|
+
|
|
163
|
+
- id: kimi
|
|
164
|
+
name: Kimi
|
|
165
|
+
baseUrl: https://api.moonshot.ai/v1
|
|
166
|
+
apiFormat: openai_chat
|
|
167
|
+
needsApiKey: true
|
|
168
|
+
websiteUrl: https://platform.moonshot.cn
|
|
169
|
+
modelsUrl: /models
|
|
170
|
+
modelsAuthStyle: bearer
|
|
171
|
+
modelsDataPath: data
|
|
172
|
+
fields:
|
|
173
|
+
- key: name
|
|
174
|
+
label: Provider Nickname
|
|
175
|
+
required: true
|
|
176
|
+
secret: false
|
|
177
|
+
placeholder: 'e.g. My Kimi'
|
|
178
|
+
- key: apiKey
|
|
179
|
+
label: API Key
|
|
180
|
+
required: true
|
|
181
|
+
secret: true
|
|
182
|
+
placeholder: 'Moonshot API Key (kimi-k2.6)'
|
|
183
|
+
|
|
184
|
+
- id: minimax
|
|
185
|
+
name: MiniMax
|
|
186
|
+
baseUrl: https://api.minimax.io/v1
|
|
187
|
+
apiFormat: openai_chat
|
|
188
|
+
needsApiKey: true
|
|
189
|
+
websiteUrl: https://platform.minimaxi.com
|
|
190
|
+
modelsUrl: /models
|
|
191
|
+
modelsAuthStyle: bearer
|
|
192
|
+
modelsDataPath: data
|
|
193
|
+
fields:
|
|
194
|
+
- key: name
|
|
195
|
+
label: Provider Nickname
|
|
196
|
+
required: true
|
|
197
|
+
secret: false
|
|
198
|
+
placeholder: 'e.g. My MiniMax'
|
|
199
|
+
- key: apiKey
|
|
200
|
+
label: API Key
|
|
201
|
+
required: true
|
|
202
|
+
secret: true
|
|
203
|
+
placeholder: 'MiniMax API Key (MiniMax-M2.7)'
|
|
204
|
+
|
|
205
|
+
- id: custom
|
|
206
|
+
name: Custom
|
|
207
|
+
baseUrl: ''
|
|
208
|
+
apiFormat: openai_chat
|
|
209
|
+
needsApiKey: true
|
|
210
|
+
websiteUrl: ''
|
|
211
|
+
modelsUrl: /v1/models
|
|
212
|
+
modelsAuthStyle: bearer
|
|
213
|
+
modelsDataPath: data
|
|
214
|
+
fields:
|
|
215
|
+
- key: name
|
|
216
|
+
label: Provider Nickname
|
|
217
|
+
required: true
|
|
218
|
+
secret: false
|
|
219
|
+
placeholder: 'e.g. My Custom Provider'
|
|
220
|
+
- key: baseUrl
|
|
221
|
+
label: Base URL
|
|
222
|
+
required: true
|
|
223
|
+
secret: false
|
|
224
|
+
placeholder: 'https://your-api-endpoint.com/v1'
|
|
225
|
+
- key: apiKey
|
|
226
|
+
label: API Key
|
|
227
|
+
required: false
|
|
228
|
+
secret: true
|
|
229
|
+
placeholder: '(Optional) API Key'
|
|
@@ -35,6 +35,19 @@ async function logToFile(message: string) {
|
|
|
35
35
|
// Disabled log output for production
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Normalize a base URL that may already contain a /v1 suffix,
|
|
40
|
+
* then append /v1/<path> so that double /v1 segments are avoided.
|
|
41
|
+
*
|
|
42
|
+
* Example:
|
|
43
|
+
* "https://integrate.api.nvidia.com/v1" + "chat/completions"
|
|
44
|
+
* => "https://integrate.api.nvidia.com/v1/chat/completions" (not /v1/v1/...)
|
|
45
|
+
*/
|
|
46
|
+
function v1Url(baseUrl: string, path: string): string {
|
|
47
|
+
const normalized = baseUrl.replace(/\/v1\/?$/, '')
|
|
48
|
+
return `${normalized}/v1/${path.replace(/^\//, '')}`
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
function sendAnthropicError(message: string, _model: string | undefined, status = 502): Response {
|
|
39
52
|
const fullMessage = `[Bingo Proxy] ${message}`
|
|
40
53
|
void logToFile(`ERROR: ${fullMessage} (status: ${status})`)
|
|
@@ -176,7 +189,7 @@ async function handleAnthropicPassthrough(
|
|
|
176
189
|
uiLabel: string | null = null,
|
|
177
190
|
betaHeader: string | null = null,
|
|
178
191
|
): Promise<Response> {
|
|
179
|
-
const url =
|
|
192
|
+
const url = v1Url(baseUrl, 'messages')
|
|
180
193
|
const upstreamHeaders: Record<string, string> = {
|
|
181
194
|
'Content-Type': 'application/json',
|
|
182
195
|
'x-api-key': apiKey,
|
|
@@ -226,7 +239,7 @@ async function handleOpenaiChat(
|
|
|
226
239
|
uiLabel: string | null = null,
|
|
227
240
|
): Promise<Response> {
|
|
228
241
|
const transformed = anthropicToOpenaiChat(body)
|
|
229
|
-
const url =
|
|
242
|
+
const url = v1Url(baseUrl, 'chat/completions')
|
|
230
243
|
|
|
231
244
|
const upstream = await fetch(url, {
|
|
232
245
|
method: 'POST',
|
|
@@ -277,7 +290,7 @@ async function handleOpenaiResponses(
|
|
|
277
290
|
uiLabel: string | null = null,
|
|
278
291
|
): Promise<Response> {
|
|
279
292
|
const transformed = anthropicToOpenaiResponses(body)
|
|
280
|
-
const url =
|
|
293
|
+
const url = v1Url(baseUrl, 'responses')
|
|
281
294
|
|
|
282
295
|
const upstream = await fetch(url, {
|
|
283
296
|
method: 'POST',
|
|
@@ -1010,10 +1010,10 @@ export class ProviderService {
|
|
|
1010
1010
|
let transformedBody: unknown
|
|
1011
1011
|
if (format === 'openai_chat') {
|
|
1012
1012
|
transformedBody = anthropicToOpenaiChat(anthropicReq)
|
|
1013
|
-
upstreamUrl =
|
|
1013
|
+
upstreamUrl = v1Url(base, 'chat/completions')
|
|
1014
1014
|
} else {
|
|
1015
1015
|
transformedBody = anthropicToOpenaiResponses(anthropicReq)
|
|
1016
|
-
upstreamUrl =
|
|
1016
|
+
upstreamUrl = v1Url(base, 'responses')
|
|
1017
1017
|
}
|
|
1018
1018
|
|
|
1019
1019
|
// Call upstream with transformed request
|
|
@@ -1060,6 +1060,15 @@ export class ProviderService {
|
|
|
1060
1060
|
|
|
1061
1061
|
// ─── Helpers ───────────────────────────────────────────────
|
|
1062
1062
|
|
|
1063
|
+
/**
|
|
1064
|
+
* Normalize a base URL that may already contain a /v1 suffix,
|
|
1065
|
+
* then append /v1/<path> so that double /v1 segments are avoided.
|
|
1066
|
+
*/
|
|
1067
|
+
function v1Url(base: string, path: string): string {
|
|
1068
|
+
const normalized = base.replace(/\/v1\/?$/, '')
|
|
1069
|
+
return `${normalized}/v1/${path.replace(/^\//, '')}`
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1063
1072
|
function buildDirectTestRequest(
|
|
1064
1073
|
base: string,
|
|
1065
1074
|
apiKey: string,
|
|
@@ -1070,21 +1079,21 @@ function buildDirectTestRequest(
|
|
|
1070
1079
|
|
|
1071
1080
|
if (format === 'openai_chat') {
|
|
1072
1081
|
return {
|
|
1073
|
-
url:
|
|
1082
|
+
url: v1Url(base, 'chat/completions'),
|
|
1074
1083
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
1075
1084
|
body: { model: modelId, max_tokens: 16, messages: [{ role: 'user', content: prompt }] },
|
|
1076
1085
|
}
|
|
1077
1086
|
}
|
|
1078
1087
|
if (format === 'openai_responses') {
|
|
1079
1088
|
return {
|
|
1080
|
-
url:
|
|
1089
|
+
url: v1Url(base, 'responses'),
|
|
1081
1090
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
1082
1091
|
body: { model: modelId, max_output_tokens: 16, input: [{ type: 'message', role: 'user', content: prompt }] },
|
|
1083
1092
|
}
|
|
1084
1093
|
}
|
|
1085
1094
|
// anthropic
|
|
1086
1095
|
return {
|
|
1087
|
-
url:
|
|
1096
|
+
url: v1Url(base, 'messages'),
|
|
1088
1097
|
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },
|
|
1089
1098
|
body: { model: modelId, max_tokens: 16, messages: [{ role: 'user', content: prompt }] },
|
|
1090
1099
|
}
|
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
} from '../../utils/permissions/yoloClassifier.js'
|
|
56
56
|
import { emitTaskProgress as emitTaskProgressEvent } from '../../utils/task/sdkProgress.js'
|
|
57
57
|
import { isInProcessTeammate } from '../../utils/teammateContext.js'
|
|
58
|
-
import {
|
|
58
|
+
import { getTokenCountFromMessages } from '../../utils/tokens.js'
|
|
59
59
|
import { EXIT_PLAN_MODE_V2_TOOL_NAME } from '../ExitPlanModeTool/constants.js'
|
|
60
60
|
import { AGENT_TOOL_NAME, LEGACY_AGENT_TOOL_NAME } from './constants.js'
|
|
61
61
|
import type { AgentDefinition } from './loadAgentsDir.js'
|
|
@@ -316,7 +316,7 @@ export function finalizeAgentTool(
|
|
|
316
316
|
}
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
-
const totalTokens =
|
|
319
|
+
const totalTokens = getTokenCountFromMessages(agentMessages)
|
|
320
320
|
const totalToolUseCount = countToolUses(agentMessages)
|
|
321
321
|
|
|
322
322
|
logEvent('tengu_agent_tool_completed', {
|
package/src/utils/tokens.ts
CHANGED
|
@@ -52,6 +52,29 @@ export function getTokenCountFromUsage(usage: Usage): number {
|
|
|
52
52
|
)
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Calculate total tokens across all agent messages, matching the ProgressTracker
|
|
57
|
+
* accumulation logic in LocalAgentTask. In Claude's API:
|
|
58
|
+
* - input_tokens (and cache tokens) are cumulative per turn — each call already
|
|
59
|
+
* includes all previous context, so we keep only the LATEST value.
|
|
60
|
+
* - output_tokens are per-turn independent, so we SUM them across all turns.
|
|
61
|
+
*/
|
|
62
|
+
export function getTokenCountFromMessages(messages: Message[]): number {
|
|
63
|
+
let latestInputTokens = 0
|
|
64
|
+
let cumulativeOutputTokens = 0
|
|
65
|
+
for (const message of messages) {
|
|
66
|
+
const usage = getTokenUsage(message)
|
|
67
|
+
if (usage) {
|
|
68
|
+
latestInputTokens =
|
|
69
|
+
usage.input_tokens +
|
|
70
|
+
(usage.cache_creation_input_tokens ?? 0) +
|
|
71
|
+
(usage.cache_read_input_tokens ?? 0)
|
|
72
|
+
cumulativeOutputTokens += usage.output_tokens
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return latestInputTokens + cumulativeOutputTokens
|
|
76
|
+
}
|
|
77
|
+
|
|
55
78
|
export function tokenCountFromLastAPIResponse(messages: Message[]): number {
|
|
56
79
|
let i = messages.length - 1
|
|
57
80
|
while (i >= 0) {
|