From ebc101fef92518eb45394560f28dfc07a05380af Mon Sep 17 00:00:00 2001 From: duguwanglong Date: Mon, 20 Jul 2026 18:37:52 +0800 Subject: [PATCH 1/2] feat(provider): add Kimi K2.7 Code to ThreatBook CN Add the model catalog entry with pricing and token limits, make it the default ThreatBook CN model, and cover catalog, onboarding, bootstrap, and vision behavior. --- flocks/provider/catalog.json | 27 +++++++++++++++++++ flocks/server/routes/onboarding.py | 2 +- tests/provider/test_chinese_providers.py | 14 ++++++++++ tests/server/routes/test_onboarding_routes.py | 5 ++-- .../routes/test_provider_model_bootstrap.py | 2 ++ webui/src/hooks/useDefaultModelVision.test.ts | 13 +++++++++ webui/src/hooks/useDefaultModelVision.ts | 6 ++++- webui/src/pages/Model/index.tsx | 6 ++++- 8 files changed, 70 insertions(+), 5 deletions(-) diff --git a/flocks/provider/catalog.json b/flocks/provider/catalog.json index 363d1408d..b34a49806 100644 --- a/flocks/provider/catalog.json +++ b/flocks/provider/catalog.json @@ -52,6 +52,33 @@ "THREATBOOK_CN_LLM_API_KEY" ], "models": { + "kimi-k2.7-code": { + "name": "kimi-k2.7-code", + "family": "kimi-k2.7-code", + "capabilities": { + "supports_tools": true, + "supports_vision": true, + "supports_reasoning": true, + "interleaved": { + "field": "reasoning_content", + "echo": "tool_calls", + "placeholder": " ", + "cross_provider_policy": "placeholder" + }, + "supports_streaming": true + }, + "limits": { + "context_window": 256000, + "max_input_tokens": 224000, + "max_output_tokens": 16000 + }, + "pricing": { + "input": 6.5, + "output": 27.0, + "cache_read": 1.3, + "currency": "CNY" + } + }, "minimax-m3": { "name": "minimax-m3", "family": "minimax", diff --git a/flocks/server/routes/onboarding.py b/flocks/server/routes/onboarding.py index a94ba41c3..00abe7927 100644 --- a/flocks/server/routes/onboarding.py +++ b/flocks/server/routes/onboarding.py @@ -141,7 +141,7 @@ def _llm_provider_has_usable_credentials(provider_id: str) -> bool: "cn": { "activation_url": "https://x.threatbook.com/flocks/activate", "threatbook_llm_provider_id": "threatbook-cn-llm", - "threatbook_default_model_id": "minimax-m2.7", + "threatbook_default_model_id": "kimi-k2.7-code", "threatbook_api_service_id": "threatbook-cn", "threatbook_mcp_name": "threatbook_mcp", "threatbook_mcp_url": "https://mcp.threatbook.cn/mcp?apikey={api_key}", diff --git a/tests/provider/test_chinese_providers.py b/tests/provider/test_chinese_providers.py index 92a7b1b5e..e768d595c 100644 --- a/tests/provider/test_chinese_providers.py +++ b/tests/provider/test_chinese_providers.py @@ -253,6 +253,7 @@ def test_threatbook_cn_llm_catalog(self): assert get_provider_default_url("threatbook-cn-llm") == "https://llm.threatbook.cn/v1" models = get_provider_model_definitions("threatbook-cn-llm") assert {m.id for m in models} == { + "kimi-k2.7-code", "minimax-m3", "minimax-m2.7", "minimax-m2.5", @@ -263,6 +264,19 @@ def test_threatbook_cn_llm_catalog(self): "deepseek-v4-flash", } + kimi_code = next(m for m in models if m.id == "kimi-k2.7-code") + assert models[0].id == "kimi-k2.7-code" + assert kimi_code.capabilities.supports_vision is True + assert kimi_code.capabilities.supports_reasoning is True + assert kimi_code.capabilities.interleaved["field"] == "reasoning_content" + assert kimi_code.pricing.currency == "CNY" + assert kimi_code.pricing.cache_read == 1.3 + assert kimi_code.pricing.input == 6.5 + assert kimi_code.pricing.output == 27.0 + assert kimi_code.limits.context_window == 256000 + assert kimi_code.limits.max_input_tokens == 224000 + assert kimi_code.limits.max_output_tokens == 16000 + qwen = next(m for m in models if m.id == "qwen3.6-plus") assert qwen.capabilities.supports_vision is True diff --git a/tests/server/routes/test_onboarding_routes.py b/tests/server/routes/test_onboarding_routes.py index f2b812a48..7d317865d 100644 --- a/tests/server/routes/test_onboarding_routes.py +++ b/tests/server/routes/test_onboarding_routes.py @@ -277,7 +277,7 @@ async def fake_connect_mcp(name: str): return True async def fake_set_default_model(model_type, body): - calls.append(("default_model", body.provider_id)) + calls.append(("default_model", body.provider_id, body.model_id)) return {"provider_id": body.provider_id, "model_id": body.model_id} monkeypatch.setattr(onboarding_routes, "_validate_onboarding_request", fake_validate) @@ -302,13 +302,14 @@ async def fake_set_default_model(model_type, body): data = resp.json() assert data["success"] is True assert data["default_model"]["provider_id"] == "threatbook-cn-llm" + assert data["default_model"]["model_id"] == "kimi-k2.7-code" assert ("provider", "threatbook-cn-llm") in calls assert ("service", "threatbook-cn") in calls assert ("service_enabled", "threatbook-cn") in calls assert ("ensure_mcp", "cn") in calls assert ("mcp_credentials", "threatbook_mcp") in calls assert ("mcp_connect", "threatbook_mcp") in calls - assert ("default_model", "threatbook-cn-llm") in calls + assert ("default_model", "threatbook-cn-llm", "kimi-k2.7-code") in calls @pytest.mark.asyncio async def test_apply_returns_400_when_validation_fails( diff --git a/tests/server/routes/test_provider_model_bootstrap.py b/tests/server/routes/test_provider_model_bootstrap.py index aaee5869c..5ab24fae7 100644 --- a/tests/server/routes/test_provider_model_bootstrap.py +++ b/tests/server/routes/test_provider_model_bootstrap.py @@ -31,6 +31,8 @@ async def test_set_provider_credentials_bootstraps_kimi_k26_from_catalog( raw = ConfigWriter.get_provider_raw("threatbook-cn-llm") assert raw is not None + assert "kimi-k2.7-code" in raw["models"] + assert raw["models"]["kimi-k2.7-code"]["name"] == "kimi-k2.7-code" assert "kimi-k2.6" in raw["models"] assert raw["models"]["kimi-k2.6"]["name"] == "kimi-k2.6" fake_secrets.set.assert_called_once_with("threatbook-cn-llm_llm_key", "tb-key") diff --git a/webui/src/hooks/useDefaultModelVision.test.ts b/webui/src/hooks/useDefaultModelVision.test.ts index a0c37fb80..90fe0e341 100644 --- a/webui/src/hooks/useDefaultModelVision.test.ts +++ b/webui/src/hooks/useDefaultModelVision.test.ts @@ -93,6 +93,19 @@ describe('useDefaultModelVision', () => { await waitFor(() => expect(result.current).toBe(true)); }); + it('returns true for the predefined kimi-k2.7-code model', async () => { + mockResolved.mockResolvedValue(makeResolvedResp('threatbook-cn-llm', 'kimi-k2.7-code')); + mockDefinitions.mockResolvedValue(makeDefinitionsResp( + { supports_vision: true }, + 'predefined', + 'threatbook-cn-llm', + 'kimi-k2.7-code', + )); + + const { result } = renderHook(() => useDefaultModelVision()); + await waitFor(() => expect(result.current).toBe(true)); + }); + it('returns null when capabilities are absent', async () => { mockResolved.mockResolvedValue(makeResolvedResp()); mockDefinitions.mockResolvedValue({ diff --git a/webui/src/hooks/useDefaultModelVision.ts b/webui/src/hooks/useDefaultModelVision.ts index 90e980597..44f37edfa 100644 --- a/webui/src/hooks/useDefaultModelVision.ts +++ b/webui/src/hooks/useDefaultModelVision.ts @@ -46,7 +46,11 @@ const subscribers = new Set<(state: VisionState) => void>(); function allowsBuiltInVision(modelId: string): boolean { const lowered = modelId.toLowerCase(); - return lowered.includes('qwen3.6-plus') || lowered.includes('kimi-k2.6'); + return ( + lowered.includes('qwen3.6-plus') + || lowered.includes('kimi-k2.6') + || lowered.includes('kimi-k2.7-code') + ); } function visionSupportFromDefinition( diff --git a/webui/src/pages/Model/index.tsx b/webui/src/pages/Model/index.tsx index 298bad220..b34dc23f1 100644 --- a/webui/src/pages/Model/index.tsx +++ b/webui/src/pages/Model/index.tsx @@ -2190,7 +2190,11 @@ function getDefaultReasoningToggleValue(providerId: string, modelId: string): bo function allowsBuiltInVisionToggle(modelId: string): boolean { const lowered = modelId.toLowerCase(); - return lowered.includes('qwen3.6-plus') || lowered.includes('kimi-k2.6'); + return ( + lowered.includes('qwen3.6-plus') + || lowered.includes('kimi-k2.6') + || lowered.includes('kimi-k2.7-code') + ); } // ==================== Configure Dialog ==================== From 996595009625ff016bc3885bdd7e7ee731459826 Mon Sep 17 00:00:00 2001 From: duguwanglong Date: Mon, 20 Jul 2026 18:43:42 +0800 Subject: [PATCH 2/2] feat(provider): add Kimi K2.7 Code to ThreatBook IO Add the international catalog entry with matching pricing and token limits, make it the default global ThreatBook model, and cover catalog ordering and regional defaults. --- flocks/provider/catalog.json | 27 +++++++++++++++++++ flocks/server/routes/onboarding.py | 2 +- tests/provider/test_chinese_providers.py | 14 ++++++++++ tests/server/routes/test_onboarding_routes.py | 8 ++++++ 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/flocks/provider/catalog.json b/flocks/provider/catalog.json index b34a49806..bef32dde4 100644 --- a/flocks/provider/catalog.json +++ b/flocks/provider/catalog.json @@ -289,6 +289,33 @@ "THREATBOOK_IO_LLM_API_KEY" ], "models": { + "kimi-k2.7-code": { + "name": "kimi-k2.7-code", + "family": "kimi-k2.7-code", + "capabilities": { + "supports_tools": true, + "supports_vision": true, + "supports_reasoning": true, + "interleaved": { + "field": "reasoning_content", + "echo": "tool_calls", + "placeholder": " ", + "cross_provider_policy": "placeholder" + }, + "supports_streaming": true + }, + "limits": { + "context_window": 256000, + "max_input_tokens": 224000, + "max_output_tokens": 16000 + }, + "pricing": { + "input": 6.5, + "output": 27.0, + "cache_read": 1.3, + "currency": "CNY" + } + }, "minimax-m3": { "name": "minimax-m3", "family": "minimax", diff --git a/flocks/server/routes/onboarding.py b/flocks/server/routes/onboarding.py index 00abe7927..7ccb2a380 100644 --- a/flocks/server/routes/onboarding.py +++ b/flocks/server/routes/onboarding.py @@ -151,7 +151,7 @@ def _llm_provider_has_usable_credentials(provider_id: str) -> bool: "global": { "activation_url": "https://threatbook.io/flocks/activate", "threatbook_llm_provider_id": "threatbook-io-llm", - "threatbook_default_model_id": "minimax-m2.7", + "threatbook_default_model_id": "kimi-k2.7-code", "threatbook_api_service_id": "threatbook-io", "threatbook_mcp_name": None, "threatbook_mcp_url": None, diff --git a/tests/provider/test_chinese_providers.py b/tests/provider/test_chinese_providers.py index e768d595c..f60060143 100644 --- a/tests/provider/test_chinese_providers.py +++ b/tests/provider/test_chinese_providers.py @@ -311,6 +311,7 @@ def test_threatbook_io_llm_catalog(self): assert get_provider_default_url("threatbook-io-llm") == "https://llm.threatbook.io/v1" models = get_provider_model_definitions("threatbook-io-llm") assert {m.id for m in models} == { + "kimi-k2.7-code", "minimax-m3", "minimax-m2.7", "minimax-m2.5", @@ -320,6 +321,19 @@ def test_threatbook_io_llm_catalog(self): "deepseek-v4-flash", } + kimi_code = next(m for m in models if m.id == "kimi-k2.7-code") + assert models[0].id == "kimi-k2.7-code" + assert kimi_code.capabilities.supports_vision is True + assert kimi_code.capabilities.supports_reasoning is True + assert kimi_code.capabilities.interleaved["field"] == "reasoning_content" + assert kimi_code.pricing.currency == "CNY" + assert kimi_code.pricing.cache_read == 1.3 + assert kimi_code.pricing.input == 6.5 + assert kimi_code.pricing.output == 27.0 + assert kimi_code.limits.context_window == 256000 + assert kimi_code.limits.max_input_tokens == 224000 + assert kimi_code.limits.max_output_tokens == 16000 + qwen = next(m for m in models if m.id == "qwen3.6-plus") assert qwen.capabilities.supports_vision is True diff --git a/tests/server/routes/test_onboarding_routes.py b/tests/server/routes/test_onboarding_routes.py index 7d317865d..53675b541 100644 --- a/tests/server/routes/test_onboarding_routes.py +++ b/tests/server/routes/test_onboarding_routes.py @@ -198,6 +198,14 @@ async def fake_test_mcp(region: str, api_key: str): class TestOnboardingApplyRoutes: + def test_threatbook_region_presets_use_kimi_k27_code(self): + assert onboarding_routes.ONBOARDING_REGION_PRESETS["cn"]["threatbook_default_model_id"] == ( + "kimi-k2.7-code" + ) + assert onboarding_routes.ONBOARDING_REGION_PRESETS["global"]["threatbook_default_model_id"] == ( + "kimi-k2.7-code" + ) + def test_ensure_threatbook_mcp_config_uses_explicit_secret_reference( self, monkeypatch: pytest.MonkeyPatch ):