feat(llm): add llm-catalog and llm-check commands, rename helper → llm-helper
Consistent llm-* naming scheme for all LLM CLI commands. llm-catalog shows provider metadata and key status; llm-check sends a minimal prompt to verify connectivity. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7095,10 +7095,12 @@ try:
|
||||
except ImportError:
|
||||
pass # Prompts module not available
|
||||
|
||||
# Register helper Q&A command
|
||||
# Register LLM commands (llm-helper, llm-catalog, llm-check)
|
||||
try:
|
||||
from markitect.helper.cli import helper_command
|
||||
from markitect.helper.cli import helper_command, llm_catalog, llm_check
|
||||
cli.add_command(helper_command)
|
||||
cli.add_command(llm_catalog)
|
||||
cli.add_command(llm_check)
|
||||
except ImportError:
|
||||
pass # Helper module not available
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
"""
|
||||
CLI command for the markitect helper.
|
||||
CLI commands for markitect LLM operations: llm-helper, llm-catalog, llm-check.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
import click
|
||||
from tabulate import tabulate
|
||||
|
||||
from markitect.helper.knowledge import collect_knowledge
|
||||
from markitect.llm.config import find_project_root, resolve_api_key
|
||||
|
||||
DEFAULT_PROVIDER = "openrouter"
|
||||
DEFAULT_MODEL = "openrouter/aurora-alpha"
|
||||
@@ -20,8 +25,68 @@ SYSTEM_PROMPT_TEMPLATE = (
|
||||
"{knowledge}"
|
||||
)
|
||||
|
||||
_PROVIDER_INFO = {
|
||||
"openrouter": {
|
||||
"default_model": "anthropic/claude-sonnet-4",
|
||||
"env_var": "OPENROUTER_API_KEY",
|
||||
"key_file": "apikey-openrouter.txt",
|
||||
},
|
||||
"claude-code": {
|
||||
"default_model": None,
|
||||
"env_var": None,
|
||||
"key_file": None,
|
||||
},
|
||||
"gemini": {
|
||||
"default_model": "gemini-2.5-flash",
|
||||
"env_var": "GEMINI_API_KEY",
|
||||
"key_file": "apikey-geminifree.txt",
|
||||
},
|
||||
"openai": {
|
||||
"default_model": "gpt-4.1-mini",
|
||||
"env_var": "OPENAI_API_KEY",
|
||||
"key_file": "apikey-chatgpt.txt",
|
||||
},
|
||||
}
|
||||
|
||||
@click.command("helper")
|
||||
|
||||
def _probe_key_status(provider: str, info: dict) -> str:
|
||||
"""Return a human-readable key status string for a provider."""
|
||||
if provider == "claude-code":
|
||||
try:
|
||||
subprocess.run(
|
||||
["claude", "--version"],
|
||||
capture_output=True,
|
||||
timeout=5,
|
||||
)
|
||||
return "ok (claude --version)"
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
return "not found (claude CLI)"
|
||||
|
||||
env_var = info["env_var"]
|
||||
key_file = info["key_file"]
|
||||
root = find_project_root()
|
||||
|
||||
# Check env var first.
|
||||
if env_var and os.environ.get(env_var, "").strip():
|
||||
return "found (env)"
|
||||
|
||||
# Check key file.
|
||||
if key_file and root:
|
||||
key_path = root / key_file
|
||||
try:
|
||||
if key_path.read_text().strip():
|
||||
return "found (file)"
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return "not found"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# llm-helper (renamed from helper)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@click.command("llm-helper")
|
||||
@click.argument("question", nargs=-1, required=True)
|
||||
@click.option(
|
||||
"--provider", "-p",
|
||||
@@ -43,9 +108,9 @@ def helper_command(question, provider, model):
|
||||
|
||||
\b
|
||||
Examples:
|
||||
markitect helper "What is markitect?"
|
||||
markitect helper How do schemas work
|
||||
markitect helper -m anthropic/claude-sonnet-4 "Explain templates"
|
||||
markitect llm-helper "What is markitect?"
|
||||
markitect llm-helper How do schemas work
|
||||
markitect llm-helper -m anthropic/claude-sonnet-4 "Explain templates"
|
||||
"""
|
||||
from markitect.llm import create_adapter
|
||||
from markitect.llm.exceptions import LLMConfigurationError, LLMError
|
||||
@@ -99,3 +164,108 @@ def helper_command(question, provider, model):
|
||||
sys.exit(1)
|
||||
|
||||
click.echo(response.content)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# llm-catalog
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@click.command("llm-catalog")
|
||||
@click.option(
|
||||
"--format", "output_format",
|
||||
type=click.Choice(["table", "json"]),
|
||||
default="table",
|
||||
show_default=True,
|
||||
help="Output format.",
|
||||
)
|
||||
def llm_catalog(output_format):
|
||||
"""Show all known LLM providers with their default model and key status."""
|
||||
rows = []
|
||||
for provider, info in _PROVIDER_INFO.items():
|
||||
key_status = _probe_key_status(provider, info)
|
||||
rows.append({
|
||||
"provider": provider,
|
||||
"default_model": info["default_model"] or "(none, uses CLI)",
|
||||
"env_var": info["env_var"] or "\u2014",
|
||||
"key_file": info["key_file"] or "\u2014",
|
||||
"key_status": key_status,
|
||||
})
|
||||
|
||||
if output_format == "json":
|
||||
click.echo(json.dumps(rows, indent=2))
|
||||
else:
|
||||
headers = {
|
||||
"provider": "Provider",
|
||||
"default_model": "Default Model",
|
||||
"env_var": "API Key Env Var",
|
||||
"key_file": "Key File",
|
||||
"key_status": "Key Status",
|
||||
}
|
||||
click.echo(tabulate(rows, headers=headers, tablefmt="simple"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# llm-check
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@click.command("llm-check")
|
||||
@click.option(
|
||||
"--provider", "-p",
|
||||
default=DEFAULT_PROVIDER,
|
||||
type=click.Choice(["openrouter", "claude-code", "gemini", "openai"]),
|
||||
show_default=True,
|
||||
help="LLM provider to check.",
|
||||
)
|
||||
@click.option(
|
||||
"--model", "-m",
|
||||
default=None,
|
||||
help=(
|
||||
f"Model name. Overrides {MODEL_ENV_VAR} env var and the default "
|
||||
f"({DEFAULT_MODEL})."
|
||||
),
|
||||
)
|
||||
def llm_check(provider, model):
|
||||
"""Send a minimal prompt to verify a provider is reachable and responding."""
|
||||
from markitect.llm import create_adapter
|
||||
from markitect.llm.exceptions import LLMConfigurationError, LLMError
|
||||
from markitect.prompts.execution.models import RunConfig
|
||||
|
||||
resolved_model = model or os.environ.get(MODEL_ENV_VAR) or DEFAULT_MODEL
|
||||
|
||||
click.echo(f"Checking {provider} ({resolved_model})...")
|
||||
|
||||
try:
|
||||
adapter = create_adapter(
|
||||
provider=provider,
|
||||
model=resolved_model,
|
||||
)
|
||||
except LLMConfigurationError as exc:
|
||||
click.echo(f"ERROR \u2014 Configuration: {exc}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
config = RunConfig(
|
||||
model_name=resolved_model,
|
||||
max_tokens=16,
|
||||
temperature=0.0,
|
||||
)
|
||||
|
||||
start = time.monotonic()
|
||||
try:
|
||||
response = adapter.execute_prompt("Reply with only the word OK.", config)
|
||||
except LLMError as exc:
|
||||
elapsed = time.monotonic() - start
|
||||
click.echo(f"ERROR \u2014 LLM error after {elapsed:.1f}s: {exc}", err=True)
|
||||
sys.exit(1)
|
||||
except Exception as exc:
|
||||
elapsed = time.monotonic() - start
|
||||
click.echo(f"ERROR \u2014 Unexpected error after {elapsed:.1f}s: {exc}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
elapsed = time.monotonic() - start
|
||||
resp_model = response.metadata.get("model", resolved_model)
|
||||
total_tokens = sum(response.usage.values()) if response.usage else "?"
|
||||
|
||||
click.echo(
|
||||
f"OK \u2014 response in {elapsed:.1f}s, model: {resp_model}, "
|
||||
f"tokens: {total_tokens}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user