Louis-François Bouchard commited on
Commit
8a9b6de
·
1 Parent(s): dc3936a

Use direct DeepSeek as default model

Browse files
.env.example CHANGED
@@ -1,7 +1,7 @@
1
  # To run the AI Tutor app (FastAPI backend)
2
- # Default chat path: DeepSeek V4 Flash through OpenRouter, with Gemini 2.5
3
- # Flash as the local application fallback if the OpenRouter call fails.
4
- OPENROUTER_API_KEY=...
5
  GEMINI_API_KEY=...
6
  # Gemini also accepts GOOGLE_API_KEY instead of GEMINI_API_KEY.
7
  GOOGLE_API_KEY=...
@@ -10,8 +10,7 @@ GOOGLE_API_KEY=...
10
  OPENAI_API_KEY=...
11
  ANTHROPIC_API_KEY=...
12
  COHERE_API_KEY=...
13
- # DEEPSEEK_API_KEY hits the first-party DeepSeek API (not the default path).
14
- DEEPSEEK_API_KEY=...
15
 
16
  # Optional: trace agent/model/tool runs in LangSmith
17
  LANGSMITH_API_KEY=...
 
1
  # To run the AI Tutor app (FastAPI backend)
2
+ # Default chat path: DeepSeek V4 Flash through the first-party DeepSeek API,
3
+ # with Gemini 2.5 Flash as the local application fallback if DeepSeek fails.
4
+ DEEPSEEK_API_KEY=...
5
  GEMINI_API_KEY=...
6
  # Gemini also accepts GOOGLE_API_KEY instead of GEMINI_API_KEY.
7
  GOOGLE_API_KEY=...
 
10
  OPENAI_API_KEY=...
11
  ANTHROPIC_API_KEY=...
12
  COHERE_API_KEY=...
13
+ OPENROUTER_API_KEY=...
 
14
 
15
  # Optional: trace agent/model/tool runs in LangSmith
16
  LANGSMITH_API_KEY=...
README.md CHANGED
@@ -48,7 +48,7 @@ Want to build this AI tutor yourself, end to end? Our [**Full Stack AI Engineer
48
  cp .env.example .env # then edit values
49
  ```
50
 
51
- The chat model is provider-agnostic, configured in `provider:model` format. The default is `openrouter:deepseek/deepseek-v4-flash`, which uses OpenRouter first and falls back in-app to `google-genai:gemini-2.5-flash` when a Gemini key is configured. Set `OPENROUTER_API_KEY` plus `GEMINI_API_KEY` or `GOOGLE_API_KEY` for the default path. Optional provider keys include `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, and `DEEPSEEK_API_KEY` for non-default provider paths.
52
  To trace requests in LangSmith, set `LANGSMITH_API_KEY`. The app enables tracing automatically when that key is present unless `LANGSMITH_TRACING=false` is set.
53
 
54
  ### LangSmith Agent Tracing
 
48
  cp .env.example .env # then edit values
49
  ```
50
 
51
+ The chat model is provider-agnostic, configured in `provider:model` format. The default is `deepseek:deepseek-v4-flash`, which uses DeepSeek's first-party API and falls back in-app to `google-genai:gemini-2.5-flash` when a Gemini key is configured. Set `DEEPSEEK_API_KEY` plus `GEMINI_API_KEY` or `GOOGLE_API_KEY` for the default path. Optional provider keys include `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, and `OPENROUTER_API_KEY` for non-default provider paths.
52
  To trace requests in LangSmith, set `LANGSMITH_API_KEY`. The app enables tracing automatically when that key is present unless `LANGSMITH_TRACING=false` is set.
53
 
54
  ### LangSmith Agent Tracing
app/chat_service.py CHANGED
@@ -72,7 +72,7 @@ from .provider_events import (
72
  from .config import (
73
  BM25_INDEX_PATH,
74
  COURSE_SOURCE_KEYS,
75
- DEEPSEEK_OPENROUTER_MODEL_NAME,
76
  DEFAULT_SELECTED_SOURCE_KEYS,
77
  DOCUMENT_DICT_PATH,
78
  GEMINI_FALLBACK_MODEL_NAME,
@@ -793,8 +793,8 @@ def _has_google_genai_key() -> bool:
793
  return bool(os.environ.get("GOOGLE_API_KEY") or os.environ.get("GEMINI_API_KEY"))
794
 
795
 
796
- def _has_openrouter_key() -> bool:
797
- return bool(os.environ.get("OPENROUTER_API_KEY"))
798
 
799
 
800
  def _build_chat_model_client(provider_model: str, include_thoughts: bool = False):
@@ -898,15 +898,15 @@ def _build_chat_model_client(provider_model: str, include_thoughts: bool = False
898
 
899
  def build_chat_model(model_name: str, include_thoughts: bool = False):
900
  provider_model = normalize_model_name(model_name)
901
- if provider_model != DEEPSEEK_OPENROUTER_MODEL_NAME:
902
  return _build_chat_model_client(
903
  provider_model,
904
  include_thoughts=include_thoughts,
905
  )
906
- if not _has_openrouter_key() and _has_google_genai_key():
907
  logger.warning(
908
- "No OPENROUTER_API_KEY is set for %s; using Gemini fallback %s.",
909
- DEEPSEEK_OPENROUTER_MODEL_NAME,
910
  GEMINI_FALLBACK_MODEL_NAME,
911
  )
912
  return _build_chat_model_client(
@@ -917,9 +917,9 @@ def build_chat_model(model_name: str, include_thoughts: bool = False):
917
  if not _has_google_genai_key():
918
  logger.warning(
919
  "Gemini fallback %s is configured for %s, but no GOOGLE_API_KEY or "
920
- "GEMINI_API_KEY is set; using OpenRouter only.",
921
  GEMINI_FALLBACK_MODEL_NAME,
922
- DEEPSEEK_OPENROUTER_MODEL_NAME,
923
  )
924
  return model
925
  fallback = _build_chat_model_client(
 
72
  from .config import (
73
  BM25_INDEX_PATH,
74
  COURSE_SOURCE_KEYS,
75
+ DEEPSEEK_DIRECT_MODEL_NAME,
76
  DEFAULT_SELECTED_SOURCE_KEYS,
77
  DOCUMENT_DICT_PATH,
78
  GEMINI_FALLBACK_MODEL_NAME,
 
793
  return bool(os.environ.get("GOOGLE_API_KEY") or os.environ.get("GEMINI_API_KEY"))
794
 
795
 
796
+ def _has_deepseek_key() -> bool:
797
+ return bool(os.environ.get("DEEPSEEK_API_KEY"))
798
 
799
 
800
  def _build_chat_model_client(provider_model: str, include_thoughts: bool = False):
 
898
 
899
  def build_chat_model(model_name: str, include_thoughts: bool = False):
900
  provider_model = normalize_model_name(model_name)
901
+ if provider_model != DEEPSEEK_DIRECT_MODEL_NAME:
902
  return _build_chat_model_client(
903
  provider_model,
904
  include_thoughts=include_thoughts,
905
  )
906
+ if not _has_deepseek_key() and _has_google_genai_key():
907
  logger.warning(
908
+ "No DEEPSEEK_API_KEY is set for %s; using Gemini fallback %s.",
909
+ DEEPSEEK_DIRECT_MODEL_NAME,
910
  GEMINI_FALLBACK_MODEL_NAME,
911
  )
912
  return _build_chat_model_client(
 
917
  if not _has_google_genai_key():
918
  logger.warning(
919
  "Gemini fallback %s is configured for %s, but no GOOGLE_API_KEY or "
920
+ "GEMINI_API_KEY is set; using DeepSeek only.",
921
  GEMINI_FALLBACK_MODEL_NAME,
922
+ DEEPSEEK_DIRECT_MODEL_NAME,
923
  )
924
  return model
925
  fallback = _build_chat_model_client(
app/config.py CHANGED
@@ -69,14 +69,14 @@ KB_INDEX_PATH = f"{KB_DIR}/wiki/index.md"
69
  KB_AGENTS_PATH = f"{KB_DIR}/AGENTS.md"
70
  # In-git template, copied into data/kb/AGENTS.md by ensure_kb_agents_md().
71
  KB_AGENTS_TEMPLATE_PATH = "data/scraping_scripts/kb_agents_template.md"
72
- DEEPSEEK_OPENROUTER_MODEL_NAME = "openrouter:deepseek/deepseek-v4-flash"
73
  GEMINI_FALLBACK_MODEL_NAME = "google-genai:gemini-2.5-flash"
74
- DEFAULT_MODEL_NAME = DEEPSEEK_OPENROUTER_MODEL_NAME
75
 
76
  AVAILABLE_MODELS: tuple[dict[str, str], ...] = (
77
  {
78
- "id": DEEPSEEK_OPENROUTER_MODEL_NAME,
79
- "label": "DeepSeek V4 Flash (OpenRouter)",
80
  },
81
  {"id": GEMINI_FALLBACK_MODEL_NAME, "label": "Gemini 2.5 Flash"},
82
  {"id": "anthropic:claude-haiku-4-5", "label": "Claude Haiku 4.5"},
@@ -270,7 +270,7 @@ __all__ = [
270
  "DEFAULT_SELECTED_SOURCE_KEYS",
271
  "DEFAULT_SELECTED_SOURCES_UI",
272
  "DEFAULT_MODEL_NAME",
273
- "DEEPSEEK_OPENROUTER_MODEL_NAME",
274
  "GEMINI_FALLBACK_MODEL_NAME",
275
  "BM25_INDEX_PATH",
276
  "DOCUMENT_DICT_PATH",
 
69
  KB_AGENTS_PATH = f"{KB_DIR}/AGENTS.md"
70
  # In-git template, copied into data/kb/AGENTS.md by ensure_kb_agents_md().
71
  KB_AGENTS_TEMPLATE_PATH = "data/scraping_scripts/kb_agents_template.md"
72
+ DEEPSEEK_DIRECT_MODEL_NAME = "deepseek:deepseek-v4-flash"
73
  GEMINI_FALLBACK_MODEL_NAME = "google-genai:gemini-2.5-flash"
74
+ DEFAULT_MODEL_NAME = DEEPSEEK_DIRECT_MODEL_NAME
75
 
76
  AVAILABLE_MODELS: tuple[dict[str, str], ...] = (
77
  {
78
+ "id": DEEPSEEK_DIRECT_MODEL_NAME,
79
+ "label": "DeepSeek V4 Flash",
80
  },
81
  {"id": GEMINI_FALLBACK_MODEL_NAME, "label": "Gemini 2.5 Flash"},
82
  {"id": "anthropic:claude-haiku-4-5", "label": "Claude Haiku 4.5"},
 
270
  "DEFAULT_SELECTED_SOURCE_KEYS",
271
  "DEFAULT_SELECTED_SOURCES_UI",
272
  "DEFAULT_MODEL_NAME",
273
+ "DEEPSEEK_DIRECT_MODEL_NAME",
274
  "GEMINI_FALLBACK_MODEL_NAME",
275
  "BM25_INDEX_PATH",
276
  "DOCUMENT_DICT_PATH",
tests/manual_e2e_langsmith.md CHANGED
@@ -13,7 +13,7 @@ Run commands from the repository root.
13
  Required local artifacts and environment:
14
 
15
  - `.env` contains `COHERE_API_KEY`
16
- - `.env` contains `OPENROUTER_API_KEY`
17
  - `.env` contains `GEMINI_API_KEY` or `GOOGLE_API_KEY`
18
  - `.env` contains `LANGSMITH_API_KEY`
19
  - `.env` has `LANGSMITH_TRACING=true`
@@ -29,7 +29,7 @@ uv run dotenv -f .env run -- python - <<'PY'
29
  import os
30
  for key in [
31
  "COHERE_API_KEY",
32
- "OPENROUTER_API_KEY",
33
  "GEMINI_API_KEY",
34
  "GOOGLE_API_KEY",
35
  "LANGSMITH_API_KEY",
@@ -95,7 +95,7 @@ cat >/tmp/ai_tutor_e2e_payload.json <<'JSON'
95
  "transformers"
96
  ],
97
  "enabledTools": [],
98
- "model": "openrouter:deepseek/deepseek-v4-flash",
99
  "includeReasoning": true,
100
  "threadId": ""
101
  }
 
13
  Required local artifacts and environment:
14
 
15
  - `.env` contains `COHERE_API_KEY`
16
+ - `.env` contains `DEEPSEEK_API_KEY`
17
  - `.env` contains `GEMINI_API_KEY` or `GOOGLE_API_KEY`
18
  - `.env` contains `LANGSMITH_API_KEY`
19
  - `.env` has `LANGSMITH_TRACING=true`
 
29
  import os
30
  for key in [
31
  "COHERE_API_KEY",
32
+ "DEEPSEEK_API_KEY",
33
  "GEMINI_API_KEY",
34
  "GOOGLE_API_KEY",
35
  "LANGSMITH_API_KEY",
 
95
  "transformers"
96
  ],
97
  "enabledTools": [],
98
+ "model": "deepseek:deepseek-v4-flash",
99
  "includeReasoning": true,
100
  "threadId": ""
101
  }
tests/test_api.py CHANGED
@@ -75,8 +75,8 @@ class ApiTestCase(unittest.TestCase):
75
  )
76
  self.assertEqual(transformers["label"], "Transformers Docs")
77
  self.assertEqual(transformers["shortLabel"], "Transformers")
78
- self.assertEqual(body["model"], "openrouter:deepseek/deepseek-v4-flash")
79
- # DeepSeek via OpenRouter is the default model, so only local KB tools
80
  # are exposed until a provider with built-in web tools is selected.
81
  tool_keys = {tool["key"] for tool in tools}
82
  self.assertNotIn("web_search", tool_keys)
@@ -958,7 +958,7 @@ def live_chat_payload(
958
  "sourceKeys": ["peft", "transformers"],
959
  "enabledTools": enabled_tools or [],
960
  "model": os.getenv(
961
- "LIVE_API_E2E_MODEL", "openrouter:deepseek/deepseek-v4-flash"
962
  ),
963
  "includeReasoning": False,
964
  "threadId": thread_id,
 
75
  )
76
  self.assertEqual(transformers["label"], "Transformers Docs")
77
  self.assertEqual(transformers["shortLabel"], "Transformers")
78
+ self.assertEqual(body["model"], "deepseek:deepseek-v4-flash")
79
+ # DeepSeek direct is the default model, so only local KB tools
80
  # are exposed until a provider with built-in web tools is selected.
81
  tool_keys = {tool["key"] for tool in tools}
82
  self.assertNotIn("web_search", tool_keys)
 
958
  "sourceKeys": ["peft", "transformers"],
959
  "enabledTools": enabled_tools or [],
960
  "model": os.getenv(
961
+ "LIVE_API_E2E_MODEL", "deepseek:deepseek-v4-flash"
962
  ),
963
  "includeReasoning": False,
964
  "threadId": thread_id,
tests/test_chat_service.py CHANGED
@@ -9,7 +9,7 @@ from unittest.mock import MagicMock, patch
9
  from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage, ToolMessage
10
  from langchain_core.runnables.fallbacks import RunnableWithFallbacks
11
 
12
- from app.config import DEEPSEEK_OPENROUTER_MODEL_NAME, GEMINI_FALLBACK_MODEL_NAME
13
  from app.chat_service import (
14
  THREAD_IDLE_TTL_SECONDS,
15
  _claim_kb_command_budget,
@@ -318,39 +318,40 @@ class ChatServiceTestCase(unittest.TestCase):
318
  self.assertGreater(reasoning.max_tokens, 8192)
319
  self.assertEqual(reasoning.thinking, {"type": "enabled", "budget_tokens": 2048})
320
 
321
- def test_deepseek_openrouter_default_has_gemini_fallback(self) -> None:
322
  with patch.dict(
323
  os.environ,
324
  {
325
- "OPENROUTER_API_KEY": "openrouter-test-key",
326
  "GEMINI_API_KEY": "gemini-test-key",
327
  },
328
  clear=True,
329
  ):
330
- model = build_chat_model(DEEPSEEK_OPENROUTER_MODEL_NAME)
331
 
332
  self.assertIsInstance(model, RunnableWithFallbacks)
333
- self.assertEqual(model.runnable.model_name, "deepseek/deepseek-v4-flash")
 
334
  self.assertEqual(len(model.fallbacks), 1)
335
  self.assertEqual(
336
  model.fallbacks[0].model,
337
  GEMINI_FALLBACK_MODEL_NAME.partition(":")[2],
338
  )
339
 
340
- def test_deepseek_openrouter_default_skips_fallback_without_gemini_key(
341
  self,
342
  ) -> None:
343
  with patch.dict(
344
  os.environ,
345
- {"OPENROUTER_API_KEY": "openrouter-test-key"},
346
  clear=True,
347
  ):
348
- model = build_chat_model(DEEPSEEK_OPENROUTER_MODEL_NAME)
349
 
350
  self.assertNotIsInstance(model, RunnableWithFallbacks)
351
- self.assertEqual(model.model_name, "deepseek/deepseek-v4-flash")
352
 
353
- def test_deepseek_openrouter_default_uses_gemini_when_openrouter_key_missing(
354
  self,
355
  ) -> None:
356
  with patch.dict(
@@ -358,7 +359,7 @@ class ChatServiceTestCase(unittest.TestCase):
358
  {"GEMINI_API_KEY": "gemini-test-key"},
359
  clear=True,
360
  ):
361
- model = build_chat_model(DEEPSEEK_OPENROUTER_MODEL_NAME)
362
 
363
  self.assertNotIsInstance(model, RunnableWithFallbacks)
364
  self.assertEqual(model.model, GEMINI_FALLBACK_MODEL_NAME.partition(":")[2])
 
9
  from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage, ToolMessage
10
  from langchain_core.runnables.fallbacks import RunnableWithFallbacks
11
 
12
+ from app.config import DEEPSEEK_DIRECT_MODEL_NAME, GEMINI_FALLBACK_MODEL_NAME
13
  from app.chat_service import (
14
  THREAD_IDLE_TTL_SECONDS,
15
  _claim_kb_command_budget,
 
318
  self.assertGreater(reasoning.max_tokens, 8192)
319
  self.assertEqual(reasoning.thinking, {"type": "enabled", "budget_tokens": 2048})
320
 
321
+ def test_deepseek_direct_default_has_gemini_fallback(self) -> None:
322
  with patch.dict(
323
  os.environ,
324
  {
325
+ "DEEPSEEK_API_KEY": "deepseek-test-key",
326
  "GEMINI_API_KEY": "gemini-test-key",
327
  },
328
  clear=True,
329
  ):
330
+ model = build_chat_model(DEEPSEEK_DIRECT_MODEL_NAME)
331
 
332
  self.assertIsInstance(model, RunnableWithFallbacks)
333
+ self.assertEqual(model.runnable.model_name, "deepseek-v4-flash")
334
+ self.assertEqual(str(model.runnable.openai_api_base), "https://api.deepseek.com")
335
  self.assertEqual(len(model.fallbacks), 1)
336
  self.assertEqual(
337
  model.fallbacks[0].model,
338
  GEMINI_FALLBACK_MODEL_NAME.partition(":")[2],
339
  )
340
 
341
+ def test_deepseek_direct_default_skips_fallback_without_gemini_key(
342
  self,
343
  ) -> None:
344
  with patch.dict(
345
  os.environ,
346
+ {"DEEPSEEK_API_KEY": "deepseek-test-key"},
347
  clear=True,
348
  ):
349
+ model = build_chat_model(DEEPSEEK_DIRECT_MODEL_NAME)
350
 
351
  self.assertNotIsInstance(model, RunnableWithFallbacks)
352
+ self.assertEqual(model.model_name, "deepseek-v4-flash")
353
 
354
+ def test_deepseek_direct_default_uses_gemini_when_deepseek_key_missing(
355
  self,
356
  ) -> None:
357
  with patch.dict(
 
359
  {"GEMINI_API_KEY": "gemini-test-key"},
360
  clear=True,
361
  ):
362
+ model = build_chat_model(DEEPSEEK_DIRECT_MODEL_NAME)
363
 
364
  self.assertNotIsInstance(model, RunnableWithFallbacks)
365
  self.assertEqual(model.model, GEMINI_FALLBACK_MODEL_NAME.partition(":")[2])
tests/test_config.py CHANGED
@@ -11,9 +11,9 @@ import pytest
11
  from app import config
12
 
13
 
14
- def test_default_chat_model_prefers_deepseek_openrouter_with_gemini_fallback() -> None:
15
- assert config.DEFAULT_MODEL_NAME == config.DEEPSEEK_OPENROUTER_MODEL_NAME
16
- assert config.DEFAULT_MODEL_NAME == "openrouter:deepseek/deepseek-v4-flash"
17
  assert config.GEMINI_FALLBACK_MODEL_NAME == "google-genai:gemini-2.5-flash"
18
  assert config.AVAILABLE_MODELS[0]["id"] == config.DEFAULT_MODEL_NAME
19
  assert any(
 
11
  from app import config
12
 
13
 
14
+ def test_default_chat_model_prefers_deepseek_direct_with_gemini_fallback() -> None:
15
+ assert config.DEFAULT_MODEL_NAME == config.DEEPSEEK_DIRECT_MODEL_NAME
16
+ assert config.DEFAULT_MODEL_NAME == "deepseek:deepseek-v4-flash"
17
  assert config.GEMINI_FALLBACK_MODEL_NAME == "google-genai:gemini-2.5-flash"
18
  assert config.AVAILABLE_MODELS[0]["id"] == config.DEFAULT_MODEL_NAME
19
  assert any(