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
4 changes: 4 additions & 0 deletions src/fast_agent/cli/commands/go.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ async def _run_agent(

fast = FastAgent(**fast_kwargs)

# Set model on args so model source detection works correctly
if model:
fast.args.model = model

if shell_runtime:
await fast.app.initialize()
setattr(fast.app.context, "shell_runtime", True)
Expand Down
3 changes: 2 additions & 1 deletion src/fast_agent/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,10 +548,11 @@ class Settings(BaseSettings):
execution_engine: Literal["asyncio"] = "asyncio"
"""Execution engine for the fast-agent application"""

default_model: str | None = "gpt-5-mini.low"
default_model: str | None = None
"""
Default model for agents. Format is provider.model_name.<reasoning_effort>, for example openai.o3-mini.low
Aliases are provided for common models e.g. sonnet, haiku, gpt-4.1, o3-mini etc.
If not set, falls back to FAST_AGENT_MODEL env var, then to "gpt-5-mini.low".
"""

auto_sampling: bool = True
Expand Down
51 changes: 49 additions & 2 deletions src/fast_agent/core/direct_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Implements type-safe factories with improved error handling.
"""

import os
from functools import partial
from typing import Any, Protocol, TypeVar

Expand Down Expand Up @@ -81,6 +82,9 @@ async def __call__(
) -> AgentDict: ...


HARDCODED_DEFAULT_MODEL = "gpt-5-mini.low"


def get_model_factory(
context,
model: str | None = None,
Expand All @@ -92,6 +96,13 @@ def get_model_factory(
Get model factory using specified or default model.
Model string is parsed by ModelFactory to determine provider and reasoning effort.

Precedence (lowest to highest):
1. Hardcoded default (gpt-5-mini.low)
2. FAST_AGENT_MODEL environment variable
3. Config file default_model
4. CLI --model argument
5. Decorator model parameter

Args:
context: Application context
model: Optional model specification string (highest precedence)
Expand All @@ -102,8 +113,17 @@ def get_model_factory(
Returns:
ModelFactory instance for the specified or default model
"""
# Config has lowest precedence
model_spec = default_model or context.config.default_model
# Hardcoded default has lowest precedence
model_spec = HARDCODED_DEFAULT_MODEL

# Environment variable has next precedence
env_model = os.getenv("FAST_AGENT_MODEL")
if env_model:
model_spec = env_model

# Config has next precedence
if default_model or context.config.default_model:
model_spec = default_model or context.config.default_model

# Command line override has next precedence
if cli_model:
Expand All @@ -123,6 +143,33 @@ def get_model_factory(
return ModelFactory.create_factory(model_spec)


def get_default_model_source(
config_default_model: str | None = None,
cli_model: str | None = None,
) -> str | None:
"""
Determine the source of the default model selection.
Returns "environment variable", "config file", or None (if CLI or hardcoded default).

This is used to display informational messages about where the model
configuration is coming from. Only shows a message for env var or config file,
not for explicit CLI usage or the hardcoded system default.
"""
# CLI model is explicit - no message needed
if cli_model:
return None

# Check if config file has a default model
if config_default_model:
return "config file"

# Check if environment variable is set
if os.getenv("FAST_AGENT_MODEL"):
return "environment variable"

return None


async def create_agents_by_type(
app_instance: Core,
agents_dict: AgentConfigDict,
Expand Down
10 changes: 10 additions & 0 deletions src/fast_agent/core/fastagent.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
)
from fast_agent.core.direct_factory import (
create_agents_in_dependency_order,
get_default_model_source,
get_model_factory,
)
from fast_agent.core.error_handling import handle_error
Expand Down Expand Up @@ -510,6 +511,15 @@ async def run(self) -> AsyncIterator["AgentApp"]:
):
quiet_mode = True
cli_model_override = getattr(self.args, "model", None)

# Store the model source for UI display
model_source = get_default_model_source(
config_default_model=self.context.config.default_model,
cli_model=cli_model_override,
)
if self.context.config:
self.context.config.model_source = model_source # type: ignore[attr-defined]

tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span(self.name):
try:
Expand Down
5 changes: 5 additions & 0 deletions src/fast_agent/ui/enhanced_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,11 @@ def _style_flag(letter: str, supported: bool) -> str:
f"[dim]Experimental: Streaming Enabled - {streaming_mode} mode[/dim]"
)

# Show model source if configured via env var or config file
model_source = getattr(agent_context.config, "model_source", None)
if model_source:
rich_print(f"[dim]Model selected via {model_source}[/dim]")

if shell_enabled:
modes_display = ", ".join(shell_access_modes or ("direct",))
shell_display = f"{modes_display}, {shell_name}" if shell_name else modes_display
Expand Down
102 changes: 102 additions & 0 deletions tests/unit/fast_agent/core/test_model_selection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""
Tests for model selection source detection logic.
"""

import os

from fast_agent.core.direct_factory import get_default_model_source


class TestGetDefaultModelSource:
"""Tests for get_default_model_source function."""

def test_cli_model_returns_none(self):
"""When CLI model is specified, returns None (no message needed)."""
result = get_default_model_source(
config_default_model="sonnet",
cli_model="haiku",
)
assert result is None

def test_config_model_returns_config_file(self):
"""When config model is set and no CLI, returns 'config file'."""
result = get_default_model_source(
config_default_model="sonnet",
cli_model=None,
)
assert result == "config file"

def test_env_var_returns_environment_variable(self):
"""When env var is set and no config/CLI, returns 'environment variable'."""
# Store original value if any
original = os.environ.get("FAST_AGENT_MODEL")

try:
os.environ["FAST_AGENT_MODEL"] = "gpt-4o"
result = get_default_model_source(
config_default_model=None,
cli_model=None,
)
assert result == "environment variable"
finally:
# Restore original state
if original is not None:
os.environ["FAST_AGENT_MODEL"] = original
elif "FAST_AGENT_MODEL" in os.environ:
del os.environ["FAST_AGENT_MODEL"]

def test_no_source_returns_none(self):
"""When nothing is set, returns None (hardcoded default used)."""
# Store original value if any
original = os.environ.get("FAST_AGENT_MODEL")

try:
# Ensure env var is not set
if "FAST_AGENT_MODEL" in os.environ:
del os.environ["FAST_AGENT_MODEL"]

result = get_default_model_source(
config_default_model=None,
cli_model=None,
)
assert result is None
finally:
# Restore original state
if original is not None:
os.environ["FAST_AGENT_MODEL"] = original

def test_config_takes_precedence_over_env_var(self):
"""Config file setting takes precedence over environment variable."""
original = os.environ.get("FAST_AGENT_MODEL")

try:
os.environ["FAST_AGENT_MODEL"] = "gpt-4o"
result = get_default_model_source(
config_default_model="sonnet",
cli_model=None,
)
# Config is checked first, so should return "config file"
assert result == "config file"
finally:
if original is not None:
os.environ["FAST_AGENT_MODEL"] = original
elif "FAST_AGENT_MODEL" in os.environ:
del os.environ["FAST_AGENT_MODEL"]

def test_cli_takes_precedence_over_all(self):
"""CLI model takes precedence over config and env var."""
original = os.environ.get("FAST_AGENT_MODEL")

try:
os.environ["FAST_AGENT_MODEL"] = "gpt-4o"
result = get_default_model_source(
config_default_model="sonnet",
cli_model="haiku",
)
# CLI is explicit, so should return None
assert result is None
finally:
if original is not None:
os.environ["FAST_AGENT_MODEL"] = original
elif "FAST_AGENT_MODEL" in os.environ:
del os.environ["FAST_AGENT_MODEL"]
Loading