Skip to content

Commit b7259ef

Browse files
committed
feat: add apiFormat field for third-party provider compatibility
- Add apiFormat field to Provider struct (anthropic/openai) - Update connectivity test to use correct endpoint and auth based on apiFormat - Default to OpenAI format for third-party providers (more common) - Add UI for apiFormat selection in provider form - Add i18n translations for apiFormat labels and hints This fixes connectivity test failures for providers like 88code that use OpenAI-compatible API format instead of native Anthropic format.
1 parent 44b91e9 commit b7259ef

7 files changed

Lines changed: 96 additions & 22 deletions

File tree

frontend/src/components/Main/Index.vue

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,16 @@
625625
<span class="field-hint">{{ t('components.main.form.hints.connectivityTestModel') }}</span>
626626
</div>
627627

628+
<div v-if="modalState.form.connectivityCheck" class="form-field">
629+
<span>{{ t('components.main.form.labels.apiFormat') }}</span>
630+
<select v-model="modalState.form.apiFormat" class="model-select">
631+
<option value="">{{ t('components.main.form.placeholders.apiFormat') }}</option>
632+
<option value="anthropic">Anthropic (x-api-key)</option>
633+
<option value="openai">OpenAI (Bearer)</option>
634+
</select>
635+
<span class="field-hint">{{ t('components.main.form.hints.apiFormat') }}</span>
636+
</div>
637+
628638
<footer class="form-actions">
629639
<BaseButton variant="outline" type="button" @click="closeModal">
630640
{{ t('components.main.form.actions.cancel') }}
@@ -1799,6 +1809,7 @@ type VendorForm = {
17991809
cliConfig?: Record<string, any>
18001810
connectivityCheck?: boolean
18011811
connectivityTestModel?: string
1812+
apiFormat?: string
18021813
}
18031814
18041815
const iconOptions = Object.keys(lobeIcons).sort((a, b) => a.localeCompare(b))
@@ -1817,6 +1828,7 @@ const defaultFormValues = (): VendorForm => ({
18171828
cliConfig: {},
18181829
connectivityCheck: false,
18191830
connectivityTestModel: '',
1831+
apiFormat: '',
18201832
})
18211833
18221834
// Level 描述文本映射(1-10)
@@ -1896,6 +1908,7 @@ const openEditModal = (card: AutomationCard) => {
18961908
cliConfig: card.cliConfig || {},
18971909
connectivityCheck: card.connectivityCheck || false,
18981910
connectivityTestModel: card.connectivityTestModel || '',
1911+
apiFormat: card.apiFormat || '',
18991912
})
19001913
modalState.errors.apiUrl = ''
19011914
modalState.open = true
@@ -1943,6 +1956,7 @@ const submitModal = async () => {
19431956
cliConfig: modalState.form.cliConfig || {},
19441957
connectivityCheck: modalState.form.connectivityCheck || false,
19451958
connectivityTestModel: modalState.form.connectivityTestModel || '',
1959+
apiFormat: modalState.form.apiFormat || '',
19461960
})
19471961
if (prevLevel !== nextLevel) {
19481962
sortProvidersByLevel(list)
@@ -1965,6 +1979,7 @@ const submitModal = async () => {
19651979
cliConfig: modalState.form.cliConfig || {},
19661980
connectivityCheck: modalState.form.connectivityCheck || false,
19671981
connectivityTestModel: modalState.form.connectivityTestModel || '',
1982+
apiFormat: modalState.form.apiFormat || '',
19681983
}
19691984
list.push(newCard)
19701985
sortProvidersByLevel(list)

frontend/src/data/cards.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export type AutomationCard = {
2020
connectivityCheck?: boolean
2121
// 连通性检测模型:用户可选择或自定义测试模型
2222
connectivityTestModel?: string
23+
// API 格式:anthropic 或 openai(默认自动检测)
24+
apiFormat?: string
2325
}
2426

2527
export const automationCardGroups: Record<'claude' | 'codex', AutomationCard[]> = {

frontend/src/locales/en.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,8 @@
193193
"enabled": "Enabled",
194194
"level": "Priority Level",
195195
"connectivityCheck": "Connectivity Check",
196-
"connectivityTestModel": "Test Model"
196+
"connectivityTestModel": "Test Model",
197+
"apiFormat": "API Format"
197198
},
198199
"placeholders": {
199200
"name": "e.g. AICoding.sh",
@@ -202,12 +203,14 @@
202203
"officialSite": "https://vendor.com",
203204
"icon": "e.g. aicoding, kimi",
204205
"connectivityTestModel": "Select or use default",
205-
"customModel": "Or enter custom model"
206+
"customModel": "Or enter custom model",
207+
"apiFormat": "Auto detect"
206208
},
207209
"hints": {
208210
"level": "Lower numbers = higher priority. Level 1 providers are tried first, then Level 2, etc.",
209211
"connectivityCheck": "When enabled, connectivity to this provider will be tested periodically; enabling this may consume a small amount of tokens",
210-
"connectivityTestModel": "Select a model for connectivity testing, or leave empty to use platform default"
212+
"connectivityTestModel": "Select a model for connectivity testing, or leave empty to use platform default",
213+
"apiFormat": "Most third-party providers use OpenAI format, official Anthropic uses Anthropic format"
211214
},
212215
"actions": {
213216
"cancel": "Cancel",

frontend/src/locales/zh.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,8 @@
194194
"enabled": "启用状态",
195195
"level": "优先级分组",
196196
"connectivityCheck": "连通性检测",
197-
"connectivityTestModel": "测试模型"
197+
"connectivityTestModel": "测试模型",
198+
"apiFormat": "API 格式"
198199
},
199200
"placeholders": {
200201
"name": "例如:AICoding.sh",
@@ -203,12 +204,14 @@
203204
"officialSite": "https://vendor.com",
204205
"icon": "例如:aicoding、kimi",
205206
"connectivityTestModel": "选择或使用默认",
206-
"customModel": "或输入自定义模型"
207+
"customModel": "或输入自定义模型",
208+
"apiFormat": "自动检测"
207209
},
208210
"hints": {
209211
"level": "数字越小优先级越高,Level 1 会被优先尝试,失败后依次尝试 Level 2、Level 3 等",
210212
"connectivityCheck": "启用后会定期检测此供应商的连通性,开启该选项可能会消耗少量 tokens",
211-
"connectivityTestModel": "选择用于连通性测试的模型,留空则使用平台默认模型"
213+
"connectivityTestModel": "选择用于连通性测试的模型,留空则使用平台默认模型",
214+
"apiFormat": "大多数第三方供应商使用 OpenAI 格式,官方 Anthropic 使用 Anthropic 格式"
212215
},
213216
"actions": {
214217
"cancel": "取消",

services/connectivitytestservice.go

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,9 @@ func (cts *ConnectivityTestService) TestProvider(ctx context.Context, provider P
112112
return result
113113
}
114114

115-
// 根据平台拼接正确的端点路径(与 providerrelay.go 保持一致)
116-
targetURL := cts.buildTargetURL(provider.APIURL, platform)
115+
// 根据平台和 API 格式拼接正确的端点路径
116+
apiFormat := cts.getEffectiveApiFormat(&provider, platform)
117+
targetURL := cts.buildTargetURLWithFormat(provider.APIURL, platform, apiFormat)
117118

118119
// 创建 HTTP 请求
119120
req, err := http.NewRequestWithContext(ctx, "POST", targetURL, bytes.NewReader(reqBody))
@@ -126,9 +127,8 @@ func (cts *ConnectivityTestService) TestProvider(ctx context.Context, provider P
126127
// 设置 Headers
127128
req.Header.Set("Content-Type", "application/json")
128129
if provider.APIKey != "" {
129-
// Claude/Anthropic 使用 x-api-key,OpenAI 使用 Authorization: Bearer
130-
if strings.Contains(strings.ToLower(provider.APIURL), "anthropic") ||
131-
strings.Contains(strings.ToLower(platform), "claude") {
130+
// 根据 apiFormat 决定认证方式(apiFormat 已在上面定义)
131+
if apiFormat == "anthropic" {
132132
req.Header.Set("x-api-key", provider.APIKey)
133133
req.Header.Set("anthropic-version", "2023-06-01")
134134
} else {
@@ -183,7 +183,7 @@ func (cts *ConnectivityTestService) TestProvider(ctx context.Context, provider P
183183
return result
184184
}
185185

186-
// buildTestRequest 根据平台构建测试请求体
186+
// buildTestRequest 根据平台和 API 格式构建测试请求体
187187
func (cts *ConnectivityTestService) buildTestRequest(platform string, provider *Provider) ([]byte, string) {
188188
var contentField string
189189

@@ -204,11 +204,26 @@ func (cts *ConnectivityTestService) buildTestRequest(platform string, provider *
204204
model = "gpt-3.5-turbo" // 兜底
205205
}
206206

207-
// 确定模型名称
207+
// 获取有效的 API 格式
208+
apiFormat := cts.getEffectiveApiFormat(provider, platform)
209+
208210
switch platformKey {
209211
case "claude":
212+
if apiFormat == "openai" {
213+
// OpenAI 兼容格式
214+
contentField = "choices"
215+
reqBody := map[string]interface{}{
216+
"model": model,
217+
"max_tokens": 1,
218+
"messages": []map[string]string{
219+
{"role": "user", "content": "hi"},
220+
},
221+
}
222+
data, _ := json.Marshal(reqBody)
223+
return data, contentField
224+
}
225+
// Anthropic 原生格式
210226
contentField = "content"
211-
// Claude/Anthropic 格式
212227
reqBody := map[string]interface{}{
213228
"model": model,
214229
"max_tokens": 1,
@@ -221,7 +236,6 @@ func (cts *ConnectivityTestService) buildTestRequest(platform string, provider *
221236

222237
case "codex":
223238
contentField = "choices"
224-
// OpenAI 格式
225239
reqBody := map[string]interface{}{
226240
"model": model,
227241
"max_tokens": 1,
@@ -234,7 +248,6 @@ func (cts *ConnectivityTestService) buildTestRequest(platform string, provider *
234248

235249
case "gemini":
236250
contentField = "candidates"
237-
// Gemini 格式
238251
reqBody := map[string]interface{}{
239252
"model": model,
240253
"contents": []map[string]interface{}{
@@ -249,7 +262,6 @@ func (cts *ConnectivityTestService) buildTestRequest(platform string, provider *
249262
return data, contentField
250263

251264
default:
252-
// 默认使用 OpenAI 格式
253265
contentField = "choices"
254266
reqBody := map[string]interface{}{
255267
"model": model,
@@ -328,21 +340,56 @@ func (cts *ConnectivityTestService) truncateMessage(msg string) string {
328340
return msg
329341
}
330342

331-
// buildTargetURL 根据平台拼接正确的 API 端点路径
332-
// 与 providerrelay.go 中的转发路径保持一致
343+
// getEffectiveApiFormat 获取有效的 API 格式
344+
// 优先使用用户配置,否则根据平台和 URL 自动判断
345+
func (cts *ConnectivityTestService) getEffectiveApiFormat(provider *Provider, platform string) string {
346+
// 用户显式配置优先
347+
if provider.ApiFormat != "" {
348+
return strings.ToLower(provider.ApiFormat)
349+
}
350+
351+
// 官方 Anthropic API 使用 anthropic 格式
352+
if strings.Contains(strings.ToLower(provider.APIURL), "anthropic.com") {
353+
return "anthropic"
354+
}
355+
356+
// 其他第三方供应商默认使用 openai 格式(更通用)
357+
return "openai"
358+
}
359+
360+
// buildTargetURL 根据平台和 API 格式拼接正确的端点路径
333361
func (cts *ConnectivityTestService) buildTargetURL(baseURL, platform string) string {
334362
baseURL = strings.TrimSuffix(baseURL, "/")
335363

336364
switch strings.ToLower(platform) {
337365
case "claude":
366+
// Claude 平台的路径由 apiFormat 决定,这里返回基础路径
367+
// 实际路径在 buildTargetURLWithFormat 中处理
368+
return baseURL + "/v1/messages"
369+
case "codex":
370+
return baseURL + "/responses"
371+
case "gemini":
372+
return baseURL
373+
default:
374+
return baseURL + "/v1/chat/completions"
375+
}
376+
}
377+
378+
// buildTargetURLWithFormat 根据 API 格式构建目标 URL
379+
func (cts *ConnectivityTestService) buildTargetURLWithFormat(baseURL, platform, apiFormat string) string {
380+
baseURL = strings.TrimSuffix(baseURL, "/")
381+
382+
switch strings.ToLower(platform) {
383+
case "claude":
384+
if apiFormat == "openai" {
385+
return baseURL + "/v1/chat/completions"
386+
}
338387
return baseURL + "/v1/messages"
339388
case "codex":
340389
return baseURL + "/responses"
341390
case "gemini":
342-
// Gemini 使用不同的路径格式,暂不支持
343391
return baseURL
344392
default:
345-
// 默认使用 OpenAI 格式
346393
return baseURL + "/v1/chat/completions"
347394
}
348395
}

services/providerservice.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ type Provider struct {
3838
// 连通性检测模型 - 用户可选择或自定义测试模型
3939
ConnectivityTestModel string `json:"connectivityTestModel,omitempty"`
4040

41+
// API 格式 - anthropic 或 openai(默认根据平台自动选择)
42+
// 某些第三方供应商使用 OpenAI 兼容 API 而非原生 Anthropic API
43+
ApiFormat string `json:"apiFormat,omitempty"`
44+
4145
// 内部字段:配置验证错误(不持久化)
4246
configErrors []string `json:"-"`
4347
}

version_service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package main
22

3-
const AppVersion = "v1.4.9"
3+
const AppVersion = "v1.5.0"
44

55
type VersionService struct {
66
version string

0 commit comments

Comments
 (0)