Spaces:
Sleeping
Sleeping
| # CLI & Provider Configuration Design | |
| Design document for the `ace` CLI, model configuration system, and provider registry. | |
| --- | |
| ## Implementation Status | |
| | Component | Status | Location | | |
| |---|---|---| | |
| | `ModelConfig` / `ACEModelConfig` dataclasses | Done | `ace/providers/config.py` | | |
| | TOML persistence (`ace.toml`) | Done | `ace/providers/config.py` | | |
| | `.env` persistence (secrets) | Done | `ace/providers/config.py` | | |
| | Provider registry (LiteLLM delegation) | Done | `ace/providers/registry.py` | | |
| | Model search / discovery | Done | `ace/providers/registry.py` | | |
| | Connection validation | Done | `ace/providers/registry.py` | | |
| | `ace setup` interactive wizard | Done | `ace/cli/setup.py` | | |
| | `ace models` search command | Done | `ace/cli/setup.py` | | |
| | `ace validate` connection test | Done | `ace/cli/setup.py` | | |
| | Lazy imports (fast CLI startup) | Done | `ace/__init__.py`, `ace/providers/__init__.py`, `ace/providers/registry.py` | | |
| --- | |
| ## Goals | |
| 1. **Zero-friction onboarding** β `ace setup` walks the user from nothing to a working config in under a minute. | |
| 2. **Secrets / config separation** β `ace.toml` (committable, no secrets) + `.env` (gitignored API keys). Teams share model choices without leaking credentials. | |
| 3. **Per-role model selection** β different models for Agent, Reflector, and Skill Manager to optimise cost vs quality. | |
| 4. **Provider agnosticism** β any model string LiteLLM supports works. No provider-specific code in the config layer. | |
| 5. **Fast CLI startup** β heavy dependencies (LiteLLM, instructor, openai) are lazily imported. The `ace` command starts in ~50ms, not ~2s. | |
| --- | |
| ## Architecture Overview | |
| ``` | |
| pyproject.toml | |
| [project.scripts] | |
| ace = "ace.cli.setup:main" # entry point | |
| ace/cli/ | |
| __init__.py | |
| setup.py # CLI commands + interactive wizard | |
| ace/providers/ | |
| __init__.py # lazy re-exports | |
| config.py # ModelConfig, ACEModelConfig, TOML + .env I/O | |
| registry.py # provider detection, model search, connection validation | |
| pydantic_ai.py # resolve_model, settings_from_config (PydanticAI model resolution) | |
| ``` | |
| The CLI layer (`ace/cli/`) depends on: | |
| - `ace/providers/config.py` β always (lightweight, no heavy deps) | |
| - `ace/providers/registry.py` β only when validating or searching (imports LiteLLM lazily) | |
| The config layer has **zero heavy dependencies** β it uses only `tomllib` (stdlib), `pathlib`, and `dataclasses`. | |
| --- | |
| ## Configuration Model | |
| ### Two-file split | |
| | File | Contains | Git | Written by | | |
| |------|----------|-----|------------| | |
| | `ace.toml` | Model names, temperature, max_tokens, extra_params | Commit | `save_config()` | | |
| | `.env` | `OPENAI_API_KEY=sk-...`, `ANTHROPIC_API_KEY=sk-ant-...` | Gitignore | `save_env_var()` | | |
| ### `ace.toml` format | |
| ```toml | |
| [default] | |
| model = "gpt-4o-mini" | |
| [agent] | |
| model = "claude-sonnet-4-20250514" | |
| max_tokens = 4096 | |
| [reflector] | |
| model = "gpt-4o-mini" | |
| temperature = 0.2 | |
| ``` | |
| Roles without an explicit section inherit from `[default]`. Only non-default values are written (e.g. `temperature` is omitted when it equals `0.0`). | |
| ### Config discovery | |
| `find_config(start)` walks up from `start` to the filesystem root looking for `ace.toml`. This supports monorepos where the config lives at the project root but commands run from subdirectories. | |
| ### Loading in code | |
| ```python | |
| from ace import ACELiteLLM | |
| # Option 1: Load from ace.toml + .env (created by `ace setup`) | |
| ace = ACELiteLLM.from_setup() | |
| # Option 2: Explicit model, keys from environment | |
| ace = ACELiteLLM.from_model("gpt-4o-mini") | |
| # Option 3: Full config object | |
| from ace import ACEModelConfig, ModelConfig | |
| ace = ACELiteLLM.from_config(ACEModelConfig( | |
| default=ModelConfig(model="gpt-4o-mini"), | |
| agent=ModelConfig(model="claude-sonnet-4-20250514"), | |
| )) | |
| ``` | |
| --- | |
| ## Data Types | |
| ### ModelConfig | |
| ```python | |
| @dataclass | |
| class ModelConfig: | |
| model: str # LiteLLM model string | |
| temperature: float = 0.0 | |
| max_tokens: int = 2048 | |
| extra_params: dict[str, Any] | None = None | |
| ``` | |
| Serialises to/from a TOML section. `to_dict()` omits default values to keep the file clean. | |
| ### ACEModelConfig | |
| ```python | |
| @dataclass | |
| class ACEModelConfig: | |
| default: ModelConfig # required β used as fallback | |
| agent: ModelConfig | None = None # overrides default for Agent role | |
| reflector: ModelConfig | None = None # overrides default for Reflector role | |
| skill_manager: ModelConfig | None = None # overrides default for Skill Manager role | |
| ``` | |
| `for_role(role)` returns the role-specific config or falls back to `default`. | |
| ### ValidationResult | |
| ```python | |
| @dataclass | |
| class ValidationResult: | |
| success: bool | |
| model: str = "" | |
| provider: str = "" | |
| latency_ms: int = 0 | |
| error: str = "" | |
| ``` | |
| Returned by `validate_connection()`. On success, includes the provider name and round-trip latency. On failure, includes a human-readable error string. | |
| ### ModelInfo | |
| ```python | |
| @dataclass | |
| class ModelInfo: | |
| model: str | |
| provider: str | |
| max_input_tokens: int | None = None | |
| max_output_tokens: int | None = None | |
| input_cost_per_m: float | None = None # cost per million tokens | |
| output_cost_per_m: float | None = None | |
| key_found: bool = False # are the required env vars set? | |
| ``` | |
| Returned by `search_models()`. Pricing is per million tokens (converted from LiteLLM's per-token values). | |
| --- | |
| ## Provider Registry | |
| All provider logic is delegated to LiteLLM. The registry module (`ace/providers/registry.py`) wraps LiteLLM with a stable API: | |
| | Function | What it does | Makes API calls? | | |
| |----------|-------------|-----------------| | |
| | `get_provider(model)` | Detect provider from model string | No | | |
| | `get_missing_keys(model)` | List required env vars that are unset | No | | |
| | `keys_are_set(model)` | Check if all required env vars exist | No | | |
| | `validate_connection(model, api_key?)` | Send a 3-token test call | Yes (tiny) | | |
| | `search_models(query, provider?, limit?)` | Search LiteLLM's static model registry | No | | |
| | `suggest_models(typo, limit?)` | Fuzzy-match model names for typo correction | No | | |
| ### LiteLLM lazy import | |
| LiteLLM takes ~1.5s to import. The registry defers import until first use: | |
| ```python | |
| def _litellm(): | |
| global _litellm_mod | |
| try: | |
| return _litellm_mod | |
| except NameError: | |
| pass | |
| import litellm as _mod | |
| _litellm_mod = _mod | |
| return _mod | |
| ``` | |
| All registry functions call `_litellm()` instead of using a top-level import. | |
| ### Provider key mapping | |
| `PROVIDER_KEY_ENV` maps provider names to the environment variables they require: | |
| | Provider | Required env vars | | |
| |----------|------------------| | |
| | `openai` | `OPENAI_API_KEY` | | |
| | `anthropic` | `ANTHROPIC_API_KEY` | | |
| | `azure` | `AZURE_API_KEY` | | |
| | `gemini` | `GEMINI_API_KEY` | | |
| | `deepseek` | `DEEPSEEK_API_KEY` | | |
| | `groq` | `GROQ_API_KEY` | | |
| | `bedrock` | `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` + `AWS_REGION_NAME` | | |
| | `bedrock_converse` | `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` + `AWS_REGION_NAME` | | |
| | `vertex_ai` | `GOOGLE_APPLICATION_CREDENTIALS` | | |
| | `cohere` | `COHERE_API_KEY` | | |
| | `mistral` | `MISTRAL_API_KEY` | | |
| | `openrouter` | `OPENROUTER_API_KEY` | | |
| | `together_ai` | `TOGETHERAI_API_KEY` | | |
| | `fireworks_ai` | `FIREWORKS_AI_API_KEY` | | |
| | `replicate` | `REPLICATE_API_KEY` | | |
| | `huggingface` | `HUGGINGFACE_API_KEY` | | |
| | `perplexity` | `PERPLEXITYAI_API_KEY` | | |
| | `anyscale` | `ANYSCALE_API_KEY` | | |
| For multi-key providers (bedrock), **all** listed vars must be set for `_quick_key_check` to report `True`. | |
| This mapping is preferred over LiteLLM's `validate_environment()` because: | |
| - LiteLLM's response can be inaccurate for certain providers (e.g. bedrock_converse) | |
| - Our mapping is used for both the `Key` column in `ace models` and the key prompting in `ace setup` | |
| --- | |
| ## CLI Commands | |
| ### Entry point | |
| Defined in `pyproject.toml`: | |
| ```toml | |
| [project.scripts] | |
| ace = "ace.cli.setup:main" | |
| ``` | |
| `main()` uses `argparse` with subcommands: | |
| ``` | |
| ace setup [--dir DIR] Interactive configuration wizard | |
| ace models [QUERY] [--provider P] [--limit N] Search model catalog | |
| ace validate MODEL Test a model connection | |
| ace config Show current configuration | |
| ``` | |
| ### `ace setup` | |
| Interactive wizard flow: | |
| ``` | |
| 1. Load existing .env (if present) | |
| 2. Check for existing ace.toml | |
| - If found: show current config, ask "Reconfigure?" | |
| - If declined: return existing config | |
| 3. Step 1: Choose your model | |
| - Prompt for model name | |
| - Detect provider via get_provider() | |
| - Validate connection | |
| - If auth fails: prompt for missing keys, retry | |
| - If model not found: suggest alternatives, re-prompt | |
| - If success: continue | |
| 4. Step 2: Role assignment | |
| - Ask "Use this model for all roles?" (default: yes) | |
| - If no: prompt for each role (Agent, Reflector, Skill Manager) | |
| - Enter = keep default (skip validation) | |
| - Different model = validate it | |
| 5. Save ace.toml and .env | |
| 6. Print configuration summary | |
| ``` | |
| Key behaviours: | |
| - **Validation-first**: the wizard tries the connection immediately. If credentials exist in the environment (env vars, `.env`, AWS profiles), no prompting needed. | |
| - **Error recovery**: on failed validation, env vars set during prompting are rolled back. | |
| - **Secret handling**: API keys are prompted via `getpass` (hidden input). Non-secret values like `AWS_REGION_NAME` and `GOOGLE_APPLICATION_CREDENTIALS` use regular `input()` so users can see what they type. | |
| - **Typo correction**: when a model is not found, `suggest_models()` offers alternatives. | |
| ### `ace models` | |
| Searches LiteLLM's static `model_cost` registry (no API calls): | |
| ``` | |
| $ ace models claude haiku | |
| Model Provider Input $/M Output $/M Key | |
| ------------------------------------------------------------------------------------------ | |
| claude-haiku-4-5-20251001 anthropic $1.00 $5.00 β | |
| us.anthropic.claude-haiku-4-5-20251001-v1:0 bedrock_converse $1.10 $5.50 β | |
| ``` | |
| - Multiple query terms are AND-matched (all must appear in the model name) | |
| - `--provider` filters by LiteLLM provider name | |
| - `--limit` caps results (default 20), shows total count | |
| - `Key` column uses `_quick_key_check()` β checks env vars only, no API calls | |
| ### `ace validate` | |
| Sends a minimal LLM call (3 tokens: "Say 'ok'") to verify: | |
| - API key authentication | |
| - Model availability at the provider | |
| - Network connectivity | |
| ``` | |
| $ ace validate gpt-4o-mini | |
| β Connected! (gpt-4o-mini via openai, 203ms) | |
| ``` | |
| On failure, suggests similar model names if the model wasn't found. | |
| All subcommands (`models`, `validate`, `config`) use `_load_project_dotenv()` which finds `.env` relative to `ace.toml` (via `find_config()`), not just CWD. | |
| ### `ace config` | |
| Displays the current configuration from `ace.toml`: | |
| ``` | |
| $ ace config | |
| Configuration (/path/to/ace.toml) | |
| Role Model | |
| ---------------- --------------------------------------------- | |
| default gpt-4o-mini | |
| agent claude-sonnet-4-20250514 | |
| reflector (default) | |
| skill_manager (default) | |
| ``` | |
| Uses `find_config()` to locate `ace.toml` from the current directory upward. | |
| ### `ace models` (no query) | |
| When called without arguments, shows usage examples instead of dumping arbitrary results: | |
| ``` | |
| $ ace models | |
| Usage: ace models <query> | |
| Examples: | |
| ace models claude All Claude models | |
| ace models gpt 4o GPT-4o variants | |
| ace models haiku us US-region Haiku models | |
| ace models --provider openai All OpenAI models | |
| ``` | |
| --- | |
| ## Kayba CLI | |
| The `kayba` entry point (`ace.cli:main`) provides the hosted API client. It wraps trace management, pipeline execution, insight triage, prompt generation, and integration management. See [Hosted API docs](../integrations/hosted-api.md) for the full reference. | |
| ```toml | |
| [project.scripts] | |
| ace = "ace.cli.setup:main" | |
| kayba = "ace.cli:main" | |
| ace-mcp = "ace.integrations.mcp.server:main" | |
| ``` | |
| Key command groups: `traces`, `run`, `insights`, `prompts`, `integrations`, `status`, `materialize`, `batch`, `setup`. | |
| --- | |
| ## Lazy Import Strategy | |
| ### Problem | |
| `import ace` eagerly imported all submodules, including `litellm` (~1.5s). This made the CLI unusable (~2s startup for a simple `ace --help`). | |
| ### Solution | |
| Three-layer lazy import: | |
| 1. **`ace/__init__.py`** β `__getattr__`-based lazy loading. `TYPE_CHECKING` block for IDE support, `_LAZY_IMPORTS` dict for runtime. | |
| 2. **`ace/providers/__init__.py`** β same pattern. Config imports are eager (lightweight), everything else is lazy. | |
| 3. **`ace/providers/registry.py`** β `_litellm()` helper defers `import litellm` until first function call. | |
| Result: `ace --help` runs in ~50ms. LiteLLM is only imported when the user actually searches, validates, or sets up. | |
| ### Pattern | |
| ```python | |
| from typing import TYPE_CHECKING | |
| if TYPE_CHECKING: | |
| from .heavy_module import HeavyClass # IDE autocomplete | |
| _LAZY_IMPORTS = { | |
| "HeavyClass": ("package.heavy_module", "HeavyClass"), | |
| } | |
| def __getattr__(name: str) -> object: | |
| if name in _LAZY_IMPORTS: | |
| module_path, attr = _LAZY_IMPORTS[name] | |
| import importlib | |
| module = importlib.import_module(module_path) | |
| value = getattr(module, attr) | |
| globals()[name] = value # cache for subsequent access | |
| return value | |
| raise AttributeError(...) | |
| ``` | |
| --- | |
| ## File Map | |
| ``` | |
| ace/ | |
| __init__.py # lazy re-exports for all ace symbols | |
| cli/ | |
| setup.py # main(), run_setup(), _cmd_models(), _cmd_validate(), _cmd_config() | |
| providers/ | |
| __init__.py # lazy re-exports (config eager, rest lazy) | |
| config.py # ModelConfig, ACEModelConfig, TOML/env I/O | |
| registry.py # provider detection, model search, validation | |
| pydantic_ai.py # resolve_model, settings_from_config (PydanticAI model resolution) | |
| ``` | |
| Generated files: | |
| ``` | |
| project-root/ | |
| ace.toml # model config (commit this) | |
| .env # API keys (gitignore this) | |
| ``` | |
| --- | |
| ## Known Issues | |
| ### Resolved | |
| | # | Issue | Fix | | |
| |---|-------|-----| | |
| | 1 | `_prompt_secret` used `getpass` for non-secrets like `AWS_REGION_NAME` | Non-secret vars (`AWS_REGION_NAME`, `GOOGLE_APPLICATION_CREDENTIALS`) now use visible `_prompt()` | | |
| | 2 | Error classification used fragile substring matching | Simplified: only "not found" is non-recoverable; everything else offers key prompting | | |
| | 3 | `save_env_var` didn't quote values | Values now written as `KEY="value"` | | |
| | 4 | `_PROVIDER_KEY_ENV` was private but imported externally | Renamed to `PROVIDER_KEY_ENV` (public) | | |
| | 5 | `v` / `x` status symbols | Replaced with `β` / `β` | | |
| | 7 | `ace models` with no query dumped arbitrary results | Now shows usage examples instead | | |
| | 8 | No way to inspect current config | Added `ace config` command | | |
| | 10 | `ace validate` / `ace models` only loaded `.env` from CWD | Added `_load_project_dotenv()` β finds `.env` relative to `ace.toml` via `find_config()` | | |
| | 11 | Unused imports `asdict`, `field` in `config.py` | Removed | | |
| | 12 | Dead function `_litellm_available()` in `registry.py` | Removed | | |
| | 13 | Unused `PROVIDER_MODEL_EXAMPLES` import in `setup.py` | Removed | | |
| | 14 | Duplicate `search_models` import in `setup.py` | Consolidated to single top-level import | | |
| ### Open | |
| | # | Issue | Impact | | |
| |---|-------|--------| | |
| | 6 | No `--non-interactive` mode for CI/Docker | Blocks CI automation β requires a future `ace setup --model MODEL --skip-validation` flag | | |
| | 9 | Per-role model selection skips validation when keeping default | Low β the default was already validated in Step 1 | | |
| | 15 | No multi-provider key setup in one pass | When reconfiguring (`ace setup` on an existing config), users who want different providers per role (e.g. OpenAI default + Anthropic agent + Bedrock reflector) must go through each role sequentially β keys are only prompted when a model fails validation. A future improvement should let users configure all providers and their keys upfront in a single credentials step, before role assignment begins. | | |
| --- | |
| ## Design Decisions | |
| ### Why hand-rolled TOML serialisation? | |
| Python's `tomllib` (stdlib since 3.11) only reads TOML. Writing requires `tomli-w` (third-party) or a manual serialiser. To avoid adding a dependency for a simple four-section config file, we hand-roll `_to_toml()`. The format is simple enough that this is reliable β the only complex case is `extra_params` (inline table). | |
| ### Why our own key mapping instead of LiteLLM's? | |
| LiteLLM's `validate_environment()` sometimes returns incorrect keys (e.g. suggesting `AWS_BEARER_TOKEN_BEDROCK` for bedrock_converse when the standard auth is `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` + `AWS_REGION_NAME`). Our `PROVIDER_KEY_ENV` mapping is simpler, auditable, and covers the common providers. For unknown providers, we fall back to LiteLLM's response or guess `{PROVIDER}_API_KEY`. | |
| ### Why validation-first in setup? | |
| Many users already have credentials in their environment (exported vars, AWS profiles, `.env` from another project). Prompting for keys before trying would waste their time. By attempting the connection first, the happy path is: type model name β instant success β done. | |
| ### Why `ace.toml` instead of `pyproject.toml [tool.ace]`? | |
| - Keeps ACE config decoupled from the Python project (ACE might be used in non-Python contexts) | |
| - `find_config()` can walk up the directory tree independently | |
| - Easier to reason about β one file, one purpose | |