|
19 | 19 |
|
20 | 20 | # Project root is two levels up from this file (notebooks/dspy_tasks/config.py → project root) |
21 | 21 | PROJECT_ROOT = Path(__file__).parent.parent.parent |
| 22 | +NOTEBOOKS_DIR = Path(__file__).parent.parent |
22 | 23 | ENV_FILE = PROJECT_ROOT / ".env" |
| 24 | +NOTEBOOKS_ENV_FILE = NOTEBOOKS_DIR / ".env" |
23 | 25 |
|
24 | 26 |
|
25 | 27 | # ============================================================================ |
26 | 28 | # ACTIONS: Load environment |
27 | 29 | # ============================================================================ |
28 | 30 |
|
29 | 31 | def _load_env(): |
30 | | - """Load .env file from project root if it exists. ACTION: file I/O.""" |
| 32 | + """Load .env file from project root and/or notebooks dir. ACTION: file I/O.""" |
31 | 33 | try: |
32 | 34 | from dotenv import load_dotenv |
33 | | - if ENV_FILE.exists(): |
34 | | - load_dotenv(ENV_FILE, override=False) |
35 | | - logger.debug("Loaded .env from %s", ENV_FILE) |
36 | | - else: |
37 | | - logger.debug("No .env file at %s, using system environment", ENV_FILE) |
| 35 | + loaded = False |
| 36 | + for env_path in [ENV_FILE, NOTEBOOKS_ENV_FILE]: |
| 37 | + if env_path.exists(): |
| 38 | + load_dotenv(env_path, override=False) |
| 39 | + logger.debug("Loaded .env from %s", env_path) |
| 40 | + loaded = True |
| 41 | + if not loaded: |
| 42 | + logger.debug("No .env file found, using system environment") |
38 | 43 | except ImportError: |
39 | 44 | logger.debug("python-dotenv not installed, using system environment only") |
40 | 45 |
|
@@ -113,15 +118,34 @@ def discover_models() -> list[str]: |
113 | 118 | return [] |
114 | 119 |
|
115 | 120 |
|
116 | | -def get_available_models() -> list[str]: |
117 | | - """Return all available models: configured + discovered, deduplicated. |
| 121 | +def discover_lmstudio_models() -> list[str]: |
| 122 | + """Discover models from a local LM Studio instance. |
118 | 123 |
|
119 | | - This mirrors backend/llm_service.py's get_model_catalog() logic. |
| 124 | + Only attempts discovery if LM_STUDIO_API_BASE is set. |
| 125 | + Uses LiteLLM's native lm_studio/ prefix convention. |
| 126 | + Returns an empty list if LM Studio is not running or not configured. |
120 | 127 | """ |
| 128 | + base_url = os.getenv("LM_STUDIO_API_BASE") |
| 129 | + if not base_url: |
| 130 | + return [] |
| 131 | + try: |
| 132 | + import httpx |
| 133 | + resp = httpx.get(f"{base_url}/models", timeout=2.0) |
| 134 | + resp.raise_for_status() |
| 135 | + data = resp.json().get("data", []) |
| 136 | + return [f"lm_studio/{m['id']}" for m in data if m.get("id")] |
| 137 | + except Exception as exc: |
| 138 | + logger.debug("LM Studio discovery failed (is it running?): %s", exc) |
| 139 | + return [] |
| 140 | + |
| 141 | + |
| 142 | +def get_available_models() -> list[str]: |
| 143 | + """Return all available models: configured + discovered + LM Studio, deduplicated.""" |
121 | 144 | primary = get_default_model() |
122 | 145 | fallbacks = get_fallback_models() |
123 | 146 | discovered = discover_models() |
124 | | - return _build_model_list(primary, fallbacks, discovered) |
| 147 | + lmstudio = discover_lmstudio_models() |
| 148 | + return _build_model_list(primary, fallbacks, discovered + lmstudio) |
125 | 149 |
|
126 | 150 |
|
127 | 151 | # ============================================================================ |
|
0 commit comments