local-ai-runtime 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. backend/__init__.py +0 -0
  2. backend/controllers/__init__.py +0 -0
  3. backend/controllers/chat_controller.py +42 -0
  4. backend/controllers/model_controller.py +15 -0
  5. backend/models/__init__.py +2 -0
  6. backend/models/chat_message.py +80 -0
  7. backend/models/model_config.py +145 -0
  8. backend/routes/__init__.py +0 -0
  9. backend/routes/chat.py +92 -0
  10. backend/routes/config.py +75 -0
  11. backend/routes/conversations.py +76 -0
  12. backend/routes/metrics.py +11 -0
  13. backend/routes/models.py +29 -0
  14. backend/routes/status.py +24 -0
  15. backend/server.py +41 -0
  16. backend/services/__init__.py +0 -0
  17. backend/services/conversation_service.py +76 -0
  18. backend/services/inference_service.py +189 -0
  19. backend/services/metrics_service.py +36 -0
  20. backend/services/model_service.py +88 -0
  21. local_ai_runtime/__init__.py +2 -0
  22. local_ai_runtime/cli.py +31 -0
  23. local_ai_runtime/config.py +44 -0
  24. local_ai_runtime/server.py +68 -0
  25. local_ai_runtime-0.1.0.dist-info/METADATA +29 -0
  26. local_ai_runtime-0.1.0.dist-info/RECORD +47 -0
  27. local_ai_runtime-0.1.0.dist-info/WHEEL +5 -0
  28. local_ai_runtime-0.1.0.dist-info/entry_points.txt +2 -0
  29. local_ai_runtime-0.1.0.dist-info/licenses/LICENSE +21 -0
  30. local_ai_runtime-0.1.0.dist-info/top_level.txt +3 -0
  31. model_runtime/__init__.py +1 -0
  32. model_runtime/backends/__init__.py +22 -0
  33. model_runtime/backends/anthropic_backend.py +130 -0
  34. model_runtime/backends/base.py +71 -0
  35. model_runtime/backends/gemini_backend.py +133 -0
  36. model_runtime/backends/llama_cpp.py +151 -0
  37. model_runtime/backends/ollama.py +139 -0
  38. model_runtime/backends/openai_backend.py +110 -0
  39. model_runtime/backends/openai_compatible.py +117 -0
  40. model_runtime/backends/registry.py +94 -0
  41. model_runtime/backends/vllm.py +171 -0
  42. model_runtime/chat_template_auto.py +78 -0
  43. model_runtime/detector.py +219 -0
  44. model_runtime/inference_engine.py +208 -0
  45. model_runtime/model_loader.py +69 -0
  46. model_runtime/prompt_formatter.py +149 -0
  47. model_runtime/tokenizer_adapter.py +58 -0
backend/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,42 @@
1
+ """Controller for chat request handling."""
2
+
3
+ from fastapi import HTTPException
4
+
5
+ from services.inference_service import generate_response, InferenceError
6
+ from models.chat_message import ChatMessage, Role
7
+ from services.conversation_service import get_conversation
8
+
9
+
10
+ async def handle_chat_request(request) -> dict:
11
+ user_message = ChatMessage(role=Role.USER, content=request.message)
12
+
13
+ conversation_history = None
14
+ if request.conversation_id:
15
+ conv = get_conversation(request.conversation_id)
16
+ if conv:
17
+ conversation_history = [
18
+ ChatMessage(role=Role(m["role"]), content=m["content"])
19
+ for m in conv["messages"]
20
+ ]
21
+
22
+ try:
23
+ result = await generate_response(
24
+ message=user_message,
25
+ conversation_id=request.conversation_id,
26
+ conversation_history=conversation_history,
27
+ )
28
+ except InferenceError as e:
29
+ raise HTTPException(status_code=500, detail=str(e))
30
+
31
+ # Auto-save user message and assistant response to conversation
32
+ if request.conversation_id:
33
+ from services.conversation_service import add_message_to_conversation
34
+ add_message_to_conversation(request.conversation_id, "user", request.message)
35
+ add_message_to_conversation(request.conversation_id, "assistant", result.content)
36
+
37
+ return {
38
+ "response": result.content,
39
+ "model": result.model_name,
40
+ "tokens_used": result.tokens_used,
41
+ "finish_reason": result.finish_reason,
42
+ }
@@ -0,0 +1,15 @@
1
+ """Controller for model management request handling."""
2
+
3
+ from services.model_service import scan_model_directory, get_active_config
4
+
5
+
6
+ def list_available_models() -> dict:
7
+ models = scan_model_directory()
8
+ return {"models": models}
9
+
10
+
11
+ def get_active_model() -> dict:
12
+ config = get_active_config()
13
+ if config is None:
14
+ return {"active_model": None}
15
+ return config.to_dict()
@@ -0,0 +1,2 @@
1
+ from models.chat_message import ChatMessage, Conversation, Role
2
+ from models.model_config import ModelConfig, GenerationParams, CHAT_TEMPLATES
@@ -0,0 +1,80 @@
1
+ # backend/models/chat_message.py
2
+ #
3
+ # Data model for a single chat message.
4
+ #
5
+ # STATUS: DEFINED — This can be used now even before inference works.
6
+ # It's a plain data class with no dependencies.
7
+ #
8
+ # Usage:
9
+ # from models.chat_message import ChatMessage, Role
10
+ # msg = ChatMessage(role=Role.USER, content="Hello!")
11
+
12
+ from enum import Enum
13
+ from datetime import datetime, timezone
14
+
15
+
16
+ class Role(str, Enum):
17
+ """
18
+ The role of the entity producing a message.
19
+ Mirrors the convention used in most chat template formats.
20
+ """
21
+ SYSTEM = "system"
22
+ USER = "user"
23
+ ASSISTANT = "assistant"
24
+
25
+ # Future: tool call results
26
+ # TOOL = "tool"
27
+
28
+
29
+ class ChatMessage:
30
+ """
31
+ Represents a single message in a conversation.
32
+
33
+ Attributes:
34
+ role (Role) — Who produced this message
35
+ content (str) — The text content
36
+ timestamp (datetime) — When this message was created (UTC)
37
+ id (str|None) — Optional unique ID for this message
38
+ """
39
+
40
+ def __init__(self, role: Role, content: str, timestamp: datetime | None = None, id: str | None = None):
41
+ self.role = role
42
+ self.content = content
43
+ self.timestamp = timestamp or datetime.now(timezone.utc)
44
+ self.id = id
45
+
46
+ def to_dict(self) -> dict:
47
+ return {
48
+ "role": self.role.value,
49
+ "content": self.content,
50
+ "timestamp": self.timestamp.isoformat(),
51
+ "id": self.id,
52
+ }
53
+
54
+ def __repr__(self) -> str:
55
+ preview = self.content[:60] + "..." if len(self.content) > 60 else self.content
56
+ return f"ChatMessage(role={self.role.value}, content='{preview}')"
57
+
58
+
59
+ class Conversation:
60
+ """
61
+ A list of ChatMessages forming a conversation thread.
62
+
63
+ TODO (Phase 4.1): Add persistence, conversation ID, title generation.
64
+ """
65
+
66
+ def __init__(self, id: str | None = None):
67
+ self.id = id
68
+ self.messages: list[ChatMessage] = []
69
+
70
+ def add_message(self, message: ChatMessage) -> None:
71
+ self.messages.append(message)
72
+
73
+ def get_messages(self) -> list[ChatMessage]:
74
+ return self.messages
75
+
76
+ def to_dict(self) -> dict:
77
+ return {
78
+ "id": self.id,
79
+ "messages": [m.to_dict() for m in self.messages],
80
+ }
@@ -0,0 +1,145 @@
1
+ """Data models for model configuration and inference I/O."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Optional
5
+
6
+
7
+ CHAT_TEMPLATES = {
8
+ "auto",
9
+ "chatml",
10
+ "llama3",
11
+ "llama2",
12
+ "mistral",
13
+ "alpaca",
14
+ "raw",
15
+ }
16
+
17
+ BACKEND_TYPES = {
18
+ "llama-cpp",
19
+ "ollama",
20
+ "vllm",
21
+ "openai",
22
+ "anthropic",
23
+ "gemini",
24
+ "openai_compatible",
25
+ }
26
+
27
+
28
+ @dataclass
29
+ class GenerationParams:
30
+ """Parameters controlling text generation."""
31
+ temperature: float = 0.7
32
+ top_p: float = 0.9
33
+ top_k: int = 40
34
+ repeat_penalty: float = 1.1
35
+ max_new_tokens: int = 512
36
+ min_new_tokens: int = 1
37
+ seed: int = -1
38
+
39
+ def to_dict(self) -> dict:
40
+ return {
41
+ "temperature": self.temperature,
42
+ "top_p": self.top_p,
43
+ "top_k": self.top_k,
44
+ "repeat_penalty": self.repeat_penalty,
45
+ "max_new_tokens": self.max_new_tokens,
46
+ "min_new_tokens": self.min_new_tokens,
47
+ "seed": self.seed,
48
+ }
49
+
50
+
51
+ @dataclass
52
+ class ModelConfig:
53
+ """Complete configuration for one model — all from config, no hardcoded defaults."""
54
+ model_file: str = ""
55
+ backend_type: str = "llama-cpp"
56
+ chat_template: str = "auto"
57
+ context_length: int = 4096
58
+ system_prompt: str = "You are a helpful assistant."
59
+ n_gpu_layers: int = 0
60
+ generation: GenerationParams = field(default_factory=GenerationParams)
61
+ profile_name: Optional[str] = None
62
+ models_dir: str = "./models"
63
+ # API backend overrides
64
+ openai_model: str = ""
65
+ anthropic_model: str = ""
66
+ gemini_model: str = ""
67
+ ollama_model: str = ""
68
+ vllm_model: str = ""
69
+ compatible_model: str = ""
70
+
71
+ def to_dict(self) -> dict:
72
+ return {
73
+ "profile_name": self.profile_name,
74
+ "backend_type": self.backend_type,
75
+ "model_file": self.model_file,
76
+ "chat_template": self.chat_template,
77
+ "context_length": self.context_length,
78
+ "system_prompt": self.system_prompt,
79
+ "n_gpu_layers": self.n_gpu_layers,
80
+ "models_dir": self.models_dir,
81
+ "generation": self.generation.to_dict(),
82
+ "openai_model": self.openai_model,
83
+ "anthropic_model": self.anthropic_model,
84
+ "gemini_model": self.gemini_model,
85
+ "ollama_model": self.ollama_model,
86
+ "vllm_model": self.vllm_model,
87
+ "compatible_model": self.compatible_model,
88
+ }
89
+
90
+ @classmethod
91
+ def from_dict(cls, data: dict) -> "ModelConfig":
92
+ gen_data = data.get("generation", {})
93
+ generation = GenerationParams(
94
+ temperature=gen_data.get("temperature", 0.7),
95
+ top_p=gen_data.get("top_p", 0.9),
96
+ top_k=gen_data.get("top_k", 40),
97
+ repeat_penalty=gen_data.get("repeat_penalty", 1.1),
98
+ max_new_tokens=gen_data.get("max_new_tokens", 512),
99
+ seed=gen_data.get("seed", -1),
100
+ )
101
+ return cls(
102
+ model_file=data.get("model_file", ""),
103
+ backend_type=data.get("backend_type", "llama-cpp"),
104
+ chat_template=data.get("chat_template", "auto"),
105
+ context_length=data.get("context_length", 4096),
106
+ system_prompt=data.get("system_prompt", "You are a helpful assistant."),
107
+ n_gpu_layers=data.get("n_gpu_layers", 0),
108
+ generation=generation,
109
+ profile_name=data.get("profile_name"),
110
+ models_dir=data.get("models_dir", "./models"),
111
+ openai_model=data.get("openai_model", ""),
112
+ anthropic_model=data.get("anthropic_model", ""),
113
+ gemini_model=data.get("gemini_model", ""),
114
+ ollama_model=data.get("ollama_model", ""),
115
+ vllm_model=data.get("vllm_model", ""),
116
+ compatible_model=data.get("compatible_model", ""),
117
+ )
118
+
119
+
120
+ @dataclass
121
+ class InferenceRequest:
122
+ """Internal representation of an inference request."""
123
+ prompt: str
124
+ system_prompt: str = ""
125
+ max_tokens: int = 512
126
+ temperature: float = 0.7
127
+ top_p: float = 0.9
128
+ top_k: int = 40
129
+ repeat_penalty: float = 1.1
130
+ seed: int = -1
131
+ stop_tokens: list[str] = field(default_factory=list)
132
+
133
+
134
+ @dataclass
135
+ class InferenceResponse:
136
+ """Internal representation of an inference response."""
137
+ text: str
138
+ tokens_prompt: int = 0
139
+ tokens_generated: int = 0
140
+ finish_reason: str = "stop"
141
+ model_name: str = ""
142
+
143
+ @property
144
+ def tokens_used(self) -> int:
145
+ return self.tokens_prompt + self.tokens_generated
File without changes
backend/routes/chat.py ADDED
@@ -0,0 +1,92 @@
1
+ """POST /chat and POST /chat/stream — Chat endpoints with streaming."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ _project_root = str(Path(__file__).resolve().parent.parent.parent)
7
+ if _project_root not in sys.path:
8
+ sys.path.insert(0, _project_root)
9
+
10
+ import json
11
+ from fastapi import APIRouter, HTTPException
12
+ from fastapi.responses import StreamingResponse
13
+ from pydantic import BaseModel
14
+
15
+ from controllers.chat_controller import handle_chat_request
16
+
17
+ router = APIRouter()
18
+
19
+
20
+ class ChatRequest(BaseModel):
21
+ message: str
22
+ conversation_id: str | None = None
23
+
24
+
25
+ class ChatResponse(BaseModel):
26
+ response: str
27
+ model: str
28
+ tokens_used: int
29
+ finish_reason: str = "stop"
30
+
31
+
32
+ @router.post("/", response_model=ChatResponse)
33
+ async def chat(request: ChatRequest):
34
+ """Send a message and receive a response from any backend."""
35
+ if not request.message.strip():
36
+ raise HTTPException(status_code=400, detail="Message cannot be empty.")
37
+ return await handle_chat_request(request)
38
+
39
+
40
+ @router.post("/stream")
41
+ async def chat_stream(request: ChatRequest):
42
+ """Stream a response token-by-token using SSE."""
43
+ if not request.message.strip():
44
+ raise HTTPException(status_code=400, detail="Message cannot be empty.")
45
+
46
+ from models.chat_message import ChatMessage, Role
47
+ from services.inference_service import generate_stream_response
48
+ from services.conversation_service import (
49
+ get_conversation,
50
+ add_message_to_conversation,
51
+ )
52
+
53
+ conversation_history = None
54
+ if request.conversation_id:
55
+ conv = get_conversation(request.conversation_id)
56
+ if conv:
57
+ conversation_history = [
58
+ ChatMessage(role=Role(m["role"]), content=m["content"])
59
+ for m in conv["messages"]
60
+ ]
61
+
62
+ user_message = ChatMessage(role=Role.USER, content=request.message)
63
+
64
+ def event_generator():
65
+ full_response = []
66
+ try:
67
+ for token in generate_stream_response(
68
+ message=user_message,
69
+ conversation_history=conversation_history,
70
+ ):
71
+ full_response.append(token)
72
+ yield f"data: {json.dumps({'token': token, 'done': False})}\n\n"
73
+ except Exception as e:
74
+ yield f"data: {json.dumps({'error': str(e), 'done': True})}\n\n"
75
+ return
76
+
77
+ # Save to conversation
78
+ if request.conversation_id:
79
+ add_message_to_conversation(request.conversation_id, "user", request.message)
80
+ add_message_to_conversation(request.conversation_id, "assistant", "".join(full_response))
81
+
82
+ yield f"data: {json.dumps({'token': '', 'done': True})}\n\n"
83
+
84
+ return StreamingResponse(
85
+ event_generator(),
86
+ media_type="text/event-stream",
87
+ headers={
88
+ "Cache-Control": "no-cache",
89
+ "Connection": "keep-alive",
90
+ "X-Accel-Buffering": "no",
91
+ },
92
+ )
@@ -0,0 +1,75 @@
1
+ """GET /config and PUT /config — Read/update active model configuration."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ _project_root = str(Path(__file__).resolve().parent.parent.parent)
7
+ if _project_root not in sys.path:
8
+ sys.path.insert(0, _project_root)
9
+
10
+ from fastapi import APIRouter, HTTPException
11
+ from pydantic import BaseModel
12
+
13
+ from models.model_config import ModelConfig, GenerationParams
14
+ from services.model_service import get_active_config, set_active_config, scan_model_directory
15
+
16
+ router = APIRouter()
17
+
18
+
19
+ class ConfigUpdate(BaseModel):
20
+ backend_type: str | None = None
21
+ model_file: str | None = None
22
+ chat_template: str | None = None
23
+ context_length: int | None = None
24
+ system_prompt: str | None = None
25
+ n_gpu_layers: int | None = None
26
+ generation: dict | None = None
27
+ ollama_model: str | None = None
28
+ vllm_model: str | None = None
29
+
30
+
31
+ @router.get("/")
32
+ async def get_config():
33
+ config = get_active_config()
34
+ if config is None:
35
+ raise HTTPException(status_code=404, detail="No active model configuration.")
36
+ return config.to_dict()
37
+
38
+
39
+ @router.put("/")
40
+ async def update_config(update: ConfigUpdate):
41
+ config = get_active_config()
42
+ if config is None:
43
+ # Create new config from defaults
44
+ config = ModelConfig(model_file="")
45
+
46
+ if update.backend_type is not None:
47
+ config.backend_type = update.backend_type
48
+ if update.model_file is not None:
49
+ models = scan_model_directory()
50
+ filenames = [m["filename"] for m in models]
51
+ if update.model_file not in filenames:
52
+ raise HTTPException(status_code=404, detail=f"Model not found: {update.model_file}")
53
+ config.model_file = update.model_file
54
+ if update.chat_template is not None:
55
+ config.chat_template = update.chat_template
56
+ if update.context_length is not None:
57
+ config.context_length = update.context_length
58
+ if update.system_prompt is not None:
59
+ config.system_prompt = update.system_prompt
60
+ if update.n_gpu_layers is not None:
61
+ config.n_gpu_layers = update.n_gpu_layers
62
+ if update.generation is not None:
63
+ for key, val in update.generation.items():
64
+ if hasattr(config.generation, key):
65
+ setattr(config.generation, key, val)
66
+
67
+ set_active_config(config)
68
+ return {"message": "Config updated", "config": config.to_dict()}
69
+
70
+
71
+ @router.get("/backends")
72
+ async def list_backends():
73
+ """List all available BYOK backends and their status."""
74
+ from model_runtime.backends import list_backends
75
+ return {"backends": list_backends()}
@@ -0,0 +1,76 @@
1
+ """Conversation history routes with full CRUD."""
2
+
3
+ from fastapi import APIRouter, HTTPException
4
+ from pydantic import BaseModel
5
+ from services.conversation_service import (
6
+ create_conversation,
7
+ get_conversation,
8
+ list_conversations,
9
+ delete_conversation,
10
+ add_message_to_conversation,
11
+ update_conversation_title,
12
+ )
13
+
14
+ router = APIRouter()
15
+
16
+
17
+ class ConversationCreate(BaseModel):
18
+ title: str | None = None
19
+
20
+
21
+ class ConversationRename(BaseModel):
22
+ title: str
23
+
24
+
25
+ class MessageAdd(BaseModel):
26
+ role: str
27
+ content: str
28
+
29
+
30
+ @router.get("/")
31
+ async def get_all_conversations():
32
+ """List all conversations, sorted by most recent."""
33
+ return {"conversations": list_conversations()}
34
+
35
+
36
+ @router.post("/")
37
+ async def create_new_conversation(body: ConversationCreate):
38
+ """Create a new conversation."""
39
+ conv = create_conversation(title=body.title)
40
+ return conv
41
+
42
+
43
+ @router.get("/{conversation_id}")
44
+ async def get_conversation_detail(conversation_id: str):
45
+ """Get a conversation with all messages."""
46
+ conv = get_conversation(conversation_id)
47
+ if conv is None:
48
+ raise HTTPException(status_code=404, detail="Conversation not found.")
49
+ return conv
50
+
51
+
52
+ @router.patch("/{conversation_id}")
53
+ async def rename_conversation(conversation_id: str, body: ConversationRename):
54
+ """Rename a conversation."""
55
+ result = update_conversation_title(conversation_id, body.title)
56
+ if result is None:
57
+ raise HTTPException(status_code=404, detail="Conversation not found.")
58
+ return result
59
+
60
+
61
+ @router.delete("/{conversation_id}")
62
+ async def delete_conversation_route(conversation_id: str):
63
+ """Delete a conversation."""
64
+ ok = delete_conversation(conversation_id)
65
+ if not ok:
66
+ raise HTTPException(status_code=404, detail="Conversation not found.")
67
+ return {"message": "Deleted"}
68
+
69
+
70
+ @router.post("/{conversation_id}/messages")
71
+ async def add_message(conversation_id: str, body: MessageAdd):
72
+ """Add a message to a conversation."""
73
+ msg = add_message_to_conversation(conversation_id, body.role, body.content)
74
+ if msg is None:
75
+ raise HTTPException(status_code=404, detail="Conversation not found.")
76
+ return msg
@@ -0,0 +1,11 @@
1
+ """GET /metrics -- Performance monitoring endpoint."""
2
+
3
+ from fastapi import APIRouter
4
+ from services.metrics_service import get_metrics
5
+
6
+ router = APIRouter()
7
+
8
+
9
+ @router.get("/")
10
+ async def metrics():
11
+ return get_metrics()
@@ -0,0 +1,29 @@
1
+ """GET /models — List available GGUF model files with auto-detected metadata."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ _project_root = str(Path(__file__).resolve().parent.parent.parent)
7
+ if _project_root not in sys.path:
8
+ sys.path.insert(0, _project_root)
9
+
10
+ from fastapi import APIRouter
11
+ from services.model_service import scan_model_directory
12
+
13
+ router = APIRouter()
14
+
15
+
16
+ @router.get("/")
17
+ async def list_models():
18
+ models = scan_model_directory()
19
+ return {"models": models}
20
+
21
+
22
+ @router.get("/detect")
23
+ async def detect_models():
24
+ """Auto-detect metadata for all GGUF files in models/ directory."""
25
+ from model_runtime.detector import detect_models_in_directory
26
+ from services.model_service import _get_models_dir
27
+ models_dir = _get_models_dir()
28
+ results = detect_models_in_directory(models_dir)
29
+ return {"models": results}
@@ -0,0 +1,24 @@
1
+ """GET /status — Health check and model state."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ # Ensure project root on sys.path
7
+ _project_root = str(Path(__file__).resolve().parent.parent.parent)
8
+ if _project_root not in sys.path:
9
+ sys.path.insert(0, _project_root)
10
+
11
+ from fastapi import APIRouter
12
+ from services.model_service import is_model_loaded, get_current_model_name
13
+
14
+ router = APIRouter()
15
+
16
+
17
+ @router.get("/")
18
+ async def get_status():
19
+ return {
20
+ "status": "ok",
21
+ "model_loaded": is_model_loaded(),
22
+ "current_model": get_current_model_name(),
23
+ "version": "0.1.0",
24
+ }
backend/server.py ADDED
@@ -0,0 +1,41 @@
1
+ """
2
+ Legacy server.py — kept for backward compat.
3
+ The real entry point is local_ai_runtime/cli.py -> local_ai_runtime/server.py.
4
+ This file exists so `python backend/server.py` still works.
5
+ """
6
+
7
+ import sys
8
+ import json
9
+ import logging
10
+ from pathlib import Path
11
+
12
+ # Add project root to sys.path
13
+ _PROJECT_ROOT = Path(__file__).resolve().parent.parent
14
+ if str(_PROJECT_ROOT) not in sys.path:
15
+ sys.path.insert(0, str(_PROJECT_ROOT))
16
+
17
+ import uvicorn
18
+
19
+ CONFIG_DIR = _PROJECT_ROOT / "config"
20
+ SERVER_CONFIG_PATH = CONFIG_DIR / "server.config.json"
21
+
22
+
23
+ def load_server_config() -> dict:
24
+ if SERVER_CONFIG_PATH.exists():
25
+ with open(SERVER_CONFIG_PATH) as f:
26
+ return json.load(f)
27
+ raise FileNotFoundError(f"Config not found: {SERVER_CONFIG_PATH}")
28
+
29
+
30
+ if __name__ == "__main__":
31
+ logging.basicConfig(
32
+ level=logging.INFO,
33
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
34
+ )
35
+ config = load_server_config()
36
+ uvicorn.run(
37
+ "backend.server:app",
38
+ host=config.get("host", "0.0.0.0"),
39
+ port=config.get("port", 8000),
40
+ log_level=config.get("log_level", "info"),
41
+ )
File without changes