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