Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
44da34d
feat: 添加LLM设置界面和DAG节点视觉升级
HalfAnElephant Mar 29, 2026
cbd9c73
Implement P0 structured ideation foundation
HalfAnElephant Mar 29, 2026
9378c9f
Merge branch 'c-ai-scientist-p0-foundation'
HalfAnElephant Mar 29, 2026
2b0ff12
feat(p1): add branch-aware search execution and live DAG branch telem…
HalfAnElephant Mar 29, 2026
7b7906b
feat(frontend): add interactive branch filter in live DAG rail
HalfAnElephant Mar 29, 2026
3e0cb18
feat(frontend): add branch evidence and conflict insight strip
HalfAnElephant Mar 29, 2026
c68ef4e
feat(frontend): add node-linked evidence preview in branch DAG rail
HalfAnElephant Mar 29, 2026
cceef29
feat(frontend): add node conflict preview in live DAG panel
HalfAnElephant Mar 29, 2026
81d4457
feat(frontend): link DAG node selection to progress event highlighting
HalfAnElephant Mar 29, 2026
1f7bfec
feat(frontend): add expandable node conflict detail cards
HalfAnElephant Mar 29, 2026
e217d52
feat(core): add scorecard-based completion gate and config contract u…
HalfAnElephant Mar 30, 2026
46602cc
feat(search-tree): persist branches and add repair retry loop
HalfAnElephant Mar 30, 2026
7d4abb2
feat(experiment): add run tracking and experiments API
HalfAnElephant Mar 30, 2026
6ab317b
feat(observability): add branch action/repair query APIs and frontend…
HalfAnElephant Mar 30, 2026
399d6dc
feat(frontend): show branch actions repairs and experiments in insigh…
HalfAnElephant Mar 30, 2026
56fd882
feat(frontend): add expandable branch trace details in live progress …
HalfAnElephant Mar 30, 2026
44d6e60
fix(tests): correct mock lambda signature for _resolve_provider
HalfAnElephant Mar 30, 2026
f8ba229
chore: apply code formatting and add uv.lock
HalfAnElephant Mar 30, 2026
1d0e54f
feat(frontend): add SettingsModal CSS styles
HalfAnElephant Mar 30, 2026
3cd6026
feat: implement comprehensive API configuration system
HalfAnElephant Mar 30, 2026
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
2 changes: 2 additions & 0 deletions backend/app/api/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
from app.api.routes.conversations import router as conversations_router
from app.api.routes.evidence import router as evidence_router
from app.api.routes.mcp import router as mcp_router
from app.api.routes.settings import router as settings_router
from app.api.routes.tasks import router as tasks_router

api_router = APIRouter()
api_router.include_router(tasks_router)
api_router.include_router(evidence_router)
api_router.include_router(mcp_router)
api_router.include_router(conversations_router)
api_router.include_router(settings_router)
173 changes: 173 additions & 0 deletions backend/app/api/routes/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
"""Settings API routes."""
from __future__ import annotations

from fastapi import APIRouter, HTTPException

from app.models.schemas import (
LLMOption,
LLMProvider,
LLMSettingsResponse,
ProviderConfigResponse,
ProviderConfigUpdate,
TaskMappingResponse,
TaskMappingUpdate,
)
from app.repositories.llm_config_repository import get_llm_config_repository

router = APIRouter(prefix="/api/v1/settings")

# Provider labels for display
PROVIDER_LABELS = {
LLMProvider.OPENROUTER: "OpenRouter",
LLMProvider.DEEPSEEK: "DeepSeek",
LLMProvider.OPENAI: "OpenAI",
}


@router.get("/llm", response_model=LLMSettingsResponse)
def get_llm_settings() -> LLMSettingsResponse:
"""Get available LLM configurations (legacy endpoint for compatibility)."""
repo = get_llm_config_repository()
providers = repo.list_providers()

options: list[LLMOption] = []
default_provider = LLMProvider.OPENROUTER

for config in providers:
provider_enum = LLMProvider(config.provider.lower())
options.append(LLMOption(
provider=provider_enum,
label=PROVIDER_LABELS.get(provider_enum, config.provider),
model=config.model,
configured=config.configured,
))
if config.is_default:
default_provider = provider_enum

return LLMSettingsResponse(defaultProvider=default_provider, options=options)


@router.get("/llm/providers", response_model=list[ProviderConfigResponse])
def list_provider_configs() -> list[ProviderConfigResponse]:
"""List all provider configurations with detailed info."""
repo = get_llm_config_repository()
providers = repo.list_providers()

result: list[ProviderConfigResponse] = []
for config in providers:
provider_enum = LLMProvider(config.provider.lower())
result.append(ProviderConfigResponse(
provider=provider_enum,
label=PROVIDER_LABELS.get(provider_enum, config.provider),
apiKey=config.mask_api_key(),
baseUrl=config.base_url,
model=config.model,
configured=config.configured,
isDefault=config.is_default,
))

return result


@router.get("/llm/providers/{provider}", response_model=ProviderConfigResponse)
def get_provider_config(provider: LLMProvider) -> ProviderConfigResponse:
"""Get configuration for a specific provider."""
repo = get_llm_config_repository()
config = repo.get_provider(provider.value)

if not config:
raise HTTPException(status_code=404, detail=f"Provider {provider} not found")

return ProviderConfigResponse(
provider=provider,
label=PROVIDER_LABELS.get(provider, provider.value),
apiKey=config.mask_api_key(),
baseUrl=config.base_url,
model=config.model,
configured=config.configured,
isDefault=config.is_default,
)


@router.put("/llm/providers/{provider}", response_model=ProviderConfigResponse)
def update_provider_config(provider: LLMProvider, update: ProviderConfigUpdate) -> ProviderConfigResponse:
"""Update configuration for a specific provider."""
repo = get_llm_config_repository()
existing = repo.get_provider(provider.value)

# Merge with existing config
from app.repositories.llm_config_repository import ProviderConfig as RepoConfig
new_config = RepoConfig(
provider=provider.value,
api_key=update.api_key if update.api_key is not None else existing.api_key,
base_url=update.baseUrl if update.baseUrl is not None else existing.base_url,
model=update.model if update.model is not None else existing.model,
is_default=update.isDefault if update.isDefault is not None else existing.is_default,
)

updated = repo.upsert_provider(new_config)

# If setting as default, update others
if update.isDefault:
repo.set_default_provider(provider.value)

return ProviderConfigResponse(
provider=provider,
label=PROVIDER_LABELS.get(provider, provider.value),
apiKey=updated.mask_api_key(),
baseUrl=updated.base_url,
model=updated.model,
configured=updated.configured,
isDefault=updated.is_default,
)


@router.delete("/llm/providers/{provider}")
def reset_provider_config(provider: LLMProvider) -> dict:
"""Reset provider configuration to environment defaults."""
repo = get_llm_config_repository()
repo.delete_provider(provider.value)
return {"status": "reset", "provider": provider.value}


@router.get("/llm/task-mapping", response_model=TaskMappingResponse)
def get_task_mapping() -> TaskMappingResponse:
"""Get task type to provider mapping."""
repo = get_llm_config_repository()
mapping = repo.get_task_mapping()

return TaskMappingResponse(
draft=LLMProvider(mapping.draft.lower()),
chat=LLMProvider(mapping.chat.lower()),
article=LLMProvider(mapping.article.lower()),
)


@router.put("/llm/task-mapping", response_model=TaskMappingResponse)
def update_task_mapping(update: TaskMappingUpdate) -> TaskMappingResponse:
"""Update task type to provider mapping."""
repo = get_llm_config_repository()
existing = repo.get_task_mapping()

from app.repositories.llm_config_repository import TaskMapping
new_mapping = TaskMapping(
draft=update.draft.value if update.draft else existing.draft,
chat=update.chat.value if update.chat else existing.chat,
article=update.article.value if update.article else existing.article,
)

updated = repo.update_task_mapping(new_mapping)

return TaskMappingResponse(
draft=LLMProvider(updated.draft.lower()),
chat=LLMProvider(updated.chat.lower()),
article=LLMProvider(updated.article.lower()),
)


@router.patch("/llm", response_model=LLMSettingsResponse)
def update_llm_settings(payload: dict) -> LLMSettingsResponse:
"""Update LLM settings (legacy endpoint for compatibility)."""
# For now, this just returns current settings
# In a full implementation, this would persist the default provider
return get_llm_settings()
Loading
Loading