mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-19 16:17:56 +00:00
Merge pull request #7562 from william-drakemond/fix/opencode-zen-deepseek-reasoning-placeholder
fix(openai): inject DeepSeek reasoning placeholder for OpenCode Zen upstream
This commit is contained in:
@@ -87,6 +87,66 @@ func TestEnsureDeepSeekChatReasoningPlaceholders(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// OpenCode Zen / Go 把 deepseek-* 模型转发给 DeepSeek,同一条 thinking-mode 400
|
||||
// 会原样回吐,占位注入必须覆盖这条链路;同账号下的非 DeepSeek 模型不受影响。
|
||||
func TestEnsureDeepSeekChatReasoningPlaceholders_OpenCodeZenUpstream(t *testing.T) {
|
||||
openCodeGo := &Account{
|
||||
ID: 24,
|
||||
Name: "opencode-go",
|
||||
Platform: PlatformOpenCodeGo,
|
||||
Type: AccountTypeAPIKey,
|
||||
Credentials: map[string]any{"api_key": "sk-test", "base_url": DefaultOpenCodeGoBaseURL},
|
||||
}
|
||||
openAIViaZen := &Account{
|
||||
ID: 25,
|
||||
Platform: PlatformOpenAI,
|
||||
Type: AccountTypeAPIKey,
|
||||
Credentials: map[string]any{"api_key": "sk-test", "base_url": DefaultOpenCodeZenBaseURL},
|
||||
}
|
||||
thirdPartyDeepSeekName := &Account{
|
||||
ID: 26,
|
||||
Platform: PlatformOpenAI,
|
||||
Type: AccountTypeAPIKey,
|
||||
Credentials: map[string]any{"api_key": "sk-test", "base_url": "http://upstream.example"},
|
||||
}
|
||||
|
||||
missingFor := func(model string) []byte {
|
||||
return []byte(`{"model":"` + model + `","messages":[{"role":"user","content":"hi"},{"role":"assistant","content":"","tool_calls":[{"id":"call_1","type":"function","function":{"name":"exec","arguments":"{}"}}]},{"role":"tool","tool_call_id":"call_1","content":"ok"}]}`)
|
||||
}
|
||||
|
||||
t.Run("opencode_go_deepseek_model_fills_empty_assistant", func(t *testing.T) {
|
||||
body := missingFor("deepseek-v4.1-flash")
|
||||
got := ensureDeepSeekChatReasoningPlaceholders(openCodeGo, body)
|
||||
require.Equal(t, deepSeekChatReasoningPlaceholderText, gjson.GetBytes(got, "messages.1.reasoning_content").String())
|
||||
require.False(t, gjson.GetBytes(got, "messages.0.reasoning_content").Exists())
|
||||
require.False(t, gjson.GetBytes(got, "messages.2.reasoning_content").Exists())
|
||||
})
|
||||
|
||||
t.Run("opencode_go_non_deepseek_model_unchanged", func(t *testing.T) {
|
||||
body := missingFor("glm-5.3")
|
||||
got := ensureDeepSeekChatReasoningPlaceholders(openCodeGo, body)
|
||||
require.Equal(t, string(body), string(got))
|
||||
})
|
||||
|
||||
t.Run("opencode_prefixed_model_id_fills_empty_assistant", func(t *testing.T) {
|
||||
body := missingFor("opencode-go/DeepSeek-V4-Pro")
|
||||
got := ensureDeepSeekChatReasoningPlaceholders(openCodeGo, body)
|
||||
require.Equal(t, deepSeekChatReasoningPlaceholderText, gjson.GetBytes(got, "messages.1.reasoning_content").String())
|
||||
})
|
||||
|
||||
t.Run("openai_platform_pointed_at_zen_host_fills_empty_assistant", func(t *testing.T) {
|
||||
body := missingFor("deepseek-v4-flash")
|
||||
got := ensureDeepSeekChatReasoningPlaceholders(openAIViaZen, body)
|
||||
require.Equal(t, deepSeekChatReasoningPlaceholderText, gjson.GetBytes(got, "messages.1.reasoning_content").String())
|
||||
})
|
||||
|
||||
t.Run("unrelated_upstream_with_deepseek_model_name_unchanged", func(t *testing.T) {
|
||||
body := missingFor("deepseek-v4.1-flash")
|
||||
got := ensureDeepSeekChatReasoningPlaceholders(thirdPartyDeepSeekName, body)
|
||||
require.Equal(t, string(body), string(got))
|
||||
})
|
||||
}
|
||||
|
||||
type reasoningHitCache struct {
|
||||
stubGatewayCache
|
||||
values map[string]string
|
||||
|
||||
@@ -431,15 +431,53 @@ func targetsDeepSeekAPIHost(account *Account) bool {
|
||||
return strings.EqualFold(u.Hostname(), ds.Hostname())
|
||||
}
|
||||
|
||||
// targetsOpenCodeZenUpstream 报告该账号上游是否是官方 OpenCode Zen / Go 网关。
|
||||
// platform=opencode_go 直接命中;其他平台按 base_url 主机判定,覆盖用 API Key
|
||||
// 直接把 openai 平台账号指向 opencode.ai 的接入方式。
|
||||
func targetsOpenCodeZenUpstream(account *Account) bool {
|
||||
if account == nil {
|
||||
return false
|
||||
}
|
||||
if account.IsOpenCodeGo() {
|
||||
return true
|
||||
}
|
||||
return isOfficialOpenCodeHost(account.GetOpenAIBaseURL())
|
||||
}
|
||||
|
||||
// isDeepSeekCatalogModel 判定(剥掉 opencode 前缀后的)模型 ID 是否属于
|
||||
// DeepSeek 系列,例如 deepseek-v4-flash / deepseek-v4.1-flash / deepseek-chat。
|
||||
func isDeepSeekCatalogModel(model string) bool {
|
||||
return strings.HasPrefix(normalizeOpenCodeGoModelID(model), "deepseek")
|
||||
}
|
||||
|
||||
// requiresDeepSeekChatReasoning 报告该 Chat Completions 请求的上游是否按
|
||||
// DeepSeek thinking mode 语义校验 reasoning_content。
|
||||
//
|
||||
// DeepSeek 官方 API 直接命中。OpenCode Zen / Go 是转发型订阅网关:deepseek-*
|
||||
// 模型由 DeepSeek 实际承载,同一条 400 原文会被一字不差地回吐,所以按
|
||||
// 「官方 OpenCode 上游 + deepseek-* 模型」补一条判定。
|
||||
//
|
||||
// 不放宽成「任意上游的 deepseek-* 模型名」:占位符是写进请求体的额外字段,
|
||||
// 对没有这条约束的上游属于无谓改写,且可能触发未知字段校验。
|
||||
func requiresDeepSeekChatReasoning(account *Account, body []byte) bool {
|
||||
if targetsDeepSeekAPIHost(account) {
|
||||
return true
|
||||
}
|
||||
if !targetsOpenCodeZenUpstream(account) {
|
||||
return false
|
||||
}
|
||||
return isDeepSeekCatalogModel(gjson.GetBytes(body, "model").String())
|
||||
}
|
||||
|
||||
// ensureDeepSeekChatReasoningPlaceholders 给缺 reasoning_content 的 assistant
|
||||
// 消息补单个空格占位。DeepSeek thinking mode 要求历史里每条产生过思维的
|
||||
// assistant 消息都回传该字段,否则 400
|
||||
// "The `reasoning_content` in the thinking mode must be passed back to the API"。
|
||||
//
|
||||
// 桥接会从 summary / 缓存回注真实明文;这里只填仍为空的缺口,不覆盖已有内容。
|
||||
// 非 DeepSeek 上游原样返回(字节不变)。
|
||||
// 判定见 requiresDeepSeekChatReasoning,不命中的上游原样返回(字节不变)。
|
||||
func ensureDeepSeekChatReasoningPlaceholders(account *Account, body []byte) []byte {
|
||||
if !targetsDeepSeekAPIHost(account) {
|
||||
if !requiresDeepSeekChatReasoning(account, body) {
|
||||
return body
|
||||
}
|
||||
messages := gjson.GetBytes(body, "messages")
|
||||
|
||||
Reference in New Issue
Block a user