diff --git a/flocks/provider/catalog.json b/flocks/provider/catalog.json index 363d1408..bef32dde 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", @@ -262,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 a94ba41c..7ccb2a38 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}", @@ -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 92a7b1b5..f6006014 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 @@ -297,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", @@ -306,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 f2b812a4..53675b54 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 ): @@ -277,7 +285,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 +310,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 aaee5869..5ab24fae 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 a0c37fb8..90fe0e34 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 90e98059..44f37edf 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 298bad22..b34dc23f 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 ====================