mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-22 01:35:28 -04:00
fix(cli): allow TUI launch without preconfigured model credentials (#3301)
The CLI no longer requires a preconfigured model or environment credentials to open. When no default model can be resolved on startup, the TUI launches in a deferred state with inline guidance instead of exiting with an error code. ## Changes - Launch the TUI even when no model credentials are configured. `run_textual_cli_async` catches the no-credentials `ModelConfigError`, sets `defer_server_start=True`, and opens the app rather than printing an error and exiting. - Defer server startup inside `DeepAgentsApp` via a new `defer_server_start` init flag. The UI is fully usable, but the backing server remains paused until the user runs `/auth`, `/model`, or `/reload`. - Auto-resume deferred startup once a model is available. `_maybe_start_deferred_server_from_default()` checks for a resolved default model after auth flows and automatically transitions from deferred to connecting state. - First-launch guidance. `_mount_deferred_start_notice()` displays a message pointing users to `/model` when they land in a session with no credentials and no pre-selected model. - Updated `_get_default_model_spec` to consider stored TUI credentials (`auth_store.get_stored_key`) and `get_provider_auth_status()` rather than only environment variables, making the fallback chain more robust for deferred-start scenarios. - Preserve initial prompt (`-m`) submissions across deferred startup. The prompt is held until `on_deep_agents_app_server_ready` fires instead of being consumed before the agent exists.
This commit is contained in:
@@ -92,6 +92,11 @@ from deepagents_cli.widgets.welcome import WelcomeBanner
|
||||
logger = logging.getLogger(__name__)
|
||||
_monotonic = time.monotonic
|
||||
|
||||
_DEFERRED_START_NOTICE = (
|
||||
"No model is configured yet. Run `/model` to choose one. "
|
||||
"Deep Agents will ask for credentials for the selected provider."
|
||||
)
|
||||
|
||||
# Serializes process-local read-modify-write operations for `config.toml`.
|
||||
# Without this, overlapping global-theme and per-terminal-theme saves can each
|
||||
# read the same pre-mutation state and then clobber the other's keys.
|
||||
@@ -1054,6 +1059,7 @@ class DeepAgentsApp(App):
|
||||
server_kwargs: dict[str, Any] | None = None,
|
||||
mcp_preload_kwargs: dict[str, Any] | None = None,
|
||||
model_kwargs: dict[str, Any] | None = None,
|
||||
defer_server_start: bool = False,
|
||||
title: str | None = None,
|
||||
sub_title: str | None = None,
|
||||
**kwargs: Any,
|
||||
@@ -1106,6 +1112,8 @@ class DeepAgentsApp(App):
|
||||
|
||||
When provided, model creation runs in a background worker after
|
||||
first paint instead of blocking startup.
|
||||
defer_server_start: Whether to keep CLI-owned server startup paused
|
||||
until the user configures credentials or explicitly picks a model.
|
||||
title: Override the Textual `App.title` shown in the optional
|
||||
header bar.
|
||||
|
||||
@@ -1270,12 +1278,21 @@ class DeepAgentsApp(App):
|
||||
"""Cached kwargs for `start_server_and_get_agent`.
|
||||
|
||||
When non-`None`, startup is deferred and the UI begins in
|
||||
"Connecting..." state.
|
||||
"Connecting..." state unless `_server_startup_deferred` is set.
|
||||
|
||||
Re-used so downstream features that restart the server (e.g. `/agents`)
|
||||
start from the same config.
|
||||
"""
|
||||
|
||||
self._server_startup_deferred = defer_server_start
|
||||
"""True when no model can be selected yet, usually first launch with
|
||||
no credentials. The TUI is usable, but server startup waits for
|
||||
`/auth`, `/reload`, or `/model`.
|
||||
"""
|
||||
|
||||
self._server_startup_deferred_notice_shown = False
|
||||
"""Whether the first-launch no-model guidance has been mounted."""
|
||||
|
||||
self._mcp_preload_kwargs = mcp_preload_kwargs
|
||||
"""Kwargs for `_preload_session_mcp_server_info`, run concurrently
|
||||
with server startup when `server_kwargs` is set."""
|
||||
@@ -1356,7 +1373,9 @@ class DeepAgentsApp(App):
|
||||
"""
|
||||
|
||||
# Lifecycle flags & re-entry guards
|
||||
self._connecting = server_kwargs is not None
|
||||
self._connecting = (
|
||||
server_kwargs is not None and not self._server_startup_deferred
|
||||
)
|
||||
"""True while the backing server is being started or restarted.
|
||||
|
||||
Gates message handling so user input is queued until the agent is
|
||||
@@ -1906,6 +1925,9 @@ class DeepAgentsApp(App):
|
||||
self._ui_adapter._on_tokens_pending = self._show_pending_tokens
|
||||
self._ui_adapter._on_tokens_show = self._show_tokens
|
||||
|
||||
if self._server_startup_deferred:
|
||||
await self._mount_deferred_start_notice()
|
||||
|
||||
# Fire-and-forget workers — none of these block the event loop.
|
||||
|
||||
# Discover skills first so /skill: autocomplete is ready as early
|
||||
@@ -1919,7 +1941,7 @@ class DeepAgentsApp(App):
|
||||
self.run_worker(self._init_session_state, exclusive=True, group="session-init")
|
||||
|
||||
# Server startup (model creation + server process)
|
||||
if self._server_kwargs is not None:
|
||||
if self._server_kwargs is not None and not self._server_startup_deferred:
|
||||
self.run_worker(
|
||||
self._start_server_background,
|
||||
exclusive=True,
|
||||
@@ -1988,7 +2010,7 @@ class DeepAgentsApp(App):
|
||||
# `on_deep_agents_app_server_ready` fires; otherwise run it now so the
|
||||
# non-connecting path (pre-built agent) also honors `--startup-cmd` and
|
||||
# serializes startup against user input.
|
||||
if not self._connecting:
|
||||
if not self._connecting and not self._server_startup_deferred:
|
||||
self.call_after_refresh(
|
||||
lambda: asyncio.create_task(self._run_session_start_sequence())
|
||||
)
|
||||
@@ -3738,6 +3760,9 @@ class DeepAgentsApp(App):
|
||||
startup command before any user-facing agent work guarantees the
|
||||
agent never observes input until the command has completed.
|
||||
"""
|
||||
if self._server_startup_deferred:
|
||||
return
|
||||
|
||||
if self._launch_init_requested:
|
||||
self._ensure_launch_init_task()
|
||||
launch_init_task = self._launch_init_task
|
||||
@@ -5048,6 +5073,7 @@ class DeepAgentsApp(App):
|
||||
skill_lines.append(f" - Removed: {', '.join(removed_skills)}")
|
||||
report += "\nSkills updated:\n" + "\n".join(skill_lines)
|
||||
await self._mount_message(AppMessage(report))
|
||||
await self._maybe_start_deferred_server_from_default()
|
||||
elif cmd.startswith("/skill:"):
|
||||
await self._handle_skill_command(command)
|
||||
# -- Hidden debug commands (not in COMMANDS / autocomplete) -----------
|
||||
@@ -5479,6 +5505,8 @@ class DeepAgentsApp(App):
|
||||
self._run_agent_task(message, message_kwargs=message_kwargs),
|
||||
exclusive=False,
|
||||
)
|
||||
elif self._server_startup_deferred:
|
||||
await self._mount_message(AppMessage(_DEFERRED_START_NOTICE))
|
||||
elif not self._server_startup_error:
|
||||
# When a server-startup failure is in flight, the chat
|
||||
# `ErrorMessage` mounted by `on_deep_agents_app_server_start_failed`
|
||||
@@ -5487,6 +5515,13 @@ class DeepAgentsApp(App):
|
||||
AppMessage("Agent not configured for this session.")
|
||||
)
|
||||
|
||||
async def _mount_deferred_start_notice(self) -> None:
|
||||
"""Tell first-launch users how to configure model credentials."""
|
||||
if self._server_startup_deferred_notice_shown:
|
||||
return
|
||||
self._server_startup_deferred_notice_shown = True
|
||||
await self._mount_message(AppMessage(_DEFERRED_START_NOTICE))
|
||||
|
||||
async def _run_agent_task(
|
||||
self,
|
||||
message: str,
|
||||
@@ -7009,6 +7044,8 @@ class DeepAgentsApp(App):
|
||||
def handle_result(_result: None) -> None:
|
||||
if self._chat_input:
|
||||
self._chat_input.focus_input()
|
||||
task = asyncio.create_task(self._maybe_start_deferred_server_from_default())
|
||||
task.add_done_callback(_log_task_exception)
|
||||
|
||||
self.push_screen(AuthManagerScreen(), handle_result)
|
||||
|
||||
@@ -8223,14 +8260,13 @@ class DeepAgentsApp(App):
|
||||
timeout=3,
|
||||
)
|
||||
return
|
||||
# Recover from a failed startup (e.g., `MissingCredentialsError`).
|
||||
# The server never came up, so the only way out without
|
||||
# restarting the CLI is to retry startup with the new model.
|
||||
# Only valid for CLI-owned servers.
|
||||
# Recover from a startup that has not produced a server yet:
|
||||
# either a deferred first launch with no credentials, or a
|
||||
# failed startup such as `MissingCredentialsError`.
|
||||
if (
|
||||
self._server_startup_error is not None
|
||||
and self._server_kwargs is not None
|
||||
):
|
||||
self._server_startup_deferred
|
||||
or self._server_startup_error is not None
|
||||
) and self._server_kwargs is not None:
|
||||
await self._retry_startup_with_model(
|
||||
model_spec, extra_kwargs=extra_kwargs
|
||||
)
|
||||
@@ -8404,6 +8440,7 @@ class DeepAgentsApp(App):
|
||||
|
||||
self._server_startup_error = None
|
||||
self._server_startup_missing_credentials_provider = None
|
||||
self._server_startup_deferred = False
|
||||
self._connecting = True
|
||||
try:
|
||||
banner = self.query_one("#welcome-banner", WelcomeBanner)
|
||||
@@ -8433,6 +8470,29 @@ class DeepAgentsApp(App):
|
||||
group="server-startup",
|
||||
)
|
||||
|
||||
async def _maybe_start_deferred_server_from_default(self) -> bool:
|
||||
"""Start a deferred first-launch server once a default model resolves.
|
||||
|
||||
Returns:
|
||||
`True` when startup was kicked off, otherwise `False`.
|
||||
"""
|
||||
if not self._server_startup_deferred:
|
||||
return False
|
||||
|
||||
from deepagents_cli.config import _get_default_model_spec
|
||||
from deepagents_cli.model_config import ModelConfigError
|
||||
|
||||
try:
|
||||
model_spec = _get_default_model_spec()
|
||||
except ModelConfigError as exc:
|
||||
if str(exc).startswith("No credentials configured"):
|
||||
return False
|
||||
await self._mount_message(ErrorMessage(str(exc)))
|
||||
return False
|
||||
|
||||
await self._retry_startup_with_model(model_spec)
|
||||
return True
|
||||
|
||||
async def _set_default_model(self, model_spec: str) -> None:
|
||||
"""Set the default model in config without switching the current session.
|
||||
|
||||
@@ -8523,6 +8583,7 @@ async def run_textual_app(
|
||||
server_kwargs: dict[str, Any] | None = None,
|
||||
mcp_preload_kwargs: dict[str, Any] | None = None,
|
||||
model_kwargs: dict[str, Any] | None = None,
|
||||
defer_server_start: bool = False,
|
||||
title: str | None = None,
|
||||
sub_title: str | None = None,
|
||||
) -> AppResult:
|
||||
@@ -8568,6 +8629,8 @@ async def run_textual_app(
|
||||
|
||||
When provided, model creation runs in a background worker after
|
||||
first paint so the splash screen appears immediately.
|
||||
defer_server_start: Whether to keep CLI-owned server startup paused
|
||||
until credentials or a model are configured from inside the TUI.
|
||||
title: Override the Textual `App.title` shown in the optional header
|
||||
bar (gated on `DEEPAGENTS_CLI_SHOW_HEADER`). When `None`, the
|
||||
default `"Deep Agents"` is used.
|
||||
@@ -8595,6 +8658,7 @@ async def run_textual_app(
|
||||
server_kwargs=server_kwargs,
|
||||
mcp_preload_kwargs=mcp_preload_kwargs,
|
||||
model_kwargs=model_kwargs,
|
||||
defer_server_start=defer_server_start,
|
||||
title=title,
|
||||
sub_title=sub_title,
|
||||
)
|
||||
|
||||
@@ -1852,7 +1852,11 @@ def _get_default_model_spec() -> str:
|
||||
Raises:
|
||||
ModelConfigError: If no credentials are configured.
|
||||
"""
|
||||
from deepagents_cli.model_config import ModelConfig, ModelConfigError
|
||||
from deepagents_cli.model_config import (
|
||||
ModelConfig,
|
||||
ModelConfigError,
|
||||
get_provider_auth_status,
|
||||
)
|
||||
|
||||
config = ModelConfig.load()
|
||||
if config.default_model:
|
||||
@@ -1861,12 +1865,11 @@ def _get_default_model_spec() -> str:
|
||||
if config.recent_model:
|
||||
return config.recent_model
|
||||
|
||||
s = _get_settings()
|
||||
if s.has_openai:
|
||||
if get_provider_auth_status("openai").as_legacy_bool() is True:
|
||||
return "openai:gpt-5.5"
|
||||
if s.has_anthropic:
|
||||
if get_provider_auth_status("anthropic").as_legacy_bool() is True:
|
||||
return "anthropic:claude-opus-4-7"
|
||||
if s.has_google:
|
||||
if get_provider_auth_status("google_genai").as_legacy_bool() is True:
|
||||
return "google_genai:gemini-3.1-pro-preview"
|
||||
|
||||
msg = (
|
||||
|
||||
@@ -1001,29 +1001,41 @@ async def run_textual_cli_async(
|
||||
# bar can show the model on first paint. The expensive create_model()
|
||||
# (~560ms) is deferred to a background worker.
|
||||
|
||||
defer_server_start = False
|
||||
try:
|
||||
resolved_spec = model_name or _get_default_model_spec()
|
||||
except ModelConfigError as e:
|
||||
from rich.markup import escape
|
||||
if not str(e).startswith("No credentials configured"):
|
||||
from rich.markup import escape
|
||||
|
||||
from deepagents_cli.config import console
|
||||
from deepagents_cli.config import console
|
||||
|
||||
console.print(f"[bold red]Error:[/bold red] {escape(str(e))}", highlight=False)
|
||||
return AppResult(return_code=1, thread_id=None)
|
||||
console.print(
|
||||
f"[bold red]Error:[/bold red] {escape(str(e))}", highlight=False
|
||||
)
|
||||
return AppResult(return_code=1, thread_id=None)
|
||||
resolved_spec = ""
|
||||
defer_server_start = True
|
||||
|
||||
parsed = ModelSpec.try_parse(resolved_spec)
|
||||
if parsed:
|
||||
settings.model_provider = parsed.provider
|
||||
settings.model_name = parsed.model
|
||||
if resolved_spec:
|
||||
parsed = ModelSpec.try_parse(resolved_spec)
|
||||
if parsed:
|
||||
settings.model_provider = parsed.provider
|
||||
settings.model_name = parsed.model
|
||||
else:
|
||||
settings.model_name = resolved_spec
|
||||
settings.model_provider = detect_provider(resolved_spec) or ""
|
||||
else:
|
||||
settings.model_name = resolved_spec
|
||||
settings.model_provider = detect_provider(resolved_spec) or ""
|
||||
settings.model_provider = ""
|
||||
settings.model_name = ""
|
||||
|
||||
model_kwargs: dict[str, Any] = {
|
||||
"model_spec": model_name,
|
||||
"extra_kwargs": model_params,
|
||||
"profile_overrides": profile_override,
|
||||
}
|
||||
model_kwargs: dict[str, Any] | None = None
|
||||
if not defer_server_start:
|
||||
model_kwargs = {
|
||||
"model_spec": model_name or resolved_spec,
|
||||
"extra_kwargs": model_params,
|
||||
"profile_overrides": profile_override,
|
||||
}
|
||||
|
||||
# Build kwargs for deferred server startup (runs inside the TUI).
|
||||
# Never pass auto_approve to the server — the interactive server must
|
||||
@@ -1032,7 +1044,7 @@ async def run_textual_cli_async(
|
||||
# session_state.auto_approve in textual_adapter.py.
|
||||
server_kwargs: dict[str, Any] = {
|
||||
"assistant_id": assistant_id,
|
||||
"model_name": model_name,
|
||||
"model_name": model_name or resolved_spec or None,
|
||||
"model_params": model_params,
|
||||
"sandbox_type": sandbox_type,
|
||||
"sandbox_id": sandbox_id,
|
||||
@@ -1068,6 +1080,7 @@ async def run_textual_cli_async(
|
||||
server_kwargs=server_kwargs,
|
||||
mcp_preload_kwargs=mcp_preload_kwargs,
|
||||
model_kwargs=model_kwargs,
|
||||
defer_server_start=defer_server_start,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug("App error", exc_info=True)
|
||||
|
||||
@@ -166,6 +166,72 @@ class TestInitialPromptOnMount:
|
||||
|
||||
assert submitted == [("code-review", "review this diff", None)]
|
||||
|
||||
async def test_deferred_start_preserves_initial_prompt_until_server_ready(
|
||||
self,
|
||||
) -> None:
|
||||
"""No-credentials startup should not consume `-m` before connect."""
|
||||
app = DeepAgentsApp(
|
||||
thread_id="new-thread-123",
|
||||
initial_prompt="hello after auth",
|
||||
server_kwargs={"assistant_id": "agent", "model_name": None},
|
||||
defer_server_start=True,
|
||||
)
|
||||
submitted: list[str] = []
|
||||
|
||||
async def capture(msg: str) -> None: # noqa: RUF029
|
||||
submitted.append(msg)
|
||||
|
||||
app._handle_user_message = capture # type: ignore[assignment]
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
await pilot.pause()
|
||||
assert submitted == []
|
||||
assert app._has_initial_submission()
|
||||
|
||||
app._server_startup_deferred = False
|
||||
app.on_deep_agents_app_server_ready(
|
||||
app.ServerReady(
|
||||
agent=MagicMock(),
|
||||
server_proc=None,
|
||||
mcp_server_info=[],
|
||||
)
|
||||
)
|
||||
for _ in range(3):
|
||||
await pilot.pause()
|
||||
|
||||
assert submitted == ["hello after auth"]
|
||||
|
||||
async def test_deferred_start_mounts_auth_guidance(self) -> None:
|
||||
"""First launch without credentials should show next-step guidance."""
|
||||
app = DeepAgentsApp(
|
||||
server_kwargs={"assistant_id": "agent", "model_name": None},
|
||||
defer_server_start=True,
|
||||
)
|
||||
messages: list[AppMessage] = []
|
||||
|
||||
async def capture(message: AppMessage) -> None: # noqa: RUF029
|
||||
messages.append(message)
|
||||
|
||||
def fake_run_worker(work: object, *args: object, **kwargs: object) -> MagicMock:
|
||||
del args, kwargs
|
||||
if inspect.iscoroutine(work):
|
||||
work.close()
|
||||
return MagicMock()
|
||||
|
||||
app._mount_message = capture # type: ignore[assignment]
|
||||
app.run_worker = fake_run_worker # type: ignore[method-assign]
|
||||
|
||||
with patch(
|
||||
"deepagents_cli.update_check.is_update_check_enabled",
|
||||
return_value=False,
|
||||
):
|
||||
await app._post_paint_init()
|
||||
|
||||
assert len(messages) == 1
|
||||
assert "/model" in str(messages[0].content)
|
||||
assert "credentials" in str(messages[0].content)
|
||||
|
||||
|
||||
class TestStartupSequence:
|
||||
"""Tests for post-connect startup sequencing."""
|
||||
|
||||
@@ -743,17 +743,43 @@ class TestBuildMissingToolNotification:
|
||||
|
||||
|
||||
class TestRunTextualCliAsyncModelConfigError:
|
||||
"""Verify ModelConfigError is caught cleanly before launching the TUI."""
|
||||
"""Verify default model config errors are handled before launching the TUI."""
|
||||
|
||||
async def test_returns_error_code_on_no_credentials(self) -> None:
|
||||
"""ModelConfigError from _get_default_model_spec gives return code 1."""
|
||||
async def test_launches_tui_on_no_credentials(self) -> None:
|
||||
"""Missing default credentials should be recoverable inside the TUI."""
|
||||
from deepagents_cli.model_config import ModelConfigError
|
||||
|
||||
app_result = AppResult(return_code=0, thread_id="t-1")
|
||||
captured_kwargs: dict[str, Any] = {}
|
||||
|
||||
async def _stub(**kwargs: Any) -> AppResult:
|
||||
captured_kwargs.update(kwargs)
|
||||
await asyncio.sleep(0)
|
||||
return app_result
|
||||
|
||||
with (
|
||||
patch(
|
||||
"deepagents_cli.config._get_default_model_spec",
|
||||
side_effect=ModelConfigError("No credentials configured"),
|
||||
),
|
||||
patch("deepagents_cli.app.run_textual_app", new=_stub),
|
||||
):
|
||||
result = await run_textual_cli_async("agent")
|
||||
|
||||
assert result == app_result
|
||||
assert captured_kwargs["defer_server_start"] is True
|
||||
assert captured_kwargs["model_kwargs"] is None
|
||||
assert captured_kwargs["server_kwargs"]["model_name"] is None
|
||||
|
||||
async def test_returns_error_code_on_other_model_config_error(self) -> None:
|
||||
"""Non-recoverable default model errors should still block startup."""
|
||||
from deepagents_cli.model_config import ModelConfigError
|
||||
|
||||
with (
|
||||
patch(
|
||||
"deepagents_cli.config._get_default_model_spec",
|
||||
side_effect=ModelConfigError("Invalid model config"),
|
||||
),
|
||||
patch("deepagents_cli.config._get_console") as mock_console_fn,
|
||||
):
|
||||
mock_console = MagicMock()
|
||||
|
||||
@@ -3858,18 +3858,38 @@ recent = "openai:gpt-5.2"
|
||||
|
||||
with (
|
||||
patch.object(model_config, "DEFAULT_CONFIG_PATH", config_path),
|
||||
patch("deepagents_cli.auth_store.get_stored_key", return_value=None),
|
||||
patch.object(settings, "openai_api_key", None),
|
||||
patch.object(settings, "anthropic_api_key", "test-key"),
|
||||
patch.dict(
|
||||
"os.environ",
|
||||
{"ANTHROPIC_API_KEY": "test-key"},
|
||||
clear=False,
|
||||
clear=True,
|
||||
),
|
||||
):
|
||||
result = _get_default_model_spec()
|
||||
|
||||
assert result == "anthropic:claude-opus-4-7"
|
||||
|
||||
def test_stored_key_used_when_neither_model_set(self, tmp_path):
|
||||
"""Falls back to stored TUI credentials when no env vars are set."""
|
||||
from deepagents_cli.config import _get_default_model_spec
|
||||
|
||||
config_path = tmp_path / "config.toml"
|
||||
config_path.write_text("")
|
||||
|
||||
def stored_key(provider: str) -> str | None:
|
||||
return "test-key" if provider == "anthropic" else None
|
||||
|
||||
with (
|
||||
patch.object(model_config, "DEFAULT_CONFIG_PATH", config_path),
|
||||
patch("deepagents_cli.auth_store.get_stored_key", side_effect=stored_key),
|
||||
patch.dict("os.environ", {}, clear=True),
|
||||
):
|
||||
result = _get_default_model_spec()
|
||||
|
||||
assert result == "anthropic:claude-opus-4-7"
|
||||
|
||||
def test_vertex_project_does_not_drive_env_default(self, tmp_path):
|
||||
"""Vertex project alone should not select an automatic default model."""
|
||||
from deepagents_cli.config import _get_default_model_spec, settings
|
||||
@@ -3880,6 +3900,8 @@ recent = "openai:gpt-5.2"
|
||||
|
||||
with (
|
||||
patch.object(model_config, "DEFAULT_CONFIG_PATH", config_path),
|
||||
patch("deepagents_cli.auth_store.get_stored_key", return_value=None),
|
||||
patch.dict("os.environ", {}, clear=True),
|
||||
patch.object(settings, "openai_api_key", None),
|
||||
patch.object(settings, "anthropic_api_key", None),
|
||||
patch.object(settings, "google_api_key", None),
|
||||
@@ -3899,6 +3921,8 @@ recent = "openai:gpt-5.2"
|
||||
|
||||
with (
|
||||
patch.object(model_config, "DEFAULT_CONFIG_PATH", config_path),
|
||||
patch("deepagents_cli.auth_store.get_stored_key", return_value=None),
|
||||
patch.dict("os.environ", {}, clear=True),
|
||||
patch.object(settings, "openai_api_key", None),
|
||||
patch.object(settings, "anthropic_api_key", None),
|
||||
patch.object(settings, "google_api_key", None),
|
||||
|
||||
Reference in New Issue
Block a user