feat(config): named LLM provider list via env vars, runtime selection

- Config.llm_profiles: LLM_PROVIDERS declares names, each group defined by
  LLM_<NAME>_BASE_URL/_API_KEY/_MODEL (required) and optional _TIMEOUT_SECONDS/
  _MAX_RETRIES/_TEMPERATURE/_EXTRA_BODY; JSON-dict form of LLM_PROVIDERS also
  supported. LLM_PROVIDER selects the active group at runtime; empty/fake =
  offline scripted model
- removed fixed openai/kimi groups (AGENT_LLM_PROVIDER, KIMI_LLM_*)
- validate(): incomplete groups and unknown LLM_PROVIDER fail fast with
  the configured-name list
- OpenAILLMService: http client trust_env from LLM_HTTP_TRUST_ENV (default
  False - local gateways must bypass the Windows system proxy)
- build_llm_service(provider): explicit name or LLM_PROVIDER default
- .env/.env.example: deepseek + kimi groups side by side, switch by
  LLM_PROVIDER=kimi (thinking off via EXTRA_BODY reasoning_effort=none)
- eval probe scripts updated to the new config interface

Verified: 172 unit tests; live calls succeed on both providers
(deepseek 28s, kimi 6.6s, both emitting valid <state> prefixes)
This commit is contained in:
Eric Wang
2026-10-22 13:59:18 +08:00
parent e72d9c9bc0
commit e5d2a78886
6 changed files with 184 additions and 43 deletions
+2 -2
View File
@@ -39,13 +39,13 @@ def main():
path = args.prompt_dir / (name+'.md')
return path.read_text(encoding='utf-8') if path.exists() else original_loader(name)
registry.load_prompt = snapshot_loader
if Config.AGENT_BACKEND != 'langgraph' or Config.AGENT_LLM_PROVIDER != 'openai':
if Config.AGENT_BACKEND != 'langgraph' or not Config.LLM_PROVIDER or Config.LLM_PROVIDER == 'fake':
raise SystemExit('Requires langgraph backend and real openai provider')
args.output_dir.mkdir(parents=True, exist_ok=True)
cases = load_cases(DEFAULT_CASES_DIR, args.case)
from src.agent.prompts import load_prompt
metadata = {
'model': Config.LLM_MODEL, 'temperature': Config.LLM_TEMPERATURE,
**{k: v for k, v in (Config.llm_profile(Config.LLM_PROVIDER) or {}).items() if k in ('model', 'temperature')},
'backend': Config.AGENT_BACKEND, 'workers': args.workers,
'commit': subprocess.check_output(['git', 'rev-parse', 'HEAD'], text=True).strip(),
'case_hashes': {str(p.relative_to(DEFAULT_CASES_DIR)): sha256(p.read_bytes()).hexdigest()
+3 -2
View File
@@ -63,7 +63,7 @@ async def main(args):
if args.prompt_dir:
import src.agent.prompts as registry
registry.load_prompt = lambda name: (args.prompt_dir / (name+'.md')).read_text(encoding='utf-8')
llm = build_llm_service('openai')
llm = build_llm_service()
semaphore = asyncio.Semaphore(4)
async def run(case):
async with semaphore:
@@ -107,7 +107,8 @@ async def main(args):
results = await asyncio.gather(*(run(c) for c in selected))
from src.core.config import Config
from src.agent.prompts import load_prompt
output = dict(model=Config.LLM_MODEL, temperature=Config.LLM_TEMPERATURE,
profile = Config.llm_profile(Config.LLM_PROVIDER) or {}
output = dict(model=profile.get('model'), temperature=profile.get('temperature'),
execution_mode='runtime' if args.runtime else 'raw_prompt',
prompt_hashes={name:sha256(load_prompt(name).encode('utf-8')).hexdigest() for name in (COLLECTION,EXTRACTION)},
passed=sum(r['passed'] for r in results), total=len(results), results=results)
+1 -1
View File
@@ -26,7 +26,7 @@ def parsed_time(value):
async def main(output):
llm = build_llm_service('openai')
llm = build_llm_service()
cases = []
for delay in (75,135):
cases.append(dict(name=f'anchored_cross_midnight_delay_{delay}',now=ORIGIN+timedelta(minutes=delay),