Files
sub2api/backend/internal/service/provider_profile.go
T
SavitarCandClaude Opus 5.5 874871a25a feat(platform): 接入 Cline 平台
Cline API(https://api.cline.bot/api/v1)是 OpenAI 兼容的多模型聚合端点,
只提供 Chat Completions(Cline 自己的客户端同样只走该端点),模型名带厂商
前缀。同一个 API Key 下有两个互不影响的额度来源,由模型名决定扣哪一边:
cline-pass/* 走 ClinePass 订阅(5 小时 / 7 天 / 30 天滚动上限),其余付费
模型扣账户积分;cline-free/* 免费模型只有逐模型的每日额度。

转发:
- 平台清单与 provider profile 新增 cline:按入站协议分流,只登记 Chat
  Completions 端点,Responses / Anthropic 入站由现有转换链转为 Chat
  Completions,无需新增转发代码。
- 只有一个接入模式,不需要选择账号类型。
- 连接测试未指定模型时使用 profile 默认模型(固定 Chat Completions 的账号
  此前落到不带厂商前缀的通用默认模型);订阅了 ClinePass 的账号改用订阅
  模型,不消耗积分,ClinePass 冷却中时仍用按量模型。

用量与余额:API Key 调 /api/v1/users/me/plan/usage-limits(三档窗口的已用
百分比与重置时间,未订阅时 404)与 /api/v1/users/me、/users/{id}/balance
(余额单位为百万分之一积分,1 积分 = 1 美元),兼容 {success, data} 信封,
只查官方主机。复用额度探测与周期检测:账号列表显示 ClinePass 窗口与积分
余额;窗口用满时 ClinePass 模型冷却到重置时间,未订阅时不调度 ClinePass
模型,积分低于阈值时冷却付费模型;数据显示恢复后自动解除,但只解除探测能
证明已恢复的原因(窗口用满、未订阅、积分不足),组织账户、所需订阅档位、推理
额度与花费上限的冷却到期后由请求重新确认。周期检测中走账号接口探测的平台
改为登记表(平台、支持判定、检测函数一一对应),按账号所属平台分派。

上游错误(依据 docs.cline.bot/api/errors 与 Cline 客户端的错误分类,按文案
识别):积分不足(含其他 402)、组织花费上限、推理额度上限、ClinePass 超限、
未订阅 ClinePass / 组织账户、免费模型每日额度。两边的超限只冷却请求模型所属
的一边(模型级冷却键 cline:pass / cline:credits,调度按请求模型检查),不停
整个账号;免费模型只限该模型,冷却到文案中的 "try again in …"。ClinePass
超限冷却到快照中对应窗口的重置时间,没有快照时 5 小时窗口 1 小时、7 天
6 小时、30 天 24 小时。普通频率限制走默认 429 逻辑。402 的冷却写入失败时
也不落入默认分支永久置 error(探测只覆盖激活账号,置 error 后无法自动恢复)。
余额类冷却时长抽为 cnBalanceCheckCooldown 共用。

上游计费探测的官方域名加入 cline.bot。

前端:官方标志(机器人头像)、violet 配色(品牌紫)、复用额度与余额单元格
(同样只对官方主机上的 API Key 账号显示);接入模式展示名抽为共用函数,编辑
弹窗不再显示原始模式值。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:11:06 +08:00

268 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import "strings"
// ProviderEndpoints 是多协议 API Key 供应商在某个接入模式下的官方默认端点与
// 内置分流规则。账号凭证里的 base_url / api_base_urls / protocol_rules 仍优先于这里。
type ProviderEndpoints struct {
// BaseURLs 以上游协议(chat_completions / responses / anthropic)为键,
// 值为该协议的官方默认基址。缺少某协议的条目即表示该模式不提供该原生端点。
// Anthropic 基址的上游路径为 {base}/v1/messages。
BaseURLs map[string]string
// ProtocolRules 是按模型分流的内置默认表(首条命中生效);为空表示该模式
// 不按模型分流,由账号 api_protocol 决定上游协议。
ProtocolRules []ProtocolRule
}
// ProviderRouting 决定多协议供应商账号如何选择上游协议。
type ProviderRouting string
const (
// ProviderRoutingByInbound 适用于单一厂商:模型在各原生端点都可用,adaptive 账号
// 按入站协议选同协议端点,供应商不提供该协议时回落 Chat Completions。
ProviderRoutingByInbound ProviderRouting = "by_inbound"
// ProviderRoutingByModel 适用于多模型聚合平台:每个模型只在某一协议端点提供,
// 按 protocol_rules 以模型名选协议,与入站协议无关。
ProviderRoutingByModel ProviderRouting = "by_model"
)
// ProviderProfile 描述一个多协议 API Key 供应商(国产厂商或多模型聚合平台)的
// 静态数据:各接入模式的默认端点、内置分流规则与端点路径约定。
// 额度解析、会话头等行为差异不在这里,仍由各自的代码路径处理。
type ProviderProfile struct {
Platform string
// DefaultMode 在 credentials.account_mode 缺失或不在 Modes 中时使用。
DefaultMode string
Modes map[string]ProviderEndpoints
// Routing 为空时按 ProviderRoutingByInbound 处理。
Routing ProviderRouting
// ResponsesPath 覆盖 Responses 端点路径(按 buildOpenAIEndpointURL 的版本感知
// 规则拼接),空值为 /v1/responses。对自定义 base_url 的账号同样生效。
ResponsesPath string
// DefaultTestModel 是管理端连接测试未指定模型时使用的模型;空值时按各协议的
// 通用默认模型。
DefaultTestModel string
// ModelCatalog 表示上游 {Chat Completions 基址}/models 为每个模型给出
// supported_endpoints;按模型分流时据此判断模型支持哪些协议(见 model_protocol_catalog.go)。
ModelCatalog bool
}
// providerDefaultTestModel 返回账号连接测试未指定模型时的默认模型;非多协议供应商或
// 未设置时为空。
func (a *Account) providerDefaultTestModel() string {
profile := a.providerProfile()
if profile == nil {
return ""
}
// Cline 订阅了 ClinePass 的账号用订阅模型测试,不消耗积分;ClinePass 钱包冷却中
// (窗口用满等)时改用按量模型,否则测试必然失败,而积分模型照常可用。
if a.IsCline() && a.clinePassSubscribed() && !a.isRateLimitActiveForKey(clinePassRateLimitKey) {
return DefaultClinePassTestModel
}
return profile.DefaultTestModel
}
// Endpoints 返回指定接入模式的端点;未知模式回落 DefaultMode。
func (p *ProviderProfile) Endpoints(mode string) ProviderEndpoints {
if p == nil {
return ProviderEndpoints{}
}
if endpoints, ok := p.Modes[strings.TrimSpace(mode)]; ok {
return endpoints
}
return p.Modes[p.DefaultMode]
}
// DefaultBaseURL 返回指定接入模式与上游协议的官方默认基址;该模式不提供此协议时为空串。
func (p *ProviderProfile) DefaultBaseURL(mode, protocol string) string {
return p.Endpoints(mode).BaseURLs[protocol]
}
// SupportsProtocol 报告指定接入模式是否提供该协议的原生端点。
func (p *ProviderProfile) SupportsProtocol(mode, protocol string) bool {
return p.DefaultBaseURL(mode, protocol) != ""
}
// providerProfiles 是多协议 API Key 供应商的单一数据来源。新增同类供应商时在此
// 登记一条即可获得默认端点、原生 Responses 能力判定(有 responses 条目即支持)、
// 上游协议分流方式、Responses 路径约定与 IsMultiProtocolAPIKeyProvider 归属。
// 默认端点需与前端 credentialsBuilder.ts 的预设保持一致。
var providerProfiles = map[string]*ProviderProfile{
PlatformKimi: {
Platform: PlatformKimi,
DefaultMode: AccountModePayG,
Routing: ProviderRoutingByInbound,
Modes: map[string]ProviderEndpoints{
AccountModePayG: {
BaseURLs: map[string]string{
APIProtocolChatCompletions: DefaultKimiPayGBaseURL,
APIProtocolResponses: DefaultKimiPayGBaseURL,
APIProtocolAnthropic: DefaultKimiPayGAnthropicBaseURL,
},
},
AccountModeCoding: {
BaseURLs: map[string]string{
APIProtocolChatCompletions: DefaultKimiCodingBaseURL,
APIProtocolResponses: DefaultKimiCodingBaseURL,
APIProtocolAnthropic: DefaultKimiCodingAnthropicBaseURL,
},
},
},
},
PlatformZhipu: {
Platform: PlatformZhipu,
DefaultMode: AccountModePayG,
Routing: ProviderRoutingByInbound,
// 智谱没有原生 Responses 端点,故不登记 responses 条目。
Modes: map[string]ProviderEndpoints{
AccountModePayG: {
BaseURLs: map[string]string{
APIProtocolChatCompletions: DefaultZhipuPayGBaseURL,
APIProtocolAnthropic: DefaultZhipuAnthropicBaseURL,
},
},
AccountModeCoding: {
BaseURLs: map[string]string{
APIProtocolChatCompletions: DefaultZhipuCodingBaseURL,
APIProtocolAnthropic: DefaultZhipuAnthropicBaseURL,
},
},
},
},
PlatformDeepseek: {
Platform: PlatformDeepseek,
DefaultMode: AccountModePayG,
Routing: ProviderRoutingByInbound,
// DeepSeek 官方 Responses 端点为 /responses(无 /v1 前缀,适配 Codex)。
ResponsesPath: "/responses",
Modes: map[string]ProviderEndpoints{
AccountModePayG: {
BaseURLs: map[string]string{
APIProtocolChatCompletions: DefaultDeepseekBaseURL,
APIProtocolResponses: DefaultDeepseekBaseURL,
APIProtocolAnthropic: DefaultDeepseekAnthropicBaseURL,
},
},
},
},
PlatformMiniMax: {
Platform: PlatformMiniMax,
DefaultMode: AccountModePayG,
Routing: ProviderRoutingByInbound,
Modes: map[string]ProviderEndpoints{
// 按量付费与 Coding/Token Plan 共用推理域名,靠 API Key 区分套餐。
AccountModePayG: {
BaseURLs: map[string]string{
APIProtocolChatCompletions: DefaultMiniMaxBaseURL,
APIProtocolResponses: DefaultMiniMaxBaseURL,
APIProtocolAnthropic: DefaultMiniMaxAnthropicBaseURL,
},
},
AccountModeCoding: {
BaseURLs: map[string]string{
APIProtocolChatCompletions: DefaultMiniMaxBaseURL,
APIProtocolResponses: DefaultMiniMaxBaseURL,
APIProtocolAnthropic: DefaultMiniMaxAnthropicBaseURL,
},
},
},
},
PlatformOpenCodeGo: {
Platform: PlatformOpenCodeGo,
DefaultMode: AccountModeGo,
Routing: ProviderRoutingByModel,
DefaultTestModel: DefaultOpenCodeGoTestModel,
Modes: map[string]ProviderEndpoints{
AccountModeGo: {
BaseURLs: map[string]string{
APIProtocolChatCompletions: DefaultOpenCodeGoBaseURL,
APIProtocolResponses: DefaultOpenCodeGoBaseURL,
APIProtocolAnthropic: DefaultOpenCodeGoAnthropicBaseURL,
},
ProtocolRules: DefaultOpenCodeGoProtocolRules(),
},
AccountModeZen: {
BaseURLs: map[string]string{
APIProtocolChatCompletions: DefaultOpenCodeZenBaseURL,
APIProtocolResponses: DefaultOpenCodeZenBaseURL,
APIProtocolAnthropic: DefaultOpenCodeZenAnthropicBaseURL,
},
ProtocolRules: DefaultOpenCodeZenProtocolRules(),
},
},
},
PlatformCommandCode: {
Platform: PlatformCommandCode,
DefaultMode: AccountModePayG,
Routing: ProviderRoutingByModel,
DefaultTestModel: DefaultCommandCodeTestModel,
ModelCatalog: true,
// 订阅套餐与 Provider 按量计费共用同一个 API Key 与端点,只有一个接入模式。
Modes: map[string]ProviderEndpoints{
AccountModePayG: {
BaseURLs: map[string]string{
APIProtocolChatCompletions: DefaultCommandCodeBaseURL,
APIProtocolResponses: DefaultCommandCodeBaseURL,
APIProtocolAnthropic: DefaultCommandCodeAnthropicBaseURL,
},
ProtocolRules: DefaultCommandCodeProtocolRules(),
},
},
},
PlatformCline: {
Platform: PlatformCline,
DefaultMode: AccountModePayG,
DefaultTestModel: DefaultClineTestModel,
// 按量积分与 ClinePass 订阅共用同一个 API Key 与端点,由模型名决定扣哪一边
// (cline-pass/* 走订阅),只有一个接入模式;两边的超限分别冷却(见 cline.go)。
Modes: map[string]ProviderEndpoints{
AccountModePayG: {
BaseURLs: map[string]string{APIProtocolChatCompletions: DefaultClineBaseURL},
},
},
},
}
// LookupProviderProfile 返回 platform 对应的多协议供应商 profile;非此类平台返回 nil。
func LookupProviderProfile(platform string) *ProviderProfile {
return providerProfiles[platform]
}
func (a *Account) providerProfile() *ProviderProfile {
if a == nil {
return nil
}
return LookupProviderProfile(a.Platform)
}
// defaultProviderBaseURL 按账号 account_mode 返回多协议供应商指定协议的官方默认基址;
// 非多协议供应商或协议未知时为空串。
func (a *Account) defaultProviderBaseURL(protocol string) string {
profile := a.providerProfile()
if profile == nil {
return ""
}
return profile.DefaultBaseURL(a.GetCredential("account_mode"), protocol)
}
// providerSupportsProtocol 报告账号所在供应商在其 account_mode 下是否提供该协议的原生端点。
func (a *Account) providerSupportsProtocol(protocol string) bool {
profile := a.providerProfile()
return profile != nil && profile.SupportsProtocol(a.GetCredential("account_mode"), protocol)
}
// routesByModel 报告账号所在供应商是否按模型名分流上游协议(多模型聚合平台)。
func (a *Account) routesByModel() bool {
profile := a.providerProfile()
return profile != nil && profile.Routing == ProviderRoutingByModel
}
// RoutesProtocolByInbound 报告账号是否为按入站协议分流的多协议 API Key 供应商
// (单一厂商型,目前即国产厂商)。这类账号共享"显式协议优先、adaptive 选同协议
// 原生端点"的转发、Responses 能力标记与本地 token 估算逻辑;按模型分流的聚合
// 平台由 protocol_rules 决定协议,走 routesByModel 分支。
func (a *Account) RoutesProtocolByInbound() bool {
profile := a.providerProfile()
return profile != nil && profile.Routing != ProviderRoutingByModel
}