Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ GOOGLE_APPLICATION_CREDENTIALS=keys/credentials.json
# (e.g. us-central1) — these models are not published in the global publisher catalog.
VERTEX_API_EMBEDDINGS_REGION=us-central1
# Region for generative-AI calls (Gemini etc.). Can be a regional location or "global".
VERTEX_API_GEN_AI_REGION=us-central1
VERTEX_API_GEN_AI_REGION=global
EMBEDDINGS_SERVICE_NAME=GOOGLE-VERTEX-AI
# See https://cloud.google.com/vertex-ai/generative-ai/docs/embeddings/get-text-embeddings#supported-models e.g.:
# text-embedding-005
Expand Down Expand Up @@ -104,11 +104,13 @@ LLM_PROVIDER=gemini
# LLM_REASONING_MODEL — classifiers, routing and extraction that need good reasoning
# LLM_DEEP_REASONING_MODEL — the strongest reasoning, e.g. evaluation judges
# When a tier is unset:
# gemini: uses its own default (gemini-2.5-flash-lite / gemini-2.5-flash / gemini-2.5-pro)
# gemini: uses its own default (gemini-3.5-flash-lite / gemini-3.8-flash / gemini-3.1-pro-preview)
# anthropic: falls back to the next tier down that is set
# (LLM_DEEP_REASONING_MODEL -> LLM_REASONING_MODEL -> LLM_DEFAULT_MODEL), then to claude-sonnet-4-6
# ollama: same as anthropic, then to qwen2.5:7b
# To use one model for everything with a non-Gemini provider, set only LLM_DEFAULT_MODEL.
# Gemini models are called through the google-genai Interactions API, which only supports
# gemini-3.5-flash-lite, gemini-3.8-flash and gemini-3.1-pro-preview (the 2.5 models are rejected).
# LLM_DEFAULT_MODEL=
# LLM_REASONING_MODEL=
# LLM_DEEP_REASONING_MODEL=
Expand Down
9 changes: 5 additions & 4 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -567,13 +567,14 @@ For more details, see [app/agent/preference_elicitation_agent/README.md](app/age
### AI & Embeddings

- `VERTEX_API_EMBEDDINGS_REGION` - Vertex AI region for embedding models (must be a regional location, e.g. `us-central1`)
- `VERTEX_API_GEN_AI_REGION` - Vertex AI region for generative-AI calls (regional or `global`, default: `us-central1`)
- `VERTEX_API_GEN_AI_REGION` - Vertex AI region for generative-AI calls (regional or `global`, default: `global`). The default and reasoning Gemini 3.x models are only available in `global`
- `EMBEDDINGS_SERVICE_NAME` - Embeddings service provider
- `EMBEDDINGS_MODEL_NAME` - Model for generating embeddings
- `LLM_PROVIDER` - LLM backend for all agents: `gemini` (default), `anthropic` or `ollama`
- `LLM_DEFAULT_MODEL` - Model for the default tier (gemini default: `gemini-2.5-flash-lite`)
- `LLM_REASONING_MODEL` - Model for the reasoning tier (gemini default: `gemini-2.5-flash`)
- `LLM_DEEP_REASONING_MODEL` - Model for the deep reasoning tier (gemini default: `gemini-2.5-pro`)
- `LLM_DEFAULT_MODEL` - Model for the default tier (gemini default: `gemini-3.5-flash-lite`)
- `LLM_REASONING_MODEL` - Model for the reasoning tier (gemini default: `gemini-3.8-flash`)
- `LLM_DEEP_REASONING_MODEL` - Model for the deep reasoning tier (gemini default: `gemini-3.1-pro-preview`)
- Gemini models are called through the google-genai Interactions API, which only supports `gemini-3.5-flash-lite`, `gemini-3.8-flash` and `gemini-3.1-pro-preview` (the 2.5 models are rejected)
- For `anthropic` and `ollama`, an unset tier falls back to the next tier down that is set (`LLM_DEEP_REASONING_MODEL` → `LLM_REASONING_MODEL` → `LLM_DEFAULT_MODEL`), then to the provider default (`claude-sonnet-4-6` / `qwen2.5:7b`)
- `ANTHROPIC_API_KEY` - Anthropic API key, required when `LLM_PROVIDER=anthropic`
- `OLLAMA_BASE_URL` - Ollama server URL when `LLM_PROVIDER=ollama` (default: `http://localhost:11434`)
Expand Down
14 changes: 8 additions & 6 deletions backend/app/agent/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
from typing import Literal, Final

Model = Literal[
"gemini-2.5-flash-lite",
"gemini-2.5-flash",
"gemini-2.5-pro",
"gemini-3.5-flash-lite",
"gemini-3.8-flash",
"gemini-3.1-pro-preview",
]


Expand Down Expand Up @@ -39,10 +39,12 @@ class ModelTier(StrEnum):


GEMINI_DEFAULT_MODELS: Final[dict[ModelTier, Model]] = {
ModelTier.DEFAULT: "gemini-2.5-flash-lite",
ModelTier.REASONING: "gemini-2.5-flash",
ModelTier.DEEP_REASONING: "gemini-2.5-pro",
ModelTier.DEFAULT: "gemini-3.5-flash-lite",
ModelTier.REASONING: "gemini-3.8-flash",
ModelTier.DEEP_REASONING: "gemini-3.1-pro-preview",
}
"""
The Gemini model used for each tier when the tier's environment variable is not set.
The Gemini 3.x models are only served from the Vertex AI `global` location, see `VERTEX_API_GEN_AI_REGION`.
These are models the google-genai Interactions API supports, it rejects the Gemini 2.5 models.
"""
6 changes: 3 additions & 3 deletions backend/app/app_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,22 +143,22 @@ class ApplicationConfig(BaseModel):
llm_default_model: Optional[str] = None
"""
The model for the default tier (ModelTier.DEFAULT) of the selected provider.
When unset, each provider uses its own default (gemini: gemini-2.5-flash-lite,
When unset, each provider uses its own default (gemini: gemini-3.5-flash-lite,
anthropic: claude-sonnet-4-6, ollama: qwen2.5:7b).
Set via LLM_DEFAULT_MODEL environment variable.
"""

llm_reasoning_model: Optional[str] = None
"""
The model for the reasoning tier (ModelTier.REASONING) of the selected provider.
When unset, gemini uses gemini-2.5-flash; other providers fall back to llm_default_model.
When unset, gemini uses gemini-3.8-flash; other providers fall back to llm_default_model.
Set via LLM_REASONING_MODEL environment variable.
"""

llm_deep_reasoning_model: Optional[str] = None
"""
The model for the deep reasoning tier (ModelTier.DEEP_REASONING) of the selected provider.
When unset, gemini uses gemini-2.5-pro; other providers fall back to llm_reasoning_model, then llm_default_model.
When unset, gemini uses gemini-3.1-pro-preview; other providers fall back to llm_reasoning_model, then llm_default_model.
Set via LLM_DEEP_REASONING_MODEL environment variable.
"""

Expand Down
7 changes: 4 additions & 3 deletions backend/common_libs/llm/models_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@

DEFAULT_VERTEX_API_GEN_AI_REGION = os.getenv("VERTEX_API_GEN_AI_REGION")
if not DEFAULT_VERTEX_API_GEN_AI_REGION:
logging.warning("VERTEX_API_GEN_AI_REGION is not set. Using 'us-central1' as the default region.")
DEFAULT_VERTEX_API_GEN_AI_REGION = "us-central1"
# The Gemini 3.x models are only served from the global location, regional locations return a 404 for them.
logging.warning("VERTEX_API_GEN_AI_REGION is not set. Using 'global' as the default region.")
DEFAULT_VERTEX_API_GEN_AI_REGION = "global"
else:
logging.info("Default Vertex AI gen-AI region is %s", DEFAULT_VERTEX_API_GEN_AI_REGION)

Expand Down Expand Up @@ -167,7 +168,7 @@ def resolve_model_name(*, tier: ModelTier, provider: "LLMProvider") -> str:
ModelTier.REASONING: app_config.llm_reasoning_model,
ModelTier.DEEP_REASONING: app_config.llm_deep_reasoning_model,
}
# Gemini falls back to its own 2.5 default for the tier rather than to another tier's model.
# Gemini falls back to its own default for the tier rather than to another tier's model.
tiers_to_try = [tier] if provider == "gemini" else [tier, *_NON_GEMINI_FALLBACK_TIERS[tier]]
for candidate_tier in tiers_to_try:
if configured_models[candidate_tier]:
Expand Down
36 changes: 18 additions & 18 deletions backend/common_libs/llm/test_model_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,19 @@ def _set_llm_config(config: ApplicationConfig, *,

class TestResolveModelName:
@pytest.mark.parametrize("given_tier, expected_model", [
(ModelTier.DEFAULT, "gemini-2.5-flash-lite"),
(ModelTier.REASONING, "gemini-2.5-flash"),
(ModelTier.DEEP_REASONING, "gemini-2.5-pro"),
(ModelTier.DEFAULT, "gemini-3.5-flash-lite"),
(ModelTier.REASONING, "gemini-3.8-flash"),
(ModelTier.DEEP_REASONING, "gemini-3.1-pro-preview"),
])
def test_gemini_uses_gemini_2_5_defaults_when_no_model_is_set(self, setup_application_config,
def test_gemini_uses_gemini_defaults_when_no_model_is_set(self, setup_application_config,
given_tier: ModelTier, expected_model: str):
# GIVEN the gemini provider is configured without any tier models
_set_llm_config(setup_application_config, provider="gemini")

# WHEN the model name for the given tier is resolved
actual_model = resolve_model_name(tier=given_tier, provider="gemini")

# THEN expect the gemini 2.5 default for the tier
# THEN expect the gemini default for the tier
assert actual_model == expected_model

@pytest.mark.parametrize("given_tier, expected_model", [
Expand All @@ -63,9 +63,9 @@ def test_uses_the_tier_model_when_it_is_set(self, setup_application_config,
assert actual_model == expected_model

@pytest.mark.parametrize("given_reasoning_model, given_tier, expected_model", [
(None, ModelTier.REASONING, "gemini-2.5-flash"),
(None, ModelTier.DEEP_REASONING, "gemini-2.5-pro"),
("given-reasoning", ModelTier.DEEP_REASONING, "gemini-2.5-pro"),
(None, ModelTier.REASONING, "gemini-3.8-flash"),
(None, ModelTier.DEEP_REASONING, "gemini-3.1-pro-preview"),
("given-reasoning", ModelTier.DEEP_REASONING, "gemini-3.1-pro-preview"),
])
def test_gemini_does_not_fall_back_to_another_tier(self, setup_application_config,
given_reasoning_model: Optional[str],
Expand All @@ -78,7 +78,7 @@ def test_gemini_does_not_fall_back_to_another_tier(self, setup_application_confi
# WHEN the model name for a tier without a model is resolved
actual_model = resolve_model_name(tier=given_tier, provider="gemini")

# THEN expect the gemini 2.5 default for the tier, not another tier's model
# THEN expect the gemini default for the tier, not another tier's model
assert actual_model == expected_model

@pytest.mark.parametrize("given_tier", list(ModelTier))
Expand Down Expand Up @@ -144,9 +144,9 @@ def test_non_gemini_default_tier_does_not_fall_back_to_a_higher_tier(self, setup
assert actual_model == expected_model

@pytest.mark.parametrize("given_tier, expected_model", [
(ModelTier.DEFAULT, "gemini-2.5-flash-lite"),
(ModelTier.REASONING, "gemini-2.5-flash"),
(ModelTier.DEEP_REASONING, "gemini-2.5-pro"),
(ModelTier.DEFAULT, "gemini-3.5-flash-lite"),
(ModelTier.REASONING, "gemini-3.8-flash"),
(ModelTier.DEEP_REASONING, "gemini-3.1-pro-preview"),
])
def test_ignores_the_models_of_another_provider(self, setup_application_config,
given_tier: ModelTier, expected_model: str):
Expand All @@ -159,13 +159,13 @@ def test_ignores_the_models_of_another_provider(self, setup_application_config,
# WHEN the model name for the given tier is resolved for gemini (e.g. a gemini-only call site)
actual_model = resolve_model_name(tier=given_tier, provider="gemini")

# THEN expect the gemini 2.5 default for the tier
# THEN expect the gemini default for the tier
assert actual_model == expected_model

@pytest.mark.parametrize("given_tier, expected_model", [
(ModelTier.DEFAULT, "gemini-2.5-flash-lite"),
(ModelTier.REASONING, "gemini-2.5-flash"),
(ModelTier.DEEP_REASONING, "gemini-2.5-pro"),
(ModelTier.DEFAULT, "gemini-3.5-flash-lite"),
(ModelTier.REASONING, "gemini-3.8-flash"),
(ModelTier.DEEP_REASONING, "gemini-3.1-pro-preview"),
])
def test_uses_the_defaults_when_the_application_config_is_not_set(self, given_tier: ModelTier,
expected_model: str):
Expand All @@ -175,7 +175,7 @@ def test_uses_the_defaults_when_the_application_config_is_not_set(self, given_ti
# WHEN the model name for the given tier is resolved
actual_model = resolve_model_name(tier=given_tier, provider="gemini")

# THEN expect the gemini 2.5 default for the tier
# THEN expect the gemini default for the tier
assert actual_model == expected_model


Expand Down Expand Up @@ -214,7 +214,7 @@ def test_explicit_model_name_is_used_as_is(self, setup_application_config):

class TestGeminiGenerativeLLM:
@pytest.mark.parametrize("given_config, expected_model", [
(LLMConfig(), "gemini-2.5-flash-lite"),
(LLMConfig(), "gemini-3.5-flash-lite"),
(LLMConfig(model_tier=ModelTier.DEEP_REASONING), "given-deep-reasoning"),
(LLMConfig(model_tier=ModelTier.DEEP_REASONING, language_model_name="given-explicit-model"),
"given-explicit-model"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ async def agent_director_exec(caplog: LogCaptureFixture, test_case: ScriptedUser


@pytest.mark.asyncio
@pytest.mark.evaluation_test("gemini-2.5-flash-lite/")
@pytest.mark.evaluation_test("gemini-3.5-flash-lite/")
@pytest.mark.repeat(3)
async def test_user_says_all_the_time_yes(caplog: LogCaptureFixture,
setup_agent_director: Awaitable[tuple[ConversationMemoryManager, Callable[
Expand Down Expand Up @@ -142,7 +142,7 @@ async def test_user_says_all_the_time_yes(caplog: LogCaptureFixture,


@pytest.mark.asyncio
@pytest.mark.evaluation_test("gemini-2.5-flash-lite/")
@pytest.mark.evaluation_test("gemini-3.5-flash-lite/")
@pytest.mark.repeat(3)
async def test_user_talks_about_occupations(caplog: LogCaptureFixture,
setup_agent_director: Awaitable[tuple[ConversationMemoryManager, Callable[
Expand Down Expand Up @@ -206,7 +206,7 @@ class AgentState:


@pytest.mark.asyncio
@pytest.mark.evaluation_test("gemini-2.5-flash-lite/")
@pytest.mark.evaluation_test("gemini-3.5-flash-lite/")
@pytest.mark.repeat(3)
async def test_argentina_counseling_flow(caplog: LogCaptureFixture,
setup_agent_director: Awaitable[tuple[ConversationMemoryManager, Callable[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def career_explorer_config_with_sectors(setup_multi_locale_app_config):


@pytest.mark.asyncio
@pytest.mark.evaluation_test("gemini-2.5-flash-lite/")
@pytest.mark.evaluation_test("gemini-3.5-flash-lite/")
@pytest.mark.parametrize("test_case", TEST_CASES, ids=[tc.name for tc in TEST_CASES])
async def test_career_explorer_sector_content(
evals_setup, setup_multi_locale_app_config, career_explorer_config_with_sectors, test_case: SectorContentTestCase
Expand Down Expand Up @@ -239,7 +239,7 @@ async def test_career_explorer_sector_content(


@pytest.mark.asyncio
@pytest.mark.evaluation_test("gemini-2.5-flash-lite/")
@pytest.mark.evaluation_test("gemini-3.5-flash-lite/")
@pytest.mark.parametrize(
"test_case", TEST_CASES_GENERAL_KNOWLEDGE, ids=[tc.name for tc in TEST_CASES_GENERAL_KNOWLEDGE]
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def career_explorer_config_with_sectors(setup_multi_locale_app_config):


@pytest.mark.asyncio
@pytest.mark.evaluation_test("gemini-2.5-flash-lite/")
@pytest.mark.evaluation_test("gemini-3.5-flash-lite/")
@pytest.mark.parametrize("test_case", NON_PRIORITY_EXPLORER_TEST_CASES, ids=[tc.name for tc in NON_PRIORITY_EXPLORER_TEST_CASES])
async def test_non_priority_sector_explorer_web_search(
evals_setup,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def career_explorer_config_with_sectors(setup_multi_locale_app_config):


@pytest.mark.asyncio
@pytest.mark.evaluation_test("gemini-2.5-flash-lite/")
@pytest.mark.evaluation_test("gemini-3.5-flash-lite/")
@pytest.mark.parametrize("test_case", PRIORITY_EXPLORER_TEST_CASES, ids=[tc.name for tc in PRIORITY_EXPLORER_TEST_CASES])
async def test_priority_sector_explorer_rag_content(
evals_setup,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def career_explorer_config_with_sectors(setup_multi_locale_app_config):


@pytest.mark.asyncio
@pytest.mark.evaluation_test("gemini-2.5-flash-lite/")
@pytest.mark.evaluation_test("gemini-3.5-flash-lite/")
@pytest.mark.parametrize("test_case", CLASSIFIER_TEST_CASES, ids=[tc.name for tc in CLASSIFIER_TEST_CASES])
async def test_sector_relevance_classifier(
evals_setup,
Expand Down Expand Up @@ -116,7 +116,7 @@ async def test_sector_relevance_classifier(


@pytest.mark.asyncio
@pytest.mark.evaluation_test("gemini-2.5-flash-lite/")
@pytest.mark.evaluation_test("gemini-3.5-flash-lite/")
async def test_classifier_extracts_multiple_sectors(
evals_setup,
career_explorer_config_with_sectors,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ def _save_conversation_output(


@pytest.mark.asyncio
@pytest.mark.evaluation_test("gemini-2.5-flash-lite/")
@pytest.mark.evaluation_test("gemini-3.5-flash-lite/")
@pytest.mark.parametrize("test_case", TEST_CASES, ids=[tc.name for tc in TEST_CASES])
async def test_career_readiness_scripted(evals_setup, setup_multi_locale_app_config,
test_case: CareerReadinessTestCase):
Expand Down Expand Up @@ -285,7 +285,7 @@ async def test_career_readiness_scripted(evals_setup, setup_multi_locale_app_con


@pytest.mark.asyncio
@pytest.mark.evaluation_test("gemini-2.5-flash-lite/")
@pytest.mark.evaluation_test("gemini-3.5-flash-lite/")
@pytest.mark.parametrize("test_case", EDGE_CASE_TEST_CASES, ids=[tc.name for tc in EDGE_CASE_TEST_CASES])
async def test_career_readiness_edge_cases(evals_setup, setup_multi_locale_app_config,
test_case: CareerReadinessEdgeCaseTestCase):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -999,7 +999,7 @@ class _TestCaseDataExtraction(CompassTestCase):


@pytest.mark.asyncio
@pytest.mark.evaluation_test("gemini-2.5-flash-lite/")
@pytest.mark.evaluation_test("gemini-3.5-flash-lite/")
@pytest.mark.repeat(3)
@pytest.mark.parametrize('test_case', get_test_cases_to_run(test_cases_data_extraction),
ids=[case.name for case in get_test_cases_to_run(test_cases_data_extraction)])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ async def collect_experiences_exec(caplog: LogCaptureFixture, test_case: Scripte


@pytest.mark.asyncio
@pytest.mark.evaluation_test("gemini-2.5-flash-lite/")
@pytest.mark.evaluation_test("gemini-3.5-flash-lite/")
@pytest.mark.repeat(3)
async def test_kenyan_user_multiple_experiences_transition_issues(caplog: LogCaptureFixture,
setup_collect_experiences_agent: Awaitable[tuple[
Expand Down Expand Up @@ -201,7 +201,7 @@ async def test_stuck_after_recap_confirmation(caplog: LogCaptureFixture,


@pytest.mark.asyncio
@pytest.mark.evaluation_test("gemini-2.5-flash-lite/")
@pytest.mark.evaluation_test("gemini-3.5-flash-lite/")
@pytest.mark.repeat(3)
async def test_quick_reply_options_appear_at_yes_no_questions(caplog: LogCaptureFixture,
setup_collect_experiences_agent: Awaitable[tuple[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@


@pytest.mark.asyncio
@pytest.mark.evaluation_test("gemini-2.5-flash-lite/")
@pytest.mark.evaluation_test("gemini-3.5-flash-lite/")
@pytest.mark.repeat(3)
@pytest.mark.parametrize('test_case', get_test_cases_to_run(test_cases),
ids=[case.name for case in get_test_cases_to_run(test_cases)])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ class EntityExtractionToolTestCase(CompassTestCase):

@pytest.mark.asyncio
@pytest.mark.repeat(3)
@pytest.mark.evaluation_test("gemini-2.5-flash-lite/")
@pytest.mark.evaluation_test("gemini-3.5-flash-lite/")
@pytest.mark.parametrize('test_case', get_test_cases_to_run(test_cases),
ids=[case.name for case in get_test_cases_to_run(test_cases)])
async def test_entity_extraction_tool(test_case: EntityExtractionToolTestCase, caplog):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ class IntentAnalyzerToolTestCase(CompassTestCase):

@pytest.mark.asyncio
@pytest.mark.repeat(3)
@pytest.mark.evaluation_test("gemini-2.5-flash-lite/")
@pytest.mark.evaluation_test("gemini-3.5-flash-lite/")
@pytest.mark.parametrize('test_case', get_test_cases_to_run(test_cases),
ids=[case.name for case in get_test_cases_to_run(test_cases)])
async def test_intent_analyzer_tool(test_case: IntentAnalyzerToolTestCase, caplog):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ class TemporalAndWorkTypeClassifierToolTestCase(CompassTestCase):

@pytest.mark.asyncio
@pytest.mark.repeat(3)
@pytest.mark.evaluation_test("gemini-2.5-flash-lite/")
@pytest.mark.evaluation_test("gemini-3.5-flash-lite/")
@pytest.mark.parametrize('test_case', get_test_cases_to_run(test_cases),
ids=[case.name for case in get_test_cases_to_run(test_cases)])
async def test_temporal_and_work_type_classification(test_case: TemporalAndWorkTypeClassifierToolTestCase, caplog,
Expand Down
Loading
Loading