2026-09-11 10:05:24 +08:00
package service
import (
"fmt"
"net/http"
2026-10-03 12:51:00 +08:00
"slices"
2026-09-11 10:05:24 +08:00
"strings"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
2026-10-06 23:18:57 +08:00
"github.com/gin-gonic/gin"
2026-09-11 10:05:24 +08:00
)
// OpenCode Go 是 OpenCode Zen 的订阅网关:同一 API Key 下按模型分流到
// Responses / Chat Completions / Anthropic Messages 三种原生端点,
// 额度窗口为 rolling(5h) / weekly / monthly。
const (
openCodeGoUsagePath = "/usage"
// DefaultOpenCodeGoTestModel is the admin connection-test fallback when
// the UI does not pick a model. glm-5.3 is a Chat Completions catalog ID.
DefaultOpenCodeGoTestModel = "glm-5.3"
2026-09-27 11:08:41 +08:00
protocolRulesCredentialKey = "protocol_rules"
maxProtocolRules = 64
maxProtocolRulePatternLength = 128
2026-09-11 10:05:24 +08:00
)
// DefaultOpenCodeGoModelIDs 是官方文档当前公开的模型 ID 目录,
// 供 /v1/models 在尚未同步上游列表时回退,以及账号白名单预填。
func DefaultOpenCodeGoModelIDs () [] string {
return [] string {
2026-09-22 10:30:01 +08:00
"grok-4.7" ,
2026-09-11 10:05:24 +08:00
"grok-4.6" ,
"gpt-5.6-luna" ,
"glm-5.3-flash" ,
"glm-5.3" ,
"glm-5.2" ,
"glm-5.1" ,
"kimi-k3" ,
"kimi-k2.7-code" ,
"kimi-k2.6" ,
"longcat-2.0" ,
"deepseek-v4-pro" ,
"deepseek-v4-flash" ,
"deepseek-v4-flash-vision-exp" ,
"mimo-v2.5" ,
"mimo-v2.5-pro" ,
"minimax-m3" ,
"minimax-m2.7" ,
"minimax-m2.5" ,
"muse-spark-1.3-contributor" ,
"muse-spark-1.2-contributor" ,
"qwen3.8-max" ,
"qwen3.8-flash" ,
"qwen3.7-max" ,
"qwen3.7-plus" ,
"qwen3.6-plus" ,
"hy4-preview" ,
"hy3" ,
"omen-alpha" ,
}
}
func normalizeOpenCodeGoModelID ( model string ) string {
model = strings . ToLower ( strings . TrimSpace ( model ))
for _ , prefix := range [] string { "opencode-go/" , "opencode_go/" , "opencode/" } {
model = strings . TrimPrefix ( model , prefix )
}
return model
}
2026-10-06 23:18:57 +08:00
// IsOpenCodeUnsupportedModel 判断请求的模型是否属于 OpenCode 专有协议端点模型。
// 官方 Zen 上 gemini-* 仅走 Google SDK 专属端点 /zen/v1/models/<id>,
// jev-* 仅走 SystemOne 专属端点 /zen/v1/systemone,
// 在通用 Chat Completions / Anthropic Messages / Responses 网关端点上不可用。
func IsOpenCodeUnsupportedModel ( model string ) bool {
normalized := normalizeOpenCodeGoModelID ( model )
return strings . HasPrefix ( normalized , "gemini-" ) || strings . HasPrefix ( normalized , "jev-" )
}
func writeOpenCodeUnsupportedModelError ( c * gin . Context , isAnthropic bool , model string ) error {
msg := fmt . Sprintf ( "Model '%s' is not supported on OpenCode standard gateway (gemini models require Google SDK endpoint, jev models require System One endpoint)" , model )
if c != nil {
if isAnthropic {
c . JSON ( http . StatusBadRequest , gin . H {
"type" : "error" ,
"error" : gin . H {
"type" : "invalid_request_error" ,
"message" : msg ,
},
})
} else {
c . JSON ( http . StatusBadRequest , gin . H {
"error" : gin . H {
"type" : "invalid_request_error" ,
"code" : "model_not_supported" ,
"message" : msg ,
},
})
}
}
return fmt . Errorf ( "opencode unsupported model: %s" , model )
}
2026-09-27 11:08:41 +08:00
// ProtocolRule is one model-pattern → native protocol mapping.
2026-09-11 10:05:24 +08:00
// Pattern is an exact ID or a suffix glob (foo* / *). First match wins.
2026-09-27 11:08:41 +08:00
type ProtocolRule struct {
2026-09-11 10:05:24 +08:00
Pattern string `json:"pattern"`
Protocol string `json:"protocol"`
2026-10-03 12:51:00 +08:00
// Protocols 是命中模型支持的全部原生协议,首项即 Protocol(首选);为空表示只支持
// Protocol。入站协议在其中时同协议直通,否则走首选协议(见 resolveModelRoutedProtocolFor)。
Protocols [] string `json:"protocols,omitempty"`
}
// normalizeProtocolSet 返回以 preferred 开头、去重且只含原生协议的协议集合。
func normalizeProtocolSet ( preferred string , protocols [] string ) [] string {
set := [] string { preferred }
for _ , protocol := range protocols {
if ! isNativeUpstreamProtocol ( protocol ) || slices . Contains ( set , protocol ) {
continue
}
set = append ( set , protocol )
}
return set
2026-09-11 10:05:24 +08:00
}
// DefaultOpenCodeGoProtocolRules is the built-in adaptive routing table for Go.
2026-09-27 11:08:41 +08:00
func DefaultOpenCodeGoProtocolRules () [] ProtocolRule {
return [] ProtocolRule {
2026-09-11 10:05:24 +08:00
{ Pattern : "grok-*" , Protocol : APIProtocolResponses },
{ Pattern : "gpt-*" , Protocol : APIProtocolResponses },
{ Pattern : "muse-spark-*" , Protocol : APIProtocolResponses },
{ Pattern : "minimax-*" , Protocol : APIProtocolAnthropic },
{ Pattern : "qwen*" , Protocol : APIProtocolAnthropic },
}
}
// DefaultOpenCodeZenProtocolRules 对齐 https://opencode.ai/docs/zen/ 端点表:
2026-10-06 23:18:57 +08:00
// GPT/Grok/Muse Spark → Responses, Claude/Qwen(除 qwen3.8-max) → Anthropic,
// qwen3.8-max 及其余模型 → Chat Completions。
2026-09-27 11:08:41 +08:00
func DefaultOpenCodeZenProtocolRules () [] ProtocolRule {
return [] ProtocolRule {
2026-09-11 10:05:24 +08:00
{ Pattern : "grok-*" , Protocol : APIProtocolResponses },
{ Pattern : "gpt-*" , Protocol : APIProtocolResponses },
{ Pattern : "muse-spark-*" , Protocol : APIProtocolResponses },
{ Pattern : "claude-*" , Protocol : APIProtocolAnthropic },
2026-10-06 23:18:57 +08:00
{ Pattern : "qwen3.8-max" , Protocol : APIProtocolChatCompletions },
2026-09-11 10:05:24 +08:00
{ Pattern : "qwen*" , Protocol : APIProtocolAnthropic },
}
}
2026-09-27 11:08:41 +08:00
func isNativeUpstreamProtocol ( protocol string ) bool {
2026-09-11 10:05:24 +08:00
switch protocol {
case APIProtocolChatCompletions , APIProtocolAnthropic , APIProtocolResponses :
return true
default :
return false
}
}
2026-09-27 11:08:41 +08:00
func protocolRulePatternMatches ( pattern , model string ) bool {
2026-09-11 10:05:24 +08:00
pattern = strings . ToLower ( strings . TrimSpace ( pattern ))
model = normalizeOpenCodeGoModelID ( model )
if pattern == "" || model == "" {
return false
}
if pattern == "*" {
return true
}
if strings . HasSuffix ( pattern , "*" ) {
return strings . HasPrefix ( model , strings . TrimSuffix ( pattern , "*" ))
}
return model == pattern
}
2026-10-03 12:51:00 +08:00
// matchProtocolRules 返回首条命中规则的首选协议;未命中时为 Chat Completions。
2026-09-27 11:08:41 +08:00
func matchProtocolRules ( model string , rules [] ProtocolRule ) string {
2026-10-03 12:51:00 +08:00
if set , ok := matchProtocolRuleSet ( model , rules ); ok {
return set [ 0 ]
}
return APIProtocolChatCompletions
}
// matchProtocolRuleSet 返回首条命中规则支持的协议集合(首项为首选);未命中时 ok=false。
func matchProtocolRuleSet ( model string , rules [] ProtocolRule ) ([] string , bool ) {
2026-09-11 10:05:24 +08:00
for _ , rule := range rules {
2026-09-27 11:08:41 +08:00
if ! isNativeUpstreamProtocol ( rule . Protocol ) {
2026-09-11 10:05:24 +08:00
continue
}
2026-09-27 11:08:41 +08:00
if protocolRulePatternMatches ( rule . Pattern , model ) {
2026-10-03 12:51:00 +08:00
return normalizeProtocolSet ( rule . Protocol , rule . Protocols ), true
2026-09-11 10:05:24 +08:00
}
}
2026-10-03 12:51:00 +08:00
return nil , false
2026-09-11 10:05:24 +08:00
}
2026-09-27 11:08:41 +08:00
func ( a * Account ) configuredProtocolRules () ([] ProtocolRule , bool ) {
2026-09-11 10:05:24 +08:00
if a == nil || a . Credentials == nil {
return nil , false
}
2026-09-27 11:08:41 +08:00
raw , ok := a . Credentials [ protocolRulesCredentialKey ]
2026-09-11 10:05:24 +08:00
if ! ok || raw == nil {
return nil , false
}
2026-09-27 11:08:41 +08:00
rules , err := parseProtocolRules ( raw )
2026-09-11 10:05:24 +08:00
if err != nil {
return nil , false
}
return rules , true
}
2026-09-27 11:08:41 +08:00
func parseProtocolRules ( raw any ) ([] ProtocolRule , error ) {
items , err := protocolRuleItems ( raw )
2026-09-11 10:05:24 +08:00
if err != nil {
return nil , err
}
2026-09-27 11:08:41 +08:00
if len ( items ) > maxProtocolRules {
return nil , fmt . Errorf ( "protocol_rules supports at most %d entries" , maxProtocolRules )
2026-09-11 10:05:24 +08:00
}
2026-09-27 11:08:41 +08:00
rules := make ([] ProtocolRule , 0 , len ( items ))
2026-09-11 10:05:24 +08:00
for i , item := range items {
entry , ok := item .( map [ string ] any )
if ! ok {
return nil , fmt . Errorf ( "protocol_rules[%d] must be an object" , i )
}
pattern , _ := entry [ "pattern" ].( string )
protocol , _ := entry [ "protocol" ].( string )
2026-09-27 11:08:41 +08:00
pattern , err := normalizeProtocolRulePattern ( pattern )
2026-09-11 10:05:24 +08:00
if err != nil {
return nil , fmt . Errorf ( "protocol_rules[%d]: %w" , i , err )
}
2026-10-03 12:51:00 +08:00
protocols , err := protocolRuleProtocols ( entry [ "protocols" ])
if err != nil {
return nil , fmt . Errorf ( "protocol_rules[%d]: %w" , i , err )
}
2026-09-11 10:05:24 +08:00
protocol = strings . TrimSpace ( protocol )
2026-10-03 12:51:00 +08:00
if protocol == "" && len ( protocols ) > 0 {
protocol = protocols [ 0 ]
}
2026-09-27 11:08:41 +08:00
if ! isNativeUpstreamProtocol ( protocol ) {
2026-09-11 10:05:24 +08:00
return nil , fmt . Errorf ( "protocol_rules[%d]: protocol must be chat_completions, anthropic, or responses" , i )
}
2026-10-03 12:51:00 +08:00
rule := ProtocolRule { Pattern : pattern , Protocol : protocol }
if set := normalizeProtocolSet ( protocol , protocols ); len ( set ) > 1 {
rule . Protocols = set
}
rules = append ( rules , rule )
2026-09-11 10:05:24 +08:00
}
return rules , nil
}
2026-10-03 12:51:00 +08:00
// protocolRuleProtocols 解析规则的 protocols 列表(可缺省);其中每一项都须为原生协议。
func protocolRuleProtocols ( raw any ) ([] string , error ) {
if raw == nil {
return nil , nil
}
var items [] any
switch values := raw .( type ) {
case [] any :
items = values
case [] string :
for _ , value := range values {
items = append ( items , value )
}
default :
return nil , fmt . Errorf ( "protocols must be an array" )
}
out := make ([] string , 0 , len ( items ))
for _ , item := range items {
protocol , _ := item .( string )
protocol = strings . TrimSpace ( protocol )
if ! isNativeUpstreamProtocol ( protocol ) {
return nil , fmt . Errorf ( "protocols must only contain chat_completions, anthropic, or responses" )
}
out = append ( out , protocol )
}
return out , nil
}
2026-09-27 11:08:41 +08:00
func protocolRuleItems ( raw any ) ([] any , error ) {
2026-09-11 10:05:24 +08:00
switch items := raw .( type ) {
case [] any :
return items , nil
case [] map [ string ] any :
out := make ([] any , 0 , len ( items ))
for _ , item := range items {
out = append ( out , item )
}
return out , nil
default :
return nil , fmt . Errorf ( "protocol_rules must be an array" )
}
}
2026-09-27 11:08:41 +08:00
func normalizeProtocolRulePattern ( pattern string ) ( string , error ) {
2026-09-11 10:05:24 +08:00
pattern = strings . ToLower ( strings . TrimSpace ( pattern ))
if pattern == "" {
return "" , fmt . Errorf ( "pattern is required" )
}
2026-09-27 11:08:41 +08:00
if len ( pattern ) > maxProtocolRulePatternLength {
2026-09-11 10:05:24 +08:00
return "" , fmt . Errorf ( "pattern is too long" )
}
if strings . ContainsAny ( pattern , " \t" ) {
return "" , fmt . Errorf ( "pattern must not contain whitespace" )
}
star := strings . Count ( pattern , "*" )
if star > 1 || ( star == 1 && ! strings . HasSuffix ( pattern , "*" )) {
return "" , fmt . Errorf ( "pattern may use a single trailing * wildcard" )
}
return pattern , nil
}
2026-09-27 11:08:41 +08:00
// NormalizeProtocolRulesCredentials 校验并原地规范化 credentials.protocol_rules。
2026-09-11 10:05:24 +08:00
// 未携带该字段时为 no-op,使旧账号继续使用内置默认表。
2026-09-27 11:08:41 +08:00
func NormalizeProtocolRulesCredentials ( credentials map [ string ] any ) error {
2026-09-11 10:05:24 +08:00
if credentials == nil {
return nil
}
2026-09-27 11:08:41 +08:00
raw , ok := credentials [ protocolRulesCredentialKey ]
2026-09-11 10:05:24 +08:00
if ! ok || raw == nil {
return nil
}
2026-09-27 11:08:41 +08:00
rules , err := parseProtocolRules ( raw )
2026-09-11 10:05:24 +08:00
if err != nil {
return infraerrors . New ( http . StatusBadRequest , "INVALID_OPENCODE_GO_PROTOCOL_RULES" , err . Error ())
}
encoded := make ([] any , 0 , len ( rules ))
for _ , rule := range rules {
2026-10-03 12:51:00 +08:00
item := map [ string ] any {
2026-09-11 10:05:24 +08:00
"pattern" : rule . Pattern ,
"protocol" : rule . Protocol ,
2026-10-03 12:51:00 +08:00
}
if len ( rule . Protocols ) > 1 {
protocols := make ([] any , 0 , len ( rule . Protocols ))
for _ , protocol := range rule . Protocols {
protocols = append ( protocols , protocol )
}
item [ "protocols" ] = protocols
}
encoded = append ( encoded , item )
2026-09-11 10:05:24 +08:00
}
2026-09-27 11:08:41 +08:00
credentials [ protocolRulesCredentialKey ] = encoded
2026-09-11 10:05:24 +08:00
return nil
}
func ( a * Account ) IsOpenCodeGo () bool {
return a != nil && a . Platform == PlatformOpenCodeGo
}
// GetOpenCodeAccountMode 返回 OpenCode 账号类型。未设置时按 Go 处理,兼容已有账号。
func ( a * Account ) GetOpenCodeAccountMode () string {
if a == nil || ! a . IsOpenCodeGo () {
return ""
}
if strings . TrimSpace ( a . GetCredential ( "account_mode" )) == AccountModeZen {
return AccountModeZen
}
return AccountModeGo
}
func ( a * Account ) IsOpenCodeZen () bool {
return a . GetOpenCodeAccountMode () == AccountModeZen
}
func ( a * Account ) IsOpenCodeGoPlan () bool {
return a . GetOpenCodeAccountMode () == AccountModeGo
}
func ( a * Account ) IsMultiProtocolAPIKey () bool {
return a != nil && IsMultiProtocolAPIKeyProvider ( a . Platform )
}
2026-09-18 09:16:42 +08:00
// openCodeGoQuotaURL 根据 base_url 解析 OpenCode Go 额度端点。
// /zen/go/v1( Chat 协议默认,DefaultOpenCodeGoBaseURL)与 /zen/go
// ( Anthropic 协议默认,DefaultOpenCodeGoAnthropicBaseURL)两种 base 统一
// 剥掉尾部 /v1 后拼回 /v1/usage(实测 /zen/go/usage → 404),协议切换不
// 影响额度探测端点。与 kimiQuotaURL 同一惯例。
2026-09-11 10:05:24 +08:00
func openCodeGoQuotaURL ( baseURL string ) string {
base := strings . TrimRight ( strings . TrimSpace ( baseURL ), "/" )
if base == "" {
base = DefaultOpenCodeGoBaseURL
}
2026-09-18 09:16:42 +08:00
base = strings . TrimSuffix ( base , "/v1" )
return base + "/v1" + openCodeGoUsagePath
2026-09-11 10:05:24 +08:00
}