Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions flocks/provider/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions flocks/server/routes/onboarding.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand All @@ -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,
Expand Down
28 changes: 28 additions & 0 deletions tests/provider/test_chinese_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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

Expand Down Expand Up @@ -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",
Expand All @@ -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

Expand Down
13 changes: 11 additions & 2 deletions tests/server/routes/test_onboarding_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
):
Expand Down Expand Up @@ -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)
Expand All @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions tests/server/routes/test_provider_model_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
13 changes: 13 additions & 0 deletions webui/src/hooks/useDefaultModelVision.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
6 changes: 5 additions & 1 deletion webui/src/hooks/useDefaultModelVision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 5 additions & 1 deletion webui/src/pages/Model/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ====================
Expand Down