feat: add Kimi K3 support

This commit is contained in:
alfadb
2026-07-28 10:08:39 +08:00
parent 1aeacf4d41
commit 7e65eafbe4
9 changed files with 120 additions and 7 deletions
+2
View File
@@ -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",
+15
View File
@@ -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) {
+18 -1
View File
@@ -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 / kimi-k3[1m](及 / 路径后缀);
// Code bare aliases 仅精确 k3 / k3-256k 或 /k3|/k3-256k 后缀,避免 kimi-k30 等未知型号误命中。
if strings.Contains(modelLower, "kimi-for-coding") {
return s.fallbackPrices["kimi-for-coding"]
}
if modelLower == "kimi-k3" || modelLower == "kimi-k3[1m]" ||
strings.HasSuffix(modelLower, "/kimi-k3") || strings.HasSuffix(modelLower, "/kimi-k3[1m]") ||
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,48 @@ 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 k3 anthropic 1m suffix",
model: "kimi-k3[1m]",
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[1m]",
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 +746,13 @@ 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-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"))
}
@@ -107,4 +114,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,10 @@ 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-k3 anthropic 1m", "kimi-k3[1m]", 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 +48,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
+2
View File
@@ -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"