Skip to content

Commit 0eafc22

Browse files
committed
Implement legacy environment variable syntax support in configuration loader
- Added custom OmegaConf resolvers to handle legacy ${VAR:-default} syntax for backward compatibility. - Introduced a preprocessing function to convert legacy syntax in YAML files to OmegaConf-compatible format. - Updated the load_config function to utilize the new preprocessing for loading defaults and user configurations. - Enhanced documentation for clarity on the new legacy syntax handling.
1 parent b660f65 commit 0eafc22

File tree

4 files changed

+33
-95
lines changed

4 files changed

+33
-95
lines changed

backends/advanced/src/advanced_omi_backend/config.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
1515

1616
from advanced_omi_backend.config_loader import (
1717
get_backend_config,
18-
save_config_section,
18+
get_config_dir,
1919
load_config,
20-
reload_config as reload_omegaconf_config,
20+
)
21+
from advanced_omi_backend.config_loader import reload_config as reload_omegaconf_config
22+
from advanced_omi_backend.config_loader import (
23+
save_config_section,
2124
)
2225

2326
logger = logging.getLogger(__name__)
@@ -31,6 +34,15 @@
3134
# Configuration Functions (OmegaConf-based)
3235
# ============================================================================
3336

37+
def get_config_yml_path() -> Path:
38+
"""
39+
Get path to config.yml file.
40+
41+
Returns:
42+
Path to config.yml
43+
"""
44+
return get_config_dir() / "config.yml"
45+
3446
def get_config(force_reload: bool = False) -> dict:
3547
"""
3648
Get merged configuration using OmegaConf.

backends/advanced/src/advanced_omi_backend/config_loader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def load_config(force_reload: bool = False) -> DictConfig:
3030
Merge priority (later overrides earlier):
3131
1. config/defaults.yml (shipped defaults)
3232
2. config/config.yml (user overrides)
33-
3. Environment variables (via oc.env resolver)
33+
3. Environment variables (via ${oc.env:VAR,default} syntax)
3434
3535
Args:
3636
force_reload: If True, reload from disk even if cached

backends/advanced/src/advanced_omi_backend/model_registry.py

Lines changed: 7 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@
44
definitions (LLM, embeddings, etc.) in a provider-agnostic way.
55
66
Now using Pydantic for robust validation and type safety.
7+
Environment variable resolution is handled by OmegaConf in the config module.
78
"""
89

910
from __future__ import annotations
1011

11-
import os
12-
import re
1312
import yaml
1413
from pathlib import Path
1514
from typing import Any, Dict, List, Optional
@@ -18,77 +17,9 @@
1817
from pydantic import BaseModel, Field, field_validator, model_validator, ConfigDict, ValidationError
1918

2019
# Import config merging for defaults.yml + config.yml integration
20+
# OmegaConf handles environment variable resolution (${VAR:-default} syntax)
2121
from advanced_omi_backend.config import get_config
2222

23-
def _resolve_env(value: Any) -> Any:
24-
"""Resolve ``${VAR:-default}`` patterns inside a single value.
25-
26-
This helper is intentionally minimal: it only operates on strings and leaves
27-
all other types unchanged. Patterns of the form ``${VAR}`` or
28-
``${VAR:-default}`` are expanded using ``os.getenv``:
29-
30-
- If the environment variable **VAR** is set, its value is used.
31-
- Otherwise the optional ``default`` is used (or ``\"\"`` if omitted).
32-
33-
Examples:
34-
>>> os.environ.get("OLLAMA_MODEL")
35-
>>> _resolve_env("${OLLAMA_MODEL:-llama3.1:latest}")
36-
'llama3.1:latest'
37-
38-
>>> os.environ["OLLAMA_MODEL"] = "llama3.2:latest"
39-
>>> _resolve_env("${OLLAMA_MODEL:-llama3.1:latest}")
40-
'llama3.2:latest'
41-
42-
>>> _resolve_env("Bearer ${OPENAI_API_KEY:-}")
43-
'Bearer ' # when OPENAI_API_KEY is not set
44-
45-
Note:
46-
Use :func:`_deep_resolve_env` to apply this logic to an entire
47-
nested config structure (dicts/lists) loaded from YAML.
48-
"""
49-
if not isinstance(value, str):
50-
return value
51-
52-
pattern = re.compile(r"\$\{([^}:]+)(?::-(.*?))?\}")
53-
54-
def repl(match: re.Match[str]) -> str:
55-
var, default = match.group(1), match.group(2)
56-
return os.getenv(var, default or "")
57-
58-
return pattern.sub(repl, value)
59-
60-
61-
def _deep_resolve_env(data: Any) -> Any:
62-
"""Recursively resolve environment variables in nested structures.
63-
64-
This walks arbitrary Python structures produced by ``yaml.safe_load`` and
65-
applies :func:`_resolve_env` to every string it finds. Dictionaries and
66-
lists are traversed deeply; scalars are passed through unchanged.
67-
68-
Examples:
69-
>>> os.environ["OPENAI_MODEL"] = "gpt-4o-mini"
70-
>>> cfg = {
71-
... "models": [
72-
... {"model_name": "${OPENAI_MODEL:-gpt-4o-mini}"},
73-
... {"model_url": "${OPENAI_BASE_URL:-https://api.openai.com/v1}"}
74-
... ]
75-
... }
76-
>>> resolved = _deep_resolve_env(cfg)
77-
>>> resolved["models"][0]["model_name"]
78-
'gpt-4o-mini'
79-
>>> resolved["models"][1]["model_url"]
80-
'https://api.openai.com/v1'
81-
82-
This is what :func:`load_models_config` uses immediately after loading
83-
``config.yml`` so that all ``${VAR:-default}`` placeholders are resolved
84-
before Pydantic validation and model registry construction.
85-
"""
86-
if isinstance(data, dict):
87-
return {k: _deep_resolve_env(v) for k, v in data.items()}
88-
if isinstance(data, list):
89-
return [_deep_resolve_env(v) for v in data]
90-
return _resolve_env(data)
91-
9223

9324
class ModelDef(BaseModel):
9425
"""Model definition with validation.
@@ -270,7 +201,8 @@ def load_models_config(force_reload: bool = False) -> Optional[AppModels]:
270201
"""Load model configuration from merged defaults.yml + config.yml.
271202
272203
This function loads defaults.yml and config.yml, merges them with user overrides,
273-
resolves environment variables, validates model definitions using Pydantic, and caches the result.
204+
validates model definitions using Pydantic, and caches the result.
205+
Environment variables are resolved by OmegaConf during config loading.
274206
275207
Args:
276208
force_reload: If True, reload from disk even if already cached
@@ -280,24 +212,18 @@ def load_models_config(force_reload: bool = False) -> Optional[AppModels]:
280212
281213
Raises:
282214
ValidationError: If config.yml has invalid model definitions
283-
yaml.YAMLError: If config.yml has invalid YAML syntax
284215
"""
285216
global _REGISTRY
286217
if _REGISTRY is not None and not force_reload:
287218
return _REGISTRY
288219

289-
# Try to get merged configuration (defaults + user config)
220+
# Get merged configuration (defaults + user config)
221+
# OmegaConf resolves environment variables automatically
290222
try:
291223
raw = get_config(force_reload=force_reload)
292224
except Exception as e:
293225
logging.error(f"Failed to load merged configuration: {e}")
294-
# Fallback to direct config.yml loading
295-
cfg_path = _find_config_path()
296-
if not cfg_path.exists():
297-
return None
298-
with cfg_path.open("r") as f:
299-
raw = yaml.safe_load(f) or {}
300-
raw = _deep_resolve_env(raw)
226+
return None
301227

302228
# Extract sections
303229
defaults = raw.get("defaults", {}) or {}

config/defaults.yml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ models:
2020
api_family: openai
2121
model_name: gpt-4o-mini
2222
model_url: https://api.openai.com/v1
23-
api_key: ${OPENAI_API_KEY:-}
23+
api_key: ${oc.env:OPENAI_API_KEY,}
2424
model_params:
2525
temperature: 0.2
2626
max_tokens: 2000
@@ -33,7 +33,7 @@ models:
3333
api_family: openai
3434
model_name: llama3.1:latest
3535
model_url: http://localhost:11434/v1
36-
api_key: ${OPENAI_API_KEY:-ollama}
36+
api_key: ${oc.env:OPENAI_API_KEY,ollama}
3737
model_params:
3838
temperature: 0.2
3939
max_tokens: 2000
@@ -46,7 +46,7 @@ models:
4646
api_family: openai
4747
model_name: llama-3.1-70b-versatile
4848
model_url: https://api.groq.com/openai/v1
49-
api_key: ${GROQ_API_KEY:-}
49+
api_key: ${oc.env:GROQ_API_KEY,}
5050
model_params:
5151
temperature: 0.2
5252
max_tokens: 2000
@@ -62,7 +62,7 @@ models:
6262
api_family: openai
6363
model_name: text-embedding-3-small
6464
model_url: https://api.openai.com/v1
65-
api_key: ${OPENAI_API_KEY:-}
65+
api_key: ${oc.env:OPENAI_API_KEY,}
6666
embedding_dimensions: 1536
6767
model_output: vector
6868

@@ -73,7 +73,7 @@ models:
7373
api_family: openai
7474
model_name: nomic-embed-text:latest
7575
model_url: http://localhost:11434/v1
76-
api_key: ${OPENAI_API_KEY:-ollama}
76+
api_key: ${oc.env:OPENAI_API_KEY,ollama}
7777
embedding_dimensions: 768
7878
model_output: vector
7979

@@ -86,13 +86,13 @@ models:
8686
model_provider: deepgram
8787
api_family: http
8888
model_url: https://api.deepgram.com/v1
89-
api_key: ${DEEPGRAM_API_KEY:-}
89+
api_key: ${oc.env:DEEPGRAM_API_KEY,}
9090
operations:
9191
stt_transcribe:
9292
method: POST
9393
path: /listen
9494
headers:
95-
Authorization: Token ${DEEPGRAM_API_KEY:-}
95+
Authorization: Token ${oc.env:DEEPGRAM_API_KEY,}
9696
Content-Type: audio/raw
9797
query:
9898
model: nova-3
@@ -115,7 +115,7 @@ models:
115115
model_type: stt
116116
model_provider: parakeet
117117
api_family: http
118-
model_url: http://${PARAKEET_ASR_URL:-172.17.0.1:8767}
118+
model_url: http://${oc.env:PARAKEET_ASR_URL,172.17.0.1:8767}
119119
api_key: ''
120120
operations:
121121
stt_transcribe:
@@ -191,10 +191,10 @@ models:
191191
model_type: vector_store
192192
model_provider: qdrant
193193
api_family: qdrant
194-
model_url: http://${QDRANT_BASE_URL:-qdrant}:${QDRANT_PORT:-6333}
194+
model_url: http://${oc.env:QDRANT_BASE_URL,qdrant}:${oc.env:QDRANT_PORT,6333}
195195
model_params:
196-
host: ${QDRANT_BASE_URL:-qdrant}
197-
port: ${QDRANT_PORT:-6333}
196+
host: ${oc.env:QDRANT_BASE_URL,qdrant}
197+
port: ${oc.env:QDRANT_PORT,6333}
198198
collection_name: omi_memories
199199

200200
# ===========================

0 commit comments

Comments
 (0)