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
62 changes: 62 additions & 0 deletions src/praisonai/praisonai/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,58 @@ def _get_agents_generator():
from praisonai.agents_generator import AgentsGenerator
return AgentsGenerator


def _provider_preflight_message():
"""Return a guidance message if no LLM provider credential is configured.

PraisonAI is provider-agnostic (OpenAI, Anthropic, Google/Gemini, Groq,
Cohere, Ollama, OpenRouter, plus 100+ via LiteLLM). Flows that call an LLM
immediately — such as ``praisonai --init`` — should fail with clear,
actionable guidance (pointing at ``praisonai setup``) instead of a raw stack
trace when the user has not configured any provider yet.

Returns:
A user-facing message string when no provider is configured, or ``None``
when a provider credential is available (so the caller proceeds). The
check itself never raises; on any unexpected error it returns ``None``
so it can never block a properly configured user.
"""
try:
from praisonai.llm.credentials import (
inject_credentials_into_env,
is_configured,
)

# Mirror the runtime credential resolution so the gate cannot disagree
# with what generation actually does:
# 1. AutoGenerator resolves via env-only resolve_llm_endpoint(), so a
# key stored via `praisonai setup` must be exported into the env
# first or it never reaches the LLM call.
# 2. The runtime model honours MODEL_NAME / OPENAI_MODEL_NAME, so gate
# on that exact model (not the inferred provider default) — a stale
# OpenAI model override with only a non-OpenAI key must still be
# caught here instead of failing later with a raw auth error.
inject_credentials_into_env()
import os as _os
runtime_model = _os.environ.get("MODEL_NAME") or _os.environ.get(
"OPENAI_MODEL_NAME"
)
if is_configured(model=runtime_model):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 OpenRouter still bypasses

This check still disagrees with the runtime path for OpenRouter models. resolve_llm_endpoint() treats openrouter/... as requiring OPENROUTER_API_KEY, but the credential guard path does not map OpenRouter for model-scoped checks or stored credential injection. When MODEL_NAME=openrouter/... is set and the key was saved through praisonai setup, this guard can proceed while the later generator path never exports OPENROUTER_API_KEY, so --init can still end in the raw provider auth failure instead of the setup guidance.

return None
except Exception:
return None # never block on the check itself
return (
"No LLM provider is configured.\n\n"
"PraisonAI supports OpenAI, Anthropic, Google/Gemini, Groq, "
"Cohere, Ollama, OpenRouter and 100+ models via LiteLLM.\n\n"
"Easiest setup (interactive, no shell 'export' needed):\n"
" praisonai setup\n\n"
"Or set a provider API key, for example:\n"
" export OPENAI_API_KEY=... # OpenAI\n"
" export ANTHROPIC_API_KEY=... # Anthropic Claude\n"
" export GEMINI_API_KEY=... # Google Gemini\n"
)

# Use centralized availability detection
from .._framework_availability import is_available

Expand Down Expand Up @@ -742,6 +794,16 @@ def __init__(self):
self.topic = temp_topic

self.agent_file = "agents.yaml"

# Pre-flight: ensure an LLM provider credential is configured before
# calling the LLM. Without this, a user with no API key (or a
# non-OpenAI key for an OpenAI-default model) gets a raw stack trace
# instead of clear, actionable guidance.
preflight = _provider_preflight_message()
if preflight:
print(preflight)
return preflight
Comment thread
greptile-apps[bot] marked this conversation as resolved.

AutoGenerator = _get_auto_generator()
generator = AutoGenerator(topic=self.topic, framework=self.framework, agent_file=self.agent_file)
self.agent_file = generator.generate(merge=getattr(args, 'merge', False))
Expand Down
121 changes: 121 additions & 0 deletions src/praisonai/tests/unit/cli/test_init_provider_guard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""
Unit tests for the `praisonai --init` provider pre-flight guard.

Verifies that when no LLM provider credential is configured, the guard returns a
clear, actionable message (pointing at `praisonai setup`) instead of letting the
`--init` flow call the LLM and surface a raw stack trace. When a provider IS
configured, the guard must return None so generation proceeds.
"""

from unittest.mock import patch

import pytest

try:
import praisonai.cli.main as cli_main
from praisonai.cli.main import _provider_preflight_message
except ImportError as e: # pragma: no cover - environment guard
pytest.skip(f"Could not import praisonai.cli.main: {e}", allow_module_level=True)


class TestProviderPreflightMessage:
def test_returns_message_when_unconfigured(self):
with patch("praisonai.llm.credentials.is_configured", return_value=False):
msg = _provider_preflight_message()

assert msg is not None
assert "No LLM provider is configured" in msg
# Points beginners at the no-export interactive setup.
assert "praisonai setup" in msg
# Mentions multiple providers so users know it's not OpenAI-only.
assert "Anthropic" in msg and "Gemini" in msg

def test_returns_none_when_configured(self):
with patch("praisonai.llm.credentials.is_configured", return_value=True):
assert _provider_preflight_message() is None

def test_never_blocks_on_internal_error(self):
# If the credential check itself raises, the guard must not block a
# potentially configured user (returns None -> generation proceeds).
with patch(
"praisonai.llm.credentials.is_configured",
side_effect=RuntimeError("boom"),
):
assert _provider_preflight_message() is None

def test_injects_stored_credentials_before_gating(self):
# A key stored via `praisonai setup` must be exported into the env
# before gating, otherwise the env-only AutoGenerator path would still
# fail with a raw auth error despite the guard passing.
with patch(
"praisonai.llm.credentials.inject_credentials_into_env"
) as inject, patch(
"praisonai.llm.credentials.is_configured", return_value=True
):
assert _provider_preflight_message() is None
inject.assert_called_once()

def test_gates_on_runtime_model_override(self):
# With a stale OpenAI model override and only a non-OpenAI key, the
# gate must use the exact runtime model so the mismatch is caught here
# (message returned) rather than failing later at the LLM call.
with patch.dict(
"os.environ", {"MODEL_NAME": "gpt-4o-mini"}, clear=False
), patch(
"praisonai.llm.credentials.inject_credentials_into_env"
), patch(
"praisonai.llm.credentials.is_configured", return_value=False
) as is_configured:
msg = _provider_preflight_message()

assert msg is not None
# The runtime model override must be passed through to the gate.
is_configured.assert_called_once_with(model="gpt-4o-mini")


class TestInitGuardWiring:
def test_init_returns_early_without_calling_generator(self):
# The real --init path must print guidance and return early WITHOUT
# constructing the AutoGenerator when no provider is configured. This
# protects against the guard being removed or moved after generation.
instance = cli_main.PraisonAI.__new__(cli_main.PraisonAI)
instance.agent_file = "agents.yaml"
instance.config_list = [{"model": "gpt-4o-mini"}]
instance.framework = None
instance.auto = False
instance.init = False
instance.topic = ""

# Use a plain namespace (not MagicMock) so unset attributes don't read
# as truthy and trip earlier branches before the --init guard.
from types import SimpleNamespace

args = SimpleNamespace(
command=None,
framework=None,
model=None,
prompt_flag=None,
file=None,
direct_prompt=None,
deploy=False,
auto=False,
init="build me a team",
ui=None,
merge=False,
)

with patch.object(instance, "parse_args", return_value=(args, [])), patch.object(
cli_main, "_load_env_once"
), patch.object(
instance, "read_stdin_if_available", return_value=None
), patch.object(
instance, "read_file_if_provided", return_value=None
), patch.object(
cli_main, "_provider_preflight_message", return_value="SETUP GUIDANCE"
), patch.object(
cli_main, "_get_auto_generator"
) as get_gen, patch("builtins.print"):
result = instance.main()

assert result == "SETUP GUIDANCE"
get_gen.assert_not_called()
Loading