mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 13:28:39 +08:00
Merge pull request #4906 from alfadb/feature/kimi-k3-support
feat: 支持 Kimi K3 与 Kimi Code 模型 ID
This commit is contained in:
@@ -1881,6 +1881,8 @@ func setDefaults() {
|
||||
"api.openai.com",
|
||||
"api.anthropic.com",
|
||||
"api.kimi.com",
|
||||
"api.moonshot.ai",
|
||||
"api.moonshot.cn",
|
||||
"open.bigmodel.cn",
|
||||
"api.minimaxi.com",
|
||||
"generativelanguage.googleapis.com",
|
||||
|
||||
@@ -755,6 +755,21 @@ func TestLoadDefaultSecurityToggles(t *testing.T) {
|
||||
if !cfg.Security.ResponseHeaders.Enabled {
|
||||
t.Fatalf("ResponseHeaders.Enabled = false, want true")
|
||||
}
|
||||
|
||||
wantHosts := []string{
|
||||
"api.kimi.com",
|
||||
"api.moonshot.ai",
|
||||
"api.moonshot.cn",
|
||||
}
|
||||
hostSet := make(map[string]struct{}, len(cfg.Security.URLAllowlist.UpstreamHosts))
|
||||
for _, h := range cfg.Security.URLAllowlist.UpstreamHosts {
|
||||
hostSet[h] = struct{}{}
|
||||
}
|
||||
for _, want := range wantHosts {
|
||||
if _, ok := hostSet[want]; !ok {
|
||||
t.Fatalf("URLAllowlist.UpstreamHosts missing %q; got %v", want, cfg.Security.URLAllowlist.UpstreamHosts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadDefaultServerMode(t *testing.T) {
|
||||
|
||||
@@ -485,6 +485,15 @@ func (s *BillingService) initFallbackPricing() {
|
||||
// 交叉验证:https://www.tmtpost.com/7961404.html (USD 口径)
|
||||
// Moonshot V1 (¥2/¥5/¥10 多 tier) 公开页未直接标注 USD 价,本分支不覆盖,避免误计价。
|
||||
// K2-0905 / K2-0711 官方页面未保留定价,不覆盖。
|
||||
// Kimi K3 国际站 USD 价目:https://platform.kimi.ai/docs/pricing/chat-k3.md
|
||||
// Kimi Code bare aliases(k3 / k3-256k)官方无按 token 价目;复用 API Platform
|
||||
// kimi-k3 档位作代理计费 fallback(同 kimi-for-coding 对 K2.6 的处理口径)。
|
||||
s.fallbackPrices["kimi-k3"] = &ModelPricing{
|
||||
InputPricePerToken: 3e-6, // $3.00 per MTok (cache miss)
|
||||
OutputPricePerToken: 15e-6, // $15.00 per MTok
|
||||
CacheReadPricePerToken: 0.30e-6, // $0.30 per MTok (cache hit)
|
||||
SupportsCacheBreakdown: false,
|
||||
}
|
||||
s.fallbackPrices["kimi-k2.6"] = &ModelPricing{
|
||||
InputPricePerToken: 0.95e-6, // $0.95 per MTok (cache miss)
|
||||
OutputPricePerToken: 4e-6, // $4.00 per MTok
|
||||
@@ -703,11 +712,19 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing {
|
||||
return s.fallbackPrices["glm-4-32b-0414-128k"]
|
||||
}
|
||||
|
||||
// 月之暗面 Kimi(kimi-k2.6 / kimi-for-coding / kimi-k2.5 / kimi-k2-thinking / kimi-k2)
|
||||
// 月之暗面 Kimi(kimi-k3 / k3 / k3-256k / kimi-k2.6 / kimi-for-coding / kimi-k2.5 / kimi-k2-thinking / kimi-k2)
|
||||
// K2-0905 / K2-0711 官方未保留定价,不进入 fallback。
|
||||
// K3 规则置于 K2 前:API Platform 仅官方 kimi-k3(及 / 路径后缀);
|
||||
// Code bare aliases 仅精确 k3 / k3-256k 或 /k3|/k3-256k 后缀,避免 kimi-k30 等未知型号误命中。
|
||||
// 注意:kimi-k3[1m] 是 Claude Code 上下文选择语法,不是 Kimi API 模型 ID,不进入 fallback。
|
||||
if strings.Contains(modelLower, "kimi-for-coding") {
|
||||
return s.fallbackPrices["kimi-for-coding"]
|
||||
}
|
||||
if modelLower == "kimi-k3" || strings.HasSuffix(modelLower, "/kimi-k3") ||
|
||||
modelLower == "k3" || modelLower == "k3-256k" ||
|
||||
strings.HasSuffix(modelLower, "/k3") || strings.HasSuffix(modelLower, "/k3-256k") {
|
||||
return s.fallbackPrices["kimi-k3"]
|
||||
}
|
||||
if strings.Contains(modelLower, "kimi-k2.6") || strings.Contains(modelLower, "kimi-k2-6") {
|
||||
return s.fallbackPrices["kimi-k2.6"]
|
||||
}
|
||||
|
||||
@@ -589,6 +589,41 @@ func TestGetFallbackPricing_FamilyMatching(t *testing.T) {
|
||||
},
|
||||
|
||||
// ---- 月之暗面 Kimi ----
|
||||
{
|
||||
name: "kimi k3 flagship",
|
||||
model: "kimi-k3",
|
||||
expectedInput: 3e-6,
|
||||
expectedOutput: floatPtr(15e-6),
|
||||
expectedCacheRead: floatPtr(0.30e-6),
|
||||
},
|
||||
{
|
||||
name: "kimi code bare alias k3",
|
||||
model: "k3",
|
||||
expectedInput: 3e-6,
|
||||
expectedOutput: floatPtr(15e-6),
|
||||
expectedCacheRead: floatPtr(0.30e-6),
|
||||
},
|
||||
{
|
||||
name: "kimi code bare alias k3-256k",
|
||||
model: "k3-256k",
|
||||
expectedInput: 3e-6,
|
||||
expectedOutput: floatPtr(15e-6),
|
||||
expectedCacheRead: floatPtr(0.30e-6),
|
||||
},
|
||||
{
|
||||
name: "kimi k3 path suffix moonshot",
|
||||
model: "moonshot/kimi-k3",
|
||||
expectedInput: 3e-6,
|
||||
expectedOutput: floatPtr(15e-6),
|
||||
expectedCacheRead: floatPtr(0.30e-6),
|
||||
},
|
||||
{
|
||||
name: "kimi code bare path suffix",
|
||||
model: "kimi-code/k3",
|
||||
expectedInput: 3e-6,
|
||||
expectedOutput: floatPtr(15e-6),
|
||||
expectedCacheRead: floatPtr(0.30e-6),
|
||||
},
|
||||
{
|
||||
name: "kimi k2.6 flagship",
|
||||
model: "kimi-k2.6",
|
||||
@@ -704,6 +739,16 @@ func TestGetFallbackPricing_FamilyMatching(t *testing.T) {
|
||||
{name: "doubao text embedding no fallback", model: "doubao-embedding-text-240515", expectNilPricing: true},
|
||||
{name: "hunyuan unknown no fallback", model: "hunyuan-t1", expectNilPricing: true},
|
||||
{name: "moonshot v1 not covered", model: "moonshot-v1-8k", expectNilPricing: true},
|
||||
// bare k3 仅精确/后缀匹配:相似未知型号不得因含 "k3" 误命中。
|
||||
{name: "k3-like unknown no fallback", model: "foo-k3-bar", expectNilPricing: true},
|
||||
// 路径最后一段不是 /k3:foo-k3 不得因 HasSuffix("/k3") 或 Contains 误命中。
|
||||
{name: "path segment not bare k3 no fallback", model: "vendor/foo-k3", expectNilPricing: true},
|
||||
// kimi-k3 非 Contains:kimi-k30 / 内嵌 foo-kimi-k3-bar 不得误命中。
|
||||
{name: "kimi-k30 unknown no fallback", model: "kimi-k30", expectNilPricing: true},
|
||||
{name: "embedded kimi-k3 unknown no fallback", model: "foo-kimi-k3-bar", expectNilPricing: true},
|
||||
// kimi-k3[1m] 是 Claude Code 上下文选择语法,不是 Kimi API 模型 ID,不命中 fallback。
|
||||
{name: "kimi-k3[1m] not an API model id no fallback", model: "kimi-k3[1m]", expectNilPricing: true},
|
||||
{name: "path kimi-k3[1m] not an API model id no fallback", model: "moonshot/kimi-k3[1m]", expectNilPricing: true},
|
||||
// kimi-k2-0905 / kimi-k2-0711 官方未公布独立价,走 kimi-k2 隐性回退(接受)——
|
||||
// 如未来官方公布独立价,需在 getFallbackPricing 加显式分支。
|
||||
{
|
||||
|
||||
@@ -60,14 +60,20 @@ var openAIOAuthForeignModelPrefixes = []string{
|
||||
|
||||
// isOpenAIOAuthServableModel 判断「空 model_mapping 的 OpenAI OAuth 账号」能否
|
||||
// 服务请求模型。空映射默认仍是「允许」,仅排除明确属于其他厂商家族的模型
|
||||
// (deepseek-*/glm-* 等)——这类请求原样透传必然被 Codex 上游以不可重试的
|
||||
// 400 拒绝,且不触发 failover,应在调度阶段就跳过该账号,把请求让给
|
||||
// 显式声明支持该模型的账号(#3662)。
|
||||
// (deepseek-*/glm-*、以及 Kimi Code 官方 bare ID k3 / k3-256k 等)——这类
|
||||
// 请求原样透传必然被 Codex 上游以不可重试的 400 拒绝,且不触发 failover,
|
||||
// 应在调度阶段就跳过该账号,把请求让给显式声明支持该模型的账号(#3662)。
|
||||
// bare k3 仅精确匹配(取 last segment 后),不使用宽泛 k3- 前缀,以免误伤
|
||||
// 自定义别名;显式 model_mapping 命中路径不经过本函数,语义不变。
|
||||
func isOpenAIOAuthServableModel(requestedModel string) bool {
|
||||
model := strings.ToLower(lastOpenAIModelSegment(requestedModel))
|
||||
if model == "" {
|
||||
return true // 空模型交由上层必填校验处理
|
||||
}
|
||||
// Kimi Code 官方 bare model ID:无厂商前缀,prefix 黑名单挡不住。
|
||||
if model == "k3" || model == "k3-256k" {
|
||||
return false
|
||||
}
|
||||
for _, prefix := range openAIOAuthForeignModelPrefixes {
|
||||
if strings.HasPrefix(model, prefix) {
|
||||
return false
|
||||
|
||||
@@ -49,6 +49,9 @@ func TestIsModelSupported_OpenAIOAuthEmptyMapping_RejectsForeignModels(t *testin
|
||||
"deepseek-chat",
|
||||
"glm-4.7",
|
||||
"kimi-k2",
|
||||
"k3", // Kimi Code bare ID(无厂商前缀,需精确拒绝)
|
||||
"k3-256k", // Kimi Code bare ID
|
||||
"provider/k3", // vendor/model 取 last segment 后仍为 k3
|
||||
"moonshot-v1-128k",
|
||||
"gemini-3.0-pro",
|
||||
"grok-4",
|
||||
@@ -65,11 +68,15 @@ func TestIsModelSupported_OpenAIOAuthEmptyMapping_RejectsForeignModels(t *testin
|
||||
func TestIsModelSupported_OpenAIOAuthExplicitMappingUnchanged(t *testing.T) {
|
||||
account := newOpenAIOAuthAccountForModelTest()
|
||||
account.Credentials = map[string]any{
|
||||
"model_mapping": map[string]any{"deepseek-v4": "gpt-5.4"},
|
||||
"model_mapping": map[string]any{
|
||||
"deepseek-v4": "gpt-5.4",
|
||||
"k3": "gpt-5.4", // 显式映射优先:bare k3 仍可被账号声明支持
|
||||
},
|
||||
}
|
||||
|
||||
// 显式映射沿用原有语义:命中映射即支持,未命中即不支持。
|
||||
require.True(t, account.IsModelSupported("deepseek-v4"))
|
||||
require.True(t, account.IsModelSupported("k3"))
|
||||
require.False(t, account.IsModelSupported("glm-4.7"))
|
||||
}
|
||||
|
||||
@@ -121,4 +128,8 @@ func TestIsOpenAIOAuthServableModel(t *testing.T) {
|
||||
require.False(t, isOpenAIOAuthServableModel("DeepSeek-V4")) // 大小写不敏感
|
||||
require.False(t, isOpenAIOAuthServableModel("qwen3-235b-thinking"))
|
||||
require.True(t, isOpenAIOAuthServableModel("deepseekcoder")) // 无连字符 → 非黑名单前缀,保持允许
|
||||
require.False(t, isOpenAIOAuthServableModel("k3"))
|
||||
require.False(t, isOpenAIOAuthServableModel("k3-256k"))
|
||||
require.False(t, isOpenAIOAuthServableModel("provider/k3"))
|
||||
require.True(t, isOpenAIOAuthServableModel("my-k3-alias")) // 非精确 bare ID,自定义别名 fail-open
|
||||
}
|
||||
|
||||
@@ -39,7 +39,8 @@ const (
|
||||
// 匹配规则按厂商前缀硬编码:
|
||||
// - anthropic-strict: claude-* / opus-* / sonnet-* / haiku-*
|
||||
// - passback-required: deepseek-* / kimi-* / moonshot-* / glm-* /
|
||||
// minimax-* / minimax-m* / (qwen-|qwen2-|qwen3-|qwen4-)*-thinking
|
||||
// minimax-* / minimax-m* / (qwen-|qwen2-|qwen3-|qwen4-)*-thinking /
|
||||
// Kimi Code bare aliases k3 / k3-256k(精确匹配,避免宽泛 k3 前缀)
|
||||
// - unknown: 其他模型(保守不剥离)
|
||||
//
|
||||
// 已知局限:前缀贪婪匹配(如 `claudette-`、`claude-foreign-relay-` 也会被分类为
|
||||
@@ -56,11 +57,15 @@ func ResolveThinkingProtocol(modelID string) ThinkingProtocol {
|
||||
id := strings.ToLower(modelID)
|
||||
|
||||
// Passback-required 优先匹配(特定厂商前缀),避免误判 claude-* 时也命中。
|
||||
// kimi-k3* 已由 kimi- 前缀覆盖;Kimi Code endpoint 的 bare model ID(k3 / k3-256k)
|
||||
// 无厂商前缀,仅精确匹配,避免 "foo-k3" 等未知型号被宽泛前缀误判。
|
||||
switch {
|
||||
case strings.HasPrefix(id, "deepseek-"),
|
||||
strings.HasPrefix(id, "kimi-"),
|
||||
strings.HasPrefix(id, "moonshot-"),
|
||||
strings.HasPrefix(id, "glm-"):
|
||||
strings.HasPrefix(id, "glm-"),
|
||||
id == "k3",
|
||||
id == "k3-256k":
|
||||
return ThinkingProtocolPassbackRequired
|
||||
}
|
||||
// MiniMax M 系列:走 https://api.minimax.io/anthropic 端点,
|
||||
|
||||
@@ -22,6 +22,9 @@ func TestResolveThinkingProtocol(t *testing.T) {
|
||||
{"deepseek-r2-thinking", "deepseek-r2-thinking", ThinkingProtocolPassbackRequired},
|
||||
{"kimi-coding", "kimi-coding-v2", ThinkingProtocolPassbackRequired},
|
||||
{"kimi-k2-thinking", "kimi-k2-thinking", ThinkingProtocolPassbackRequired},
|
||||
{"kimi-k3 platform", "kimi-k3", ThinkingProtocolPassbackRequired},
|
||||
{"kimi code bare k3", "k3", ThinkingProtocolPassbackRequired},
|
||||
{"kimi code bare k3-256k", "k3-256k", ThinkingProtocolPassbackRequired},
|
||||
{"moonshot-v1", "moonshot-v1-32k", ThinkingProtocolPassbackRequired},
|
||||
{"glm-5.1", "glm-5.1", ThinkingProtocolPassbackRequired},
|
||||
{"qwen-2 thinking variant", "qwen-2-72b-thinking", ThinkingProtocolPassbackRequired},
|
||||
@@ -44,6 +47,8 @@ func TestResolveThinkingProtocol(t *testing.T) {
|
||||
{"qwen3 non-thinking", "qwen3-32b", ThinkingProtocolUnknown},
|
||||
{"qwen2 non-thinking", "qwen-2-72b", ThinkingProtocolUnknown},
|
||||
{"random vendor", "yi-large", ThinkingProtocolUnknown},
|
||||
// 相似但未知的 k3 型号:不得因含 k3 被宽泛匹配为 passback-required
|
||||
{"k3-like unknown", "foo-k3-bar", ThinkingProtocolUnknown},
|
||||
// MiniMax 非 M 系列(如 abab、speech 等其他产品线)—— unknown
|
||||
{"minimax abab non-M", "abab6.5-chat", ThinkingProtocolUnknown},
|
||||
// Doubao 走 OpenAI 协议,不属于本网关 Anthropic 路径——归 unknown
|
||||
|
||||
@@ -127,6 +127,8 @@ security:
|
||||
- "api.openai.com"
|
||||
- "api.anthropic.com"
|
||||
- "api.kimi.com"
|
||||
- "api.moonshot.ai"
|
||||
- "api.moonshot.cn"
|
||||
- "open.bigmodel.cn"
|
||||
- "api.minimaxi.com"
|
||||
- "generativelanguage.googleapis.com"
|
||||
|
||||
Reference in New Issue
Block a user