AI Provider Setup

docsfy supports three provider options: claude, gemini, and cursor. Provider/model are treated as a first-class variant key, so the same repo can have multiple generated doc variants side by side.

```10:20:src/docsfy/models.py class GenerateRequest(BaseModel): repo_url: str | None = Field( default=None, description="Git repository URL (HTTPS or SSH)" ) repo_path: str | None = Field(default=None, description="Local git repository path") ai_provider: Literal["claude", "gemini", "cursor"] | None = None ai_model: str | None = None ai_cli_timeout: int | None = Field(default=None, gt=0) force: bool = Field( default=False, description="Force full regeneration, ignoring cache" )

```1365:1370:src/docsfy/templates/dashboard.html
<label for="gen-provider">Provider</label>
<select id="gen-provider" class="form-select">
    <option value="claude"{% if default_provider == 'claude' %} selected{% endif %}>claude</option>
    <option value="gemini"{% if default_provider == 'gemini' %} selected{% endif %}>gemini</option>
    <option value="cursor"{% if default_provider == 'cursor' %} selected{% endif %}>cursor</option>
</select>

```3:11:src/docsfy/ai_client.py from ai_cli_runner import ( PROVIDERS, VALID_AI_PROVIDERS, ProviderConfig, call_ai_cli, check_ai_cli_available, get_ai_cli_timeout, run_parallel_with_limit, )

> **Note:** `docsfy` delegates provider execution to `ai_cli_runner`; credentials are expected via environment variables consumed by provider CLIs.

## Credentials and Environment Variables

Use `.env` (loaded automatically by settings) to configure both app-level defaults and provider credentials.

```10:23:.env.example
# Claude - Option 1: API Key
# ANTHROPIC_API_KEY=

# Claude - Option 2: Vertex AI
# CLAUDE_CODE_USE_VERTEX=1
# CLOUD_ML_REGION=
# ANTHROPIC_VERTEX_PROJECT_ID=

# Gemini
# GEMINI_API_KEY=

# Cursor
# CURSOR_API_KEY=

```10:13:src/docsfy/config.py model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore", )

Set app defaults in `.env`:

```4:8:.env.example
# AI Configuration
AI_PROVIDER=claude
# [1m] = 1 million token context window, this is a valid model identifier
AI_MODEL=claude-opus-4-6[1m]
AI_CLI_TIMEOUT=60

ADMIN_KEY is required at startup and must be at least 16 characters:

```82:89:src/docsfy/main.py settings = get_settings() if not settings.admin_key: logger.error("ADMIN_KEY environment variable is required") raise SystemExit(1)

if len(settings.admin_key) < 16: logger.error("ADMIN_KEY must be at least 16 characters long") raise SystemExit(1)

If you run with Docker Compose, `.env` is wired automatically:

```1:8:docker-compose.yaml
services:
  docsfy:
    build: .
    ports:
      - "8000:8000"
    env_file: .env
    volumes:
      - ./data:/data

Provider CLI Prerequisites

The container image installs all three CLIs:

```26:57:Dockerfile

Install bash (needed for CLI install scripts), git (required at runtime for gitpython), curl (for Claude CLI), and nodejs/npm (for Gemini CLI)

RUN apt-get update && apt-get install -y --no-install-recommends \ bash \ git \ curl \ nodejs \ npm \ && rm -rf /var/lib/apt/lists/* ...

Install Claude Code CLI (installs to ~/.local/bin)

RUN /bin/bash -o pipefail -c "curl -fsSL https://claude.ai/install.sh | bash"

Install Cursor Agent CLI (installs to ~/.local/bin)

RUN /bin/bash -o pipefail -c "curl -fsSL https://cursor.com/install | bash"

Configure npm for non-root global installs and install Gemini CLI

RUN mkdir -p /home/appuser/.npm-global \ && npm config set prefix '/home/appuser/.npm-global' \ && npm install -g @google/gemini-cli

## Model Selection Behavior

### 1) Server-side fallback and validation

If request values are omitted, `docsfy` falls back to settings defaults:

```454:466:src/docsfy/main.py
settings = get_settings()
ai_provider = gen_request.ai_provider or settings.ai_provider
ai_model = gen_request.ai_model or settings.ai_model
project_name = gen_request.project_name
owner = request.state.username

if ai_provider not in ("claude", "gemini", "cursor"):
    raise HTTPException(
        status_code=400,
        detail=f"Invalid AI provider: '{ai_provider}'. Must be claude, gemini, or cursor.",
    )
if not ai_model:
    raise HTTPException(status_code=400, detail="AI model must be specified.")

Each (project, provider, model) is stored as a separate variant path:

```501:519:src/docsfy/storage.py def get_project_dir( name: str, ai_provider: str = "", ai_model: str = "", owner: str = "" ) -> Path: if not ai_provider or not ai_model: msg = "ai_provider and ai_model are required for project directory paths" raise ValueError(msg) ... return PROJECTS_DIR / safe_owner / _validate_name(name) / ai_provider / ai_model

### 2) UI suggestions and auto-fill behavior

Model suggestions come from **ready** projects only:

```572:577:src/docsfy/storage.py
async def get_known_models() -> dict[str, list[str]]:
    """Get distinct ai_model values per ai_provider from completed projects."""
    async with aiosqlite.connect(DB_PATH) as db:
        cursor = await db.execute(
            "SELECT DISTINCT ai_provider, ai_model FROM projects WHERE ai_provider != '' AND ai_model != '' AND status = 'ready' ORDER BY ai_provider, ai_model"
        )

When provider changes in the dashboard form: - if current model is invalid for that provider, UI auto-fills the first known model - if no known models exist for that provider, UI clears the model input

```1677:1697:src/docsfy/templates/dashboard.html if (providerSelect && modelDropdown) { providerSelect.addEventListener('change', function() { if (_restoring) return; var newProvider = this.value; var modelsForProvider = knownModels[newProvider] || [];

    // If current model is not valid for the new provider, auto-fill
    if (modelInput) {
        var currentModel = modelInput.value;
        if (modelsForProvider.length > 0 && modelsForProvider.indexOf(currentModel) === -1) {
            modelInput.value = modelsForProvider[0];
            saveFormState();
        } else if (modelsForProvider.length === 0) {
            modelInput.value = '';
            modelInput.placeholder = 'Enter model name';
            saveFormState();
        }
    }

    filterModelOptions(modelDropdown, modelInput ? modelInput.value : '', newProvider);
});

}

Generate request payload only includes `ai_model` when the input is non-empty:

```2043:2049:src/docsfy/templates/dashboard.html
var body = {
    repo_url: repoUrl,
    ai_provider: provider,
    force: force
};
if (model) body.ai_model = model;

Status page retry always sends the model input value:

```1367:1370:src/docsfy/templates/status.html var payload = { repo_url: repoUrl }; if (providerSelect) payload.ai_provider = providerSelect.value; if (modelInput) payload.ai_model = modelInput.value; if (forceCheckbox && forceCheckbox.checked) payload.force = true;

> **Warning:** If `ai_model` is blank, server fallback uses `AI_MODEL` from settings. If you switched provider and left model empty, the fallback model may not match that provider.

> **Tip:** Keep `AI_PROVIDER` and `AI_MODEL` aligned in `.env`, and run one successful generation per provider/model pair to seed `known_models` suggestions.

### 3) Dynamic model list refresh

`known_models` is returned by `/api/status` and refreshed in the dashboard without full reload:

```409:419:src/docsfy/main.py
@app.get("/api/status")
async def status(request: Request) -> dict[str, Any]:
    ...
    known_models = await get_known_models()
    return {"projects": projects, "known_models": known_models}

```1886:1891:src/docsfy/templates/dashboard.html // Update known models from the API so new models // appear in dropdowns without a full page reload. if (data.known_models) { knownModels = data.known_models; rebuildModelDropdownOptions(); }

## Cursor-Specific Behavior

For `cursor`, `docsfy` always adds `--trust` when checking availability and running generation calls.

```732:735:src/docsfy/main.py
cli_flags = ["--trust"] if ai_provider == "cursor" else None
available, msg = await check_ai_cli_available(
    ai_provider, ai_model, cli_flags=cli_flags
)

```41:49:src/docsfy/generator.py

Build CLI flags based on provider

cli_flags = ["--trust"] if ai_provider == "cursor" else None success, output = await call_ai_cli( prompt=prompt, cwd=repo_path, ai_provider=ai_provider, ai_model=ai_model, ai_cli_timeout=ai_cli_timeout, cli_flags=cli_flags, )

> **Warning:** `cursor` runs with trust mode enabled by default in this app flow; only generate docs for repositories you trust.

## Secrets Hygiene in Tooling

`.env` is ignored by git, and pre-commit includes secret scanners:

```1:4:.gitignore
# Environment files with secrets
.env
.dev/.env
*.env.local

38:52:.pre-commit-config.yaml - repo: https://github.com/Yelp/detect-secrets rev: v1.5.0 hooks: - id: detect-secrets ... - repo: https://github.com/gitleaks/gitleaks rev: v8.30.0 hooks: - id: gitleaks