yc1838 commited on
Commit
28a277f
·
1 Parent(s): bad5fbe

add deepseek compatibility

Browse files
.gitignore CHANGED
@@ -18,3 +18,7 @@ gaia_eval_dashboard.html
18
  .claude/
19
  .codegraph/
20
  .cursor/
 
 
 
 
 
18
  .claude/
19
  .codegraph/
20
  .cursor/
21
+ .pycache/
22
+ .__pycache__/
23
+ .ruff_cache/
24
+ .venv/
CLAUDE.md CHANGED
@@ -49,7 +49,7 @@ Untrusted-input boundary: the user's first `HumanMessage` is wrapped in `<gaia_q
49
 
50
  ### Config (`src/lilith_agent/config.py`)
51
 
52
- `Config.from_env()` reads `GAIA_*` env vars (note: `GAIA_` prefix, not `LILITH_`). Three model tiers: `cheap` / `strong` / `extra_strong`, each with independent provider+model. The agent's main model is **`extra_strong`**; `cheap` powers the tool-result summarizer. `extra_strong_*` defaults to `strong_*` if unset. The `vision_*` pair is separate, used by `inspect_visual_content`. Behavior flags: `caveman`, `caveman_mode`, `recursion_limit` (50), `budget_hard_cap` (25), `budget_warn_at` (15), `semantic_dedup_threshold` (0.5), `compact_summarize`, `llm_formatter_enabled`.
53
 
54
  ### Tools (`src/lilith_agent/tools/`)
55
 
 
49
 
50
  ### Config (`src/lilith_agent/config.py`)
51
 
52
+ `Config.from_env()` reads `GAIA_*` env vars (note: `GAIA_` prefix, not `LILITH_`). Three model tiers: `cheap` / `strong` / `extra_strong`, each with independent provider+model. The agent's main model defaults to **`extra_strong`**, can select `cheap` or `strong` via `GAIA_AGENT_MODEL_TIER`, and can be directly overridden with `GAIA_AGENT_PROVIDER` / `GAIA_AGENT_MODEL`; `cheap` powers the tool-result summarizer. `extra_strong_*` defaults to `strong_*` if unset. DeepSeek is available as provider `deepseek` using `DEEPSEEK_API_KEY` or `GAIA_DEEPSEEK_API_KEY`. The `vision_*` pair is separate, used by `inspect_visual_content`. Behavior flags: `caveman`, `caveman_mode`, `recursion_limit` (50), `budget_hard_cap` (25), `budget_warn_at` (15), `semantic_dedup_threshold` (0.5), `compact_summarize`, `llm_formatter_enabled`.
53
 
54
  ### Tools (`src/lilith_agent/tools/`)
55
 
README.md CHANGED
@@ -65,6 +65,19 @@ GAIA_STRONG_PROVIDER=anthropic
65
  GAIA_STRONG_MODEL=claude-sonnet-4-6
66
  GAIA_EXTRA_STRONG_PROVIDER=anthropic
67
  GAIA_EXTRA_STRONG_MODEL=claude-sonnet-4-6
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  GAIA_VISION_PROVIDER=fal
69
  GAIA_VISION_MODEL=gemini-3-flash-preview
70
  GAIA_CAVEMAN=true
 
65
  GAIA_STRONG_MODEL=claude-sonnet-4-6
66
  GAIA_EXTRA_STRONG_PROVIDER=anthropic
67
  GAIA_EXTRA_STRONG_MODEL=claude-sonnet-4-6
68
+ GAIA_AGENT_MODEL_TIER=extra_strong # cheap | strong | extra_strong
69
+ # Optional one-off override for the main agent model.
70
+ # GAIA_AGENT_PROVIDER=deepseek
71
+ # GAIA_AGENT_MODEL=deepseek-v4-pro
72
+
73
+ # DeepSeek uses an OpenAI-compatible API.
74
+ # DEEPSEEK_API_KEY=...
75
+ # GAIA_DEEPSEEK_BASE_URL=https://api.deepseek.com
76
+ # GAIA_CHEAP_PROVIDER=deepseek
77
+ # GAIA_CHEAP_MODEL=deepseek-v4-flash
78
+ # GAIA_STRONG_PROVIDER=deepseek
79
+ # GAIA_STRONG_MODEL=deepseek-v4-pro
80
+
81
  GAIA_VISION_PROVIDER=fal
82
  GAIA_VISION_MODEL=gemini-3-flash-preview
83
  GAIA_CAVEMAN=true
src/lilith_agent/app.py CHANGED
@@ -922,7 +922,10 @@ def build_react_agent(cfg: Config):
922
  guidance = str(state.get("supervisor_guidance", "") or "").strip()
923
  original_question = _initial_question_from_state(state)
924
  compacted = _compact_old_tool_messages(state["messages"], summarize_fn=summarize_fn)
925
- response = base_model.invoke([
 
 
 
926
  SystemMessage(content=(
927
  "SUPERVISOR FINALIZER: Stop tool use. Answer the original question, not an intermediate hop. "
928
  "Produce a bare final answer in 'Final Answer: ...' form using the existing evidence. "
@@ -1012,7 +1015,9 @@ def build_react_agent(cfg: Config):
1012
  log.debug("[memory] skipping extraction: only %d messages", len(messages))
1013
  return state
1014
  try:
1015
- cheap_model = get_cheap_model(cfg)
 
 
1016
  extract_and_compress_facts(messages, cheap_model)
1017
  except Exception as e:
1018
  log.warning("[memory] failed to run extraction: %s", e)
 
922
  guidance = str(state.get("supervisor_guidance", "") or "").strip()
923
  original_question = _initial_question_from_state(state)
924
  compacted = _compact_old_tool_messages(state["messages"], summarize_fn=summarize_fn)
925
+ # Thinking mode (DeepSeek v4) emits tool-call markup as raw text here, where no
926
+ # tools are bound to parse it; disable it so the finalizer returns plain prose.
927
+ finalizer_model = get_extra_strong_model(cfg, thinking=False)
928
+ response = finalizer_model.invoke([
929
  SystemMessage(content=(
930
  "SUPERVISOR FINALIZER: Stop tool use. Answer the original question, not an intermediate hop. "
931
  "Produce a bare final answer in 'Final Answer: ...' form using the existing evidence. "
 
1015
  log.debug("[memory] skipping extraction: only %d messages", len(messages))
1016
  return state
1017
  try:
1018
+ # langmem/trustcall forces tool_choice for structured extraction, which
1019
+ # DeepSeek thinking mode rejects with a 400 it then retries forever.
1020
+ cheap_model = get_cheap_model(cfg, thinking=False)
1021
  extract_and_compress_facts(messages, cheap_model)
1022
  except Exception as e:
1023
  log.warning("[memory] failed to run extraction: %s", e)
src/lilith_agent/config.py CHANGED
@@ -41,6 +41,11 @@ class Config:
41
  tavily_api_key: str
42
  lmstudio_base_url: str
43
  max_tokens: int
 
 
 
 
 
44
  caveman: bool = False
45
  caveman_mode: str = "full"
46
  recursion_limit: int = 100
@@ -54,6 +59,12 @@ class Config:
54
 
55
  @classmethod
56
  def from_env(cls) -> "Config":
 
 
 
 
 
 
57
  return cls(
58
  cheap_provider=os.getenv("GAIA_CHEAP_PROVIDER", "google"),
59
  cheap_model=os.getenv("GAIA_CHEAP_MODEL", "gemini-3-flash-preview"),
@@ -72,6 +83,11 @@ class Config:
72
  huggingface_api_key=os.getenv("GAIA_HUGGINGFACE_API_KEY", ""),
73
  tavily_api_key=os.getenv("GAIA_TAVILY_API_KEY", ""),
74
  lmstudio_base_url=os.getenv("GAIA_LMSTUDIO_BASE_URL", ""),
 
 
 
 
 
75
  max_tokens=_get_int_env("GAIA_MAX_TOKENS", "65536"),
76
  caveman=os.getenv("GAIA_CAVEMAN", "true").lower() == "true",
77
  caveman_mode=os.getenv("GAIA_CAVEMAN_MODE", "full"),
 
41
  tavily_api_key: str
42
  lmstudio_base_url: str
43
  max_tokens: int
44
+ deepseek_api_key: str = ""
45
+ deepseek_base_url: str = "https://api.deepseek.com"
46
+ agent_model_tier: str = "extra_strong"
47
+ agent_provider: str = ""
48
+ agent_model: str = ""
49
  caveman: bool = False
50
  caveman_mode: str = "full"
51
  recursion_limit: int = 100
 
59
 
60
  @classmethod
61
  def from_env(cls) -> "Config":
62
+ agent_model_tier = os.getenv("GAIA_AGENT_MODEL_TIER", "extra_strong").strip().lower()
63
+ if agent_model_tier not in {"cheap", "strong", "extra_strong"}:
64
+ raise ValueError(
65
+ "GAIA_AGENT_MODEL_TIER must be one of: cheap, strong, extra_strong"
66
+ )
67
+
68
  return cls(
69
  cheap_provider=os.getenv("GAIA_CHEAP_PROVIDER", "google"),
70
  cheap_model=os.getenv("GAIA_CHEAP_MODEL", "gemini-3-flash-preview"),
 
83
  huggingface_api_key=os.getenv("GAIA_HUGGINGFACE_API_KEY", ""),
84
  tavily_api_key=os.getenv("GAIA_TAVILY_API_KEY", ""),
85
  lmstudio_base_url=os.getenv("GAIA_LMSTUDIO_BASE_URL", ""),
86
+ deepseek_api_key=os.getenv("GAIA_DEEPSEEK_API_KEY", os.getenv("DEEPSEEK_API_KEY", "")),
87
+ deepseek_base_url=os.getenv("GAIA_DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
88
+ agent_model_tier=agent_model_tier,
89
+ agent_provider=os.getenv("GAIA_AGENT_PROVIDER", "").strip(),
90
+ agent_model=os.getenv("GAIA_AGENT_MODEL", "").strip(),
91
  max_tokens=_get_int_env("GAIA_MAX_TOKENS", "65536"),
92
  caveman=os.getenv("GAIA_CAVEMAN", "true").lower() == "true",
93
  caveman_mode=os.getenv("GAIA_CAVEMAN_MODE", "full"),
src/lilith_agent/models.py CHANGED
@@ -109,6 +109,7 @@ if SQLiteCache:
109
  log = logging.getLogger(__name__)
110
 
111
  LMSTUDIO_DEFAULT_BASE_URL = "http://localhost:1234/v1"
 
112
  _NO_THINK = "/no_think"
113
  _GEMINI_COOLDOWN_MODELS = {"gemini-3-flash-preview", "gemini-3.1-pro"}
114
  _COOLDOWN_LADDER_SECONDS = (60, 120, 300)
@@ -460,7 +461,27 @@ class _BoundRetryWrapper(Runnable):
460
  return getattr(self._bound, name)
461
 
462
 
463
- def _build(provider: str, model: str, cfg: Config) -> BaseChatModel:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
464
  log.info("Building model for provider=%r, model=%r", provider, model)
465
  max_tokens = cfg.max_tokens
466
 
@@ -493,6 +514,19 @@ def _build(provider: str, model: str, cfg: Config) -> BaseChatModel:
493
  repo_id=model, huggingfacehub_api_token=cfg.huggingface_api_key, max_new_tokens=max_tokens
494
  )
495
  return _wrap(ChatHuggingFace(llm=endpoint))
 
 
 
 
 
 
 
 
 
 
 
 
 
496
  if provider == "lmstudio":
497
  # LM Studio exposes an OpenAI-compatible API.
498
  inner = ChatOpenAI(
@@ -509,13 +543,14 @@ def _build(provider: str, model: str, cfg: Config) -> BaseChatModel:
509
  raise ValueError(f"Unknown provider: {provider}")
510
 
511
 
512
- def get_cheap_model(cfg: Config) -> BaseChatModel:
513
- return _build(cfg.cheap_provider, cfg.cheap_model, cfg)
514
 
515
 
516
  def get_strong_model(cfg: Config) -> BaseChatModel:
517
  return _build(cfg.strong_provider, cfg.strong_model, cfg)
518
 
519
 
520
- def get_extra_strong_model(cfg: Config) -> BaseChatModel:
521
- return _build(cfg.extra_strong_provider, cfg.extra_strong_model, cfg)
 
 
109
  log = logging.getLogger(__name__)
110
 
111
  LMSTUDIO_DEFAULT_BASE_URL = "http://localhost:1234/v1"
112
+ DEEPSEEK_DEFAULT_BASE_URL = "https://api.deepseek.com"
113
  _NO_THINK = "/no_think"
114
  _GEMINI_COOLDOWN_MODELS = {"gemini-3-flash-preview", "gemini-3.1-pro"}
115
  _COOLDOWN_LADDER_SECONDS = (60, 120, 300)
 
461
  return getattr(self._bound, name)
462
 
463
 
464
+ def _resolve_agent_model_choice(cfg: Config) -> tuple[str, str]:
465
+ tier = (cfg.agent_model_tier or "extra_strong").strip().lower()
466
+ tiers = {
467
+ "cheap": (cfg.cheap_provider, cfg.cheap_model),
468
+ "strong": (cfg.strong_provider, cfg.strong_model),
469
+ "extra_strong": (cfg.extra_strong_provider, cfg.extra_strong_model),
470
+ }
471
+ if tier not in tiers:
472
+ raise ValueError(
473
+ "GAIA_AGENT_MODEL_TIER must be one of: cheap, strong, extra_strong"
474
+ )
475
+
476
+ provider, model = tiers[tier]
477
+ return (
478
+ (cfg.agent_provider or provider).strip(),
479
+ (cfg.agent_model or model).strip(),
480
+ )
481
+
482
+
483
+ def _build(provider: str, model: str, cfg: Config, thinking: bool = True) -> BaseChatModel:
484
+ provider = provider.strip().lower()
485
  log.info("Building model for provider=%r, model=%r", provider, model)
486
  max_tokens = cfg.max_tokens
487
 
 
514
  repo_id=model, huggingfacehub_api_token=cfg.huggingface_api_key, max_new_tokens=max_tokens
515
  )
516
  return _wrap(ChatHuggingFace(llm=endpoint))
517
+ if provider == "deepseek":
518
+ ds_kwargs = dict(
519
+ model=model,
520
+ base_url=cfg.deepseek_base_url or DEEPSEEK_DEFAULT_BASE_URL,
521
+ api_key=cfg.deepseek_api_key,
522
+ max_tokens=max_tokens,
523
+ )
524
+ # DeepSeek v4 defaults to thinking mode, which rejects forced tool_choice
525
+ # (langmem extraction) and leaks raw tool-call markup in free-text calls
526
+ # (finalizer). Opt out explicitly when the caller needs a plain response.
527
+ if not thinking:
528
+ ds_kwargs["extra_body"] = {"thinking": {"type": "disabled"}}
529
+ return _wrap(ChatOpenAI(**ds_kwargs))
530
  if provider == "lmstudio":
531
  # LM Studio exposes an OpenAI-compatible API.
532
  inner = ChatOpenAI(
 
543
  raise ValueError(f"Unknown provider: {provider}")
544
 
545
 
546
+ def get_cheap_model(cfg: Config, thinking: bool = True) -> BaseChatModel:
547
+ return _build(cfg.cheap_provider, cfg.cheap_model, cfg, thinking=thinking)
548
 
549
 
550
  def get_strong_model(cfg: Config) -> BaseChatModel:
551
  return _build(cfg.strong_provider, cfg.strong_model, cfg)
552
 
553
 
554
+ def get_extra_strong_model(cfg: Config, thinking: bool = True) -> BaseChatModel:
555
+ provider, model = _resolve_agent_model_choice(cfg)
556
+ return _build(provider, model, cfg, thinking=thinking)
tests/test_graph.py CHANGED
@@ -256,7 +256,7 @@ def test_supervisor_nudges_agent_to_answer_when_evidence_is_enough(monkeypatch,
256
  cfg.compact_summarize = False
257
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
258
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 1, raising=False)
259
- monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg: strong)
260
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: object())
261
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
262
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
@@ -313,7 +313,7 @@ def test_supervisor_uses_extra_strong_model_not_cheap_model(monkeypatch, tmp_pat
313
  cfg.compact_summarize = False
314
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
315
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 1, raising=False)
316
- monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg: strong)
317
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", cheap_should_not_be_used)
318
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
319
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
@@ -362,7 +362,7 @@ def test_supervisor_finalizer_prompt_reinforces_original_question_contract(monke
362
  cfg.compact_summarize = False
363
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
364
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 1, raising=False)
365
- monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg: strong)
366
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: object())
367
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
368
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
@@ -413,7 +413,7 @@ def test_supervisor_finalizer_rejects_unknown_best_answer_and_forces_best_guess(
413
  cfg.compact_summarize = False
414
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
415
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 1, raising=False)
416
- monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg: strong)
417
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: object())
418
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
419
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
@@ -471,7 +471,7 @@ def test_supervisor_finalizes_even_with_placeholder_if_requested(monkeypatch, tm
471
  cfg.compact_summarize = False
472
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
473
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 1, raising=False)
474
- monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg: strong)
475
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: object())
476
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
477
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
@@ -528,7 +528,7 @@ def test_supervisor_forces_finalize_after_max_nudges(monkeypatch, tmp_path):
528
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
529
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 1, raising=False)
530
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MAX_NUDGES", 5, raising=False)
531
- monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg: strong)
532
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: object())
533
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
534
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
@@ -580,7 +580,7 @@ def test_final_answer_gets_supervisor_review_and_can_be_returned_for_revision(mo
580
  cfg.budget_warn_at = 99
581
  cfg.compact_summarize = False
582
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
583
- monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg: strong)
584
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: object())
585
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [])
586
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
@@ -638,7 +638,7 @@ def test_fail_safe_falls_back_to_supervisor_best_answer_when_empty(monkeypatch,
638
  cfg.compact_summarize = False
639
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
640
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 1, raising=False)
641
- monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg: strong)
642
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: object())
643
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
644
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
@@ -690,7 +690,7 @@ def test_fail_safe_never_returns_empty_answer_without_best_answer(monkeypatch, t
690
  cfg.compact_summarize = False
691
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
692
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 99, raising=False)
693
- monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg: strong)
694
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: object())
695
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
696
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
@@ -749,7 +749,7 @@ def test_supervisor_review_auto_approves_after_fail_safe(monkeypatch, tmp_path):
749
  cfg.compact_summarize = False
750
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
751
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 99, raising=False)
752
- monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg: strong)
753
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: object())
754
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
755
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
@@ -821,7 +821,7 @@ def test_supervisor_finalizes_when_agent_ignores_prior_nudge(monkeypatch, tmp_pa
821
  cfg.compact_summarize = False
822
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
823
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 1, raising=False)
824
- monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg: strong)
825
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: object())
826
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
827
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
@@ -874,7 +874,7 @@ def test_supervisor_overhead_leaves_room_for_hard_cap_fail_safe(monkeypatch, tmp
874
  cfg.compact_summarize = False
875
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
876
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 1, raising=False)
877
- monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg: strong)
878
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: FakeSupervisorModel())
879
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
880
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
 
256
  cfg.compact_summarize = False
257
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
258
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 1, raising=False)
259
+ monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg, thinking=True: strong)
260
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: object())
261
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
262
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
 
313
  cfg.compact_summarize = False
314
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
315
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 1, raising=False)
316
+ monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg, thinking=True: strong)
317
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", cheap_should_not_be_used)
318
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
319
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
 
362
  cfg.compact_summarize = False
363
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
364
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 1, raising=False)
365
+ monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg, thinking=True: strong)
366
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: object())
367
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
368
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
 
413
  cfg.compact_summarize = False
414
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
415
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 1, raising=False)
416
+ monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg, thinking=True: strong)
417
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: object())
418
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
419
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
 
471
  cfg.compact_summarize = False
472
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
473
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 1, raising=False)
474
+ monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg, thinking=True: strong)
475
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: object())
476
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
477
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
 
528
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
529
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 1, raising=False)
530
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MAX_NUDGES", 5, raising=False)
531
+ monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg, thinking=True: strong)
532
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: object())
533
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
534
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
 
580
  cfg.budget_warn_at = 99
581
  cfg.compact_summarize = False
582
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
583
+ monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg, thinking=True: strong)
584
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: object())
585
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [])
586
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
 
638
  cfg.compact_summarize = False
639
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
640
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 1, raising=False)
641
+ monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg, thinking=True: strong)
642
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: object())
643
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
644
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
 
690
  cfg.compact_summarize = False
691
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
692
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 99, raising=False)
693
+ monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg, thinking=True: strong)
694
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: object())
695
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
696
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
 
749
  cfg.compact_summarize = False
750
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
751
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 99, raising=False)
752
+ monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg, thinking=True: strong)
753
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: object())
754
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
755
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
 
821
  cfg.compact_summarize = False
822
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
823
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 1, raising=False)
824
+ monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg, thinking=True: strong)
825
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: object())
826
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
827
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
 
874
  cfg.compact_summarize = False
875
  monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
876
  monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 1, raising=False)
877
+ monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg, thinking=True: strong)
878
  monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: FakeSupervisorModel())
879
  monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
880
  monkeypatch.setattr("lilith_agent.memory.extract_and_compress_facts", lambda messages, model: None)
tests/test_models.py CHANGED
@@ -423,3 +423,132 @@ async def test_async_bound_retry_wrapper_raises_cooldown_for_gemini_lane(monkeyp
423
  await bound.ainvoke([("user", "hi")])
424
 
425
  assert raised.value.model == "gemini-3.1-pro"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
  await bound.ainvoke([("user", "hi")])
424
 
425
  assert raised.value.model == "gemini-3.1-pro"
426
+
427
+
428
+ def test_deepseek_config_defaults_and_env(monkeypatch):
429
+ from lilith_agent.config import Config
430
+
431
+ monkeypatch.delenv("GAIA_DEEPSEEK_API_KEY", raising=False)
432
+ monkeypatch.delenv("GAIA_DEEPSEEK_BASE_URL", raising=False)
433
+ monkeypatch.setenv("DEEPSEEK_API_KEY", "ds-env-key")
434
+
435
+ cfg = Config.from_env()
436
+
437
+ assert cfg.deepseek_api_key == "ds-env-key"
438
+ assert cfg.deepseek_base_url == "https://api.deepseek.com"
439
+
440
+ monkeypatch.setenv("GAIA_DEEPSEEK_API_KEY", "gaia-ds-key")
441
+ monkeypatch.setenv("GAIA_DEEPSEEK_BASE_URL", "https://deepseek.internal")
442
+
443
+ cfg = Config.from_env()
444
+
445
+ assert cfg.deepseek_api_key == "gaia-ds-key"
446
+ assert cfg.deepseek_base_url == "https://deepseek.internal"
447
+
448
+
449
+ def test_agent_model_tier_selects_configured_tier(monkeypatch):
450
+ from lilith_agent.config import Config
451
+ from lilith_agent.models import _resolve_agent_model_choice
452
+
453
+ monkeypatch.setenv("GAIA_CHEAP_PROVIDER", "deepseek")
454
+ monkeypatch.setenv("GAIA_CHEAP_MODEL", "deepseek-v4-flash")
455
+ monkeypatch.setenv("GAIA_STRONG_PROVIDER", "deepseek")
456
+ monkeypatch.setenv("GAIA_STRONG_MODEL", "deepseek-v4-pro")
457
+ monkeypatch.setenv("GAIA_AGENT_MODEL_TIER", "strong")
458
+
459
+ assert _resolve_agent_model_choice(Config.from_env()) == ("deepseek", "deepseek-v4-pro")
460
+
461
+
462
+ def test_agent_model_direct_override_wins_over_tier(monkeypatch):
463
+ from lilith_agent.config import Config
464
+ from lilith_agent.models import _resolve_agent_model_choice
465
+
466
+ monkeypatch.setenv("GAIA_AGENT_MODEL_TIER", "cheap")
467
+ monkeypatch.setenv("GAIA_AGENT_PROVIDER", "deepseek")
468
+ monkeypatch.setenv("GAIA_AGENT_MODEL", "deepseek-v4-pro")
469
+
470
+ assert _resolve_agent_model_choice(Config.from_env()) == ("deepseek", "deepseek-v4-pro")
471
+
472
+
473
+ def test_invalid_agent_model_tier_is_rejected(monkeypatch):
474
+ from lilith_agent.config import Config
475
+
476
+ monkeypatch.setenv("GAIA_AGENT_MODEL_TIER", "medium")
477
+
478
+ with pytest.raises(ValueError, match="GAIA_AGENT_MODEL_TIER"):
479
+ Config.from_env()
480
+
481
+
482
+ def test_build_deepseek_uses_openai_compatible_endpoint(monkeypatch):
483
+ from dataclasses import replace
484
+
485
+ from lilith_agent.config import Config
486
+ from lilith_agent.models import _build
487
+
488
+ class FakeChatOpenAI:
489
+ def __init__(self, **kwargs):
490
+ self.kwargs = kwargs
491
+
492
+ monkeypatch.setattr("lilith_agent.models.ChatOpenAI", FakeChatOpenAI)
493
+ monkeypatch.setattr(
494
+ "lilith_agent.models._RetryWrapper",
495
+ lambda inner, provider, model_name: inner,
496
+ )
497
+
498
+ cfg = replace(
499
+ Config.from_env(),
500
+ deepseek_api_key="ds-key",
501
+ deepseek_base_url="https://deepseek.internal",
502
+ max_tokens=123,
503
+ )
504
+
505
+ model = _build("deepseek", "deepseek-v4-flash", cfg)
506
+
507
+ assert model.kwargs == {
508
+ "model": "deepseek-v4-flash",
509
+ "api_key": "ds-key",
510
+ "base_url": "https://deepseek.internal",
511
+ "max_tokens": 123,
512
+ }
513
+
514
+
515
+ def _fake_deepseek_cfg(monkeypatch):
516
+ from dataclasses import replace
517
+
518
+ from lilith_agent.config import Config
519
+
520
+ class FakeChatOpenAI:
521
+ def __init__(self, **kwargs):
522
+ self.kwargs = kwargs
523
+
524
+ monkeypatch.setattr("lilith_agent.models.ChatOpenAI", FakeChatOpenAI)
525
+ monkeypatch.setattr(
526
+ "lilith_agent.models._RetryWrapper",
527
+ lambda inner, provider, model_name: inner,
528
+ )
529
+ return replace(
530
+ Config.from_env(),
531
+ deepseek_api_key="ds-key",
532
+ deepseek_base_url="https://deepseek.internal",
533
+ max_tokens=123,
534
+ )
535
+
536
+
537
+ def test_build_deepseek_thinking_disabled_sets_extra_body(monkeypatch):
538
+ from lilith_agent.models import _build
539
+
540
+ cfg = _fake_deepseek_cfg(monkeypatch)
541
+
542
+ model = _build("deepseek", "deepseek-v4-flash", cfg, thinking=False)
543
+
544
+ assert model.kwargs["extra_body"] == {"thinking": {"type": "disabled"}}
545
+
546
+
547
+ def test_build_deepseek_thinking_enabled_omits_extra_body(monkeypatch):
548
+ from lilith_agent.models import _build
549
+
550
+ cfg = _fake_deepseek_cfg(monkeypatch)
551
+
552
+ model = _build("deepseek", "deepseek-v4-flash", cfg)
553
+
554
+ assert "extra_body" not in model.kwargs