docs: rewrite README and CONTRIBUTING for full registry scope

- README now covers all 5 content types (agents, hands, integrations, skills, providers)
- CONTRIBUTING has per-type instructions and checklists
- validate.py expanded to validate agents, hands, integrations, and skills
- PR template covers all content types
- Added new-content.yml issue template for non-model contributions
- CI workflow updated to Python 3.12
This commit is contained in:
Evan Hu committed 2026-03-21 02:10:12 +09:00
1 parent 17d32ed4a7
commit 1f3ef406ee
6 files changed
+639 -268

No files matched your search

+49
View File
@@ -0,0 +1,49 @@
name: New Content Request
description: Request addition of an agent, hand, integration, or skill
labels: ["new-content"]
body:
- type: dropdown
id: content_type
attributes:
label: Content Type
description: What type of content are you requesting?
options:
- Agent
- Hand
- Integration (MCP server)
- Skill
validations:
required: true
- type: input
id: name
attributes:
label: Name
description: The name/ID for this content.
placeholder: e.g. my-agent, browser, github
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: What does this content do? Why is it useful?
placeholder: |
Describe the purpose and key capabilities.
validations:
required: true
- type: textarea
id: details
attributes:
label: Additional Details
description: |
Any extra context: required tools, external dependencies,
MCP server package, reference links, etc.
placeholder: |
- MCP server: @some/mcp-server
- Requires: API key from https://...
- Tools needed: web_search, file_read
validations:
required: false
+47 -9
View File
@@ -1,14 +1,52 @@
## Model Changes ## Content Type
### Added/Updated Models <!-- Check all that apply -->
- [ ] Model ID:
- [ ] Provider:
- [ ] Pricing verified from official source:
### Checklist - [ ] Agent (`agents/`)
- [ ] Hand (`hands/`)
- [ ] Integration (`integrations/`)
- [ ] Skill (`skills/`)
- [ ] Provider / Model (`providers/`)
- [ ] Other
## Changes
### Added/Updated
-
### Details
<!-- Brief description of what was added or changed -->
## Checklist
### General
- [ ] Content placed in the correct directory
- [ ] TOML files parse without errors
- [ ] Description is clear and concise
### Providers / Models (if applicable)
- [ ] `python scripts/validate.py` passes - [ ] `python scripts/validate.py` passes
- [ ] No duplicate model IDs - [ ] No duplicate model IDs
- [ ] Pricing is in USD per million tokens - [ ] Pricing is in USD per million tokens and verified from official source
- [ ] Tier is one of: `frontier`, `smart`, `balanced`, `fast`, `local` - [ ] Tier is one of: `frontier`, `smart`, `balanced`, `fast`, `local`
- [ ] `context_window` and `max_output_tokens` are positive integers
- [ ] Boolean fields (`supports_tools`, `supports_vision`, `supports_streaming`) are correct ### Agents (if applicable)
- [ ] `name` matches directory name
- [ ] System prompt provides clear instructions
- [ ] Tools list only includes required tools
### Hands (if applicable)
- [ ] `id` matches directory name
- [ ] `[agent]` section has complete system prompt
- [ ] `[[requires]]` lists external dependencies
- [ ] `[[settings]]` provides user-configurable options
### Integrations (if applicable)
- [ ] `[transport]` config tested locally
- [ ] `[[required_env]]` lists all needed variables
- [ ] `setup_instructions` are clear for first-time users
### Skills (if applicable)
- [ ] `[runtime].type` is correct
- [ ] `[input]` documents all parameters
- [ ] Prompt templates use correct `{{param}}` syntax
+3 -6
View File
@@ -1,4 +1,4 @@
name: Validate Catalog name: Validate Registry
on: on:
push: push:
@@ -14,10 +14,7 @@ jobs:
- uses: actions/setup-python@v6 - uses: actions/setup-python@v6
with: with:
python-version: "3.11" python-version: "3.12"
- name: Install dependencies - name: Validate registry
run: pip install toml
- name: Validate catalog
run: python scripts/validate.py run: python scripts/validate.py
+187 -59
View File
@@ -1,59 +1,202 @@
# Contributing to LibreFang Model Catalog # Contributing to LibreFang Registry
Thank you for helping keep the model catalog up to date! This guide explains how to add or update model entries. Thank you for helping grow the LibreFang ecosystem! This guide explains how to add or update content for each type.
## How to Add a New Model ## General Workflow
### 1. Fork & Clone 1. Fork & clone the repository
2. Create a branch: `git checkout -b feat/add-my-content`
3. Add or edit files in the appropriate directory
4. Run validation: `python scripts/validate.py`
5. Submit a Pull Request
```bash ## Adding an Agent
git clone https://github.com/<your-fork>/model-catalog.git
cd model-catalog
```
### 2. Find the Right Provider File Create a directory `agents/<name>/` with an `agent.toml` file:
Each provider has its own file in `providers/`. For example:
- OpenAI models go in `providers/openai.toml`
- Anthropic models go in `providers/anthropic.toml`
If the provider doesn't exist yet, create a new file (e.g. `providers/newprovider.toml`) with the `[provider]` section and your `[[models]]` entries.
### 3. Add Your Model Entry
Append a `[[models]]` block to the provider file:
```toml ```toml
name = "my-agent"
version = "0.1.0"
description = "What this agent does"
author = "your-name"
module = "builtin:chat"
[model]
provider = "default"
model = "default"
max_tokens = 4096
temperature = 0.7
system_prompt = """Your system prompt here."""
[capabilities]
tools = ["web_search", "file_read"]
```
### Agent Checklist
- [ ] `name` matches the directory name
- [ ] `description` is clear and concise (one sentence)
- [ ] `system_prompt` provides clear behavioral instructions
- [ ] `tools` only lists tools the agent actually needs
- [ ] Routing aliases (if any) are relevant and don't conflict with existing agents
## Adding a Hand
Create a directory `hands/<name>/` with a `HAND.toml` file and optionally a `SKILL.md`:
```toml
id = "my-hand"
name = "My Hand"
description = "What this hand does"
category = "productivity" # communication | content | data | development | devops | finance | productivity | research | social
icon = "🔧"
tools = ["tool1", "tool2"]
[routing]
aliases = ["activate my hand", "do the thing"]
[agent]
name = "my-hand-agent"
module = "builtin:chat"
system_prompt = """Your agent prompt here."""
[[settings]]
key = "some_setting"
label = "Setting Label"
setting_type = "toggle"
default = "true"
```
### Hand Checklist
- [ ] `id` matches the directory name
- [ ] `category` is valid (`communication`, `content`, `data`, `development`, `devops`, `finance`, `productivity`, `research`, `social`)
- [ ] `tools` lists all required tools
- [ ] `[agent]` section has a complete system prompt
- [ ] `[[requires]]` sections list any external dependencies (binaries, services)
- [ ] `[[settings]]` sections provide user-configurable options where appropriate
## Adding an Integration
Create a file `integrations/<name>.toml`:
```toml
id = "my-service"
name = "My Service"
description = "What this integration provides"
category = "devtools" # devtools | communication | storage | monitoring | data
icon = "🔌"
tags = ["relevant", "tags"]
[transport]
type = "stdio"
command = "npx"
args = ["-y", "@some/mcp-server"]
[[required_env]]
name = "MY_SERVICE_API_KEY"
label = "API Key"
help = "Get your key from https://..."
is_secret = true
get_url = "https://my-service.com/settings/api-keys"
setup_instructions = """
1. Get an API key from ...
2. Paste it into the field above.
"""
```
### Integration Checklist
- [ ] `id` matches the filename (without `.toml`)
- [ ] `[transport]` section is correct (test the MCP server command locally)
- [ ] `[[required_env]]` lists all needed environment variables
- [ ] `setup_instructions` are clear enough for first-time users
- [ ] `is_secret = true` for any sensitive values (API keys, tokens)
## Adding a Skill
Create a directory `skills/<name>/` with a `skill.toml` and optionally implementation files:
### Prompt-only Skill
```toml
[skill]
name = "my-skill"
version = "0.1.0"
description = "What this skill does"
author = "your-name"
tags = ["relevant", "tags"]
[runtime]
type = "promptonly"
[input]
param1 = { type = "string", description = "Description", required = true }
[prompt]
template = """Your prompt template using {{param1}}."""
```
### Python Skill
```toml
[skill]
name = "my-skill"
version = "0.1.0"
description = "What this skill does"
[runtime]
type = "python"
entry = "main.py"
```
Plus a `main.py` with your implementation.
### Skill Checklist
- [ ] `name` matches the directory name
- [ ] `[runtime].type` is `promptonly` or `python`
- [ ] `[input]` section documents all parameters
- [ ] Prompt-only skills have a `[prompt].template` with correct `{{param}}` placeholders
- [ ] Python skills include all required files
## Adding or Updating a Provider / Model
Edit the appropriate provider file in `providers/`. If the provider doesn't exist, create a new file.
```toml
[provider]
id = "my-provider"
display_name = "My Provider"
api_key_env = "MY_PROVIDER_API_KEY"
base_url = "https://api.my-provider.com"
key_required = true
[[models]] [[models]]
id = "new-model-id" # The exact API model ID id = "model-id"
display_name = "New Model Name" # Human-readable name display_name = "Model Name"
tier = "smart" # frontier | smart | balanced | fast | local tier = "smart" # frontier | smart | balanced | fast | local
context_window = 128000 context_window = 128000
max_output_tokens = 16384 max_output_tokens = 16384
input_cost_per_m = 2.50 # USD per million input tokens input_cost_per_m = 2.50 # USD per million input tokens
output_cost_per_m = 10.0 # USD per million output tokens output_cost_per_m = 10.0 # USD per million output tokens
supports_tools = true supports_tools = true
supports_vision = false supports_vision = false
supports_streaming = true supports_streaming = true
aliases = [] # Optional short names aliases = ["short-name"]
``` ```
### 4. Validate ### Provider Checklist
```bash - [ ] `python scripts/validate.py` passes
python scripts/validate.py - [ ] No duplicate model IDs
``` - [ ] Pricing is in USD per million tokens
- [ ] Tier is one of: `frontier`, `smart`, `balanced`, `fast`, `local`
This checks: - [ ] `context_window` and `max_output_tokens` are positive integers
- All TOML files parse correctly - [ ] Boolean capability fields are correct
- Required fields are present - [ ] Pricing verified from official source
- Tier values are valid
- Costs are non-negative
- No duplicate model IDs
### 5. Submit a Pull Request
Push your branch and open a PR. The PR template will guide you through the checklist.
## Where to Find Model Information ## Where to Find Model Information
@@ -64,26 +207,11 @@ Push your branch and open a PR. The PR template will guide you through the check
- **Mistral**: https://mistral.ai/technology/#pricing - **Mistral**: https://mistral.ai/technology/#pricing
- **Groq**: https://wow.groq.com/ - **Groq**: https://wow.groq.com/
- **xAI**: https://docs.x.ai/docs - **xAI**: https://docs.x.ai/docs
- **Cohere**: https://cohere.com/pricing
- **Together**: https://www.together.ai/pricing
- **Fireworks**: https://fireworks.ai/pricing
- **Perplexity**: https://docs.perplexity.ai/guides/pricing
## Tier Definitions
| Tier | Description | Examples |
|------|-------------|----------|
| `frontier` | Most capable, cutting-edge | Claude Opus, GPT-4.1, Gemini 2.5 Pro |
| `smart` | Smart and cost-effective | Claude Sonnet, GPT-4o, Gemini 2.5 Flash |
| `balanced` | Balanced speed and cost | GPT-4.1 Mini, Llama 3.3 70B |
| `fast` | Fastest, cheapest | GPT-4o Mini, Claude Haiku, Gemma 2 9B |
| `local` | Local models, zero cost | Ollama, vLLM, LM Studio |
## Guidelines ## Guidelines
- **Pricing must be in USD per million tokens** -- convert from other units if needed
- **Use the exact model ID** that the provider's API expects
- **Don't guess** -- only add data you can verify from official sources - **Don't guess** -- only add data you can verify from official sources
- **One provider per file** -- don't mix providers in a single TOML file - **Keep descriptions concise** -- one sentence that explains the purpose
- **Keep aliases short** -- 1-3 word abbreviations that users would naturally type - **Test locally** -- try your content with LibreFang before submitting
- **One PR per content type** -- don't mix agent additions with provider updates
- **Keep aliases short** -- 1-3 word abbreviations users would naturally type
+140 -78
View File
@@ -1,101 +1,148 @@
# LibreFang Model Catalog # LibreFang Registry
Community-maintained model metadata catalog for [LibreFang](https://github.com/librefang/librefang) -- the open-source Agent Operating System. Community-maintained content registry for [LibreFang](https://github.com/librefang/librefang) -- the open-source Agent Operating System.
This repository is the source of truth for model metadata (pricing, context windows, capabilities). When new models are released (e.g. GPT-5.5, Claude 5), anyone can submit a PR here without touching the LibreFang binary. This repository is the single source of truth for all installable content definitions. Anyone can submit a PR to add new agents, hands, integrations, skills, or provider models -- no changes to the LibreFang binary required.
## Structure ## Structure
``` ```
model-catalog/ librefang-registry/
├── providers/ # One TOML file per provider ├── agents/ # Agent definitions (TOML manifests)
│ ├── hello-world/agent.toml
│ ├── researcher/agent.toml
│ └── ... (33 agents)
├── hands/ # Hand definitions (TOML + docs)
│ ├── browser/HAND.toml
│ ├── trader/HAND.toml
│ └── ... (14 hands)
├── integrations/ # MCP server integration templates
│ ├── github.toml
│ ├── slack.toml
│ └── ... (25 integrations)
├── skills/ # Reusable skill definitions
│ ├── custom-skill-prompt/skill.toml
│ └── custom-skill-python/
├── providers/ # LLM provider & model metadata
│ ├── anthropic.toml │ ├── anthropic.toml
│ ├── openai.toml │ ├── openai.toml
│ ├── gemini.toml │ └── ... (46 providers, 190+ models)
│ └── ... ├── plugins/ # Plugin packages
├── aliases.toml # Global alias mappings (e.g. "sonnet" -> "claude-sonnet-4-6") ├── aliases.toml # Global model alias mappings
├── schema.toml # Reference schema documenting all fields ├── schema.toml # Provider/model schema reference
├── scripts/ ├── scripts/
│ └── validate.py # Validation script │ └── validate.py # Validation script
├── CONTRIBUTING.md # How to add a new model ├── CONTRIBUTING.md
└── LICENSE # MIT └── LICENSE # MIT
``` ```
## How LibreFang Uses This Catalog ## Content Types
LibreFang ships with a built-in model catalog compiled into the binary. This repository serves as the upstream source. To update your local catalog: ### Agents
```bash Agent definitions in `agents/<name>/agent.toml` describe autonomous agents with their model config, tools, capabilities, and routing aliases.
librefang catalog update
```
This fetches the latest TOML files from this repository and merges them into your local catalog.
### Custom Local Models
You can also add custom models locally without submitting a PR:
```bash
# Add to your personal config
# ~/.librefang/model_catalog.toml
[[models]]
id = "my-custom-model"
display_name = "My Custom Model"
provider = "ollama"
tier = "local"
context_window = 32768
max_output_tokens = 4096
input_cost_per_m = 0.0
output_cost_per_m = 0.0
supports_tools = true
supports_vision = false
supports_streaming = true
```
## Schema Reference
Each provider file contains a `[provider]` section and one or more `[[models]]` entries:
```toml ```toml
[provider] name = "hello-world"
id = "provider-id" # Unique provider identifier description = "A friendly greeting agent"
display_name = "Provider Name" # Human-readable name module = "builtin:chat"
api_key_env = "PROVIDER_API_KEY" # Environment variable for API key
base_url = "https://api.example.com" # Default API endpoint
key_required = true # Whether an API key is needed
[[models]] [model]
id = "model-id" # Unique model identifier (API model ID) provider = "default"
display_name = "Human Name" # Human-readable display name model = "default"
tier = "smart" # frontier | smart | balanced | fast | local system_prompt = "You are a helpful assistant."
context_window = 128000 # Maximum input tokens
max_output_tokens = 16384 # Maximum output tokens [capabilities]
input_cost_per_m = 2.50 # USD per million input tokens tools = ["web_search", "file_read"]
output_cost_per_m = 10.0 # USD per million output tokens
supports_tools = true # Tool/function calling support
supports_vision = true # Vision/image input support
supports_streaming = true # Streaming response support
aliases = ["alias1", "alias2"] # Short names for this model
``` ```
### Tier Definitions ### Hands
| Tier | Description | Examples | Hands in `hands/<name>/HAND.toml` are higher-level application bundles -- the user-facing "apps" in LibreFang. Each hand bundles an agent config, tools, settings, dashboard metrics, and dependency requirements.
|------|-------------|----------|
| `frontier` | Most capable, cutting-edge models | Claude Opus, GPT-4.1, Gemini 2.5 Pro |
| `smart` | Smart, cost-effective models | Claude Sonnet, GPT-4o, Gemini 2.5 Flash |
| `balanced` | Balanced speed/cost | GPT-4.1 Mini, Llama 3.3 70B |
| `fast` | Fastest, cheapest | GPT-4o Mini, Claude Haiku |
| `local` | Local models (zero cost) | Ollama, vLLM, LM Studio |
## How to Add a New Model ```toml
id = "browser"
name = "Browser Hand"
category = "productivity"
tools = ["browser_navigate", "browser_click", "browser_type"]
1. Edit the appropriate provider file in `providers/` [agent]
2. Run validation: `python scripts/validate.py` name = "browser-hand"
3. Submit a Pull Request module = "builtin:chat"
system_prompt = "You are an autonomous web browser agent..."
See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed instructions. [[settings]]
key = "headless"
setting_type = "toggle"
default = "true"
```
### Integrations
Integration templates in `integrations/<name>.toml` define MCP server connections (GitHub, Slack, databases, etc.) with transport config, required env vars, and setup instructions.
```toml
id = "github"
name = "GitHub"
category = "devtools"
[transport]
type = "stdio"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]
[[required_env]]
name = "GITHUB_PERSONAL_ACCESS_TOKEN"
is_secret = true
```
### Skills
Skills in `skills/<name>/skill.toml` are reusable prompt templates or Python scripts that agents can invoke.
```toml
[skill]
name = "meeting-agenda"
description = "Generate a structured meeting agenda"
[runtime]
type = "promptonly"
[prompt]
template = "Create a meeting agenda for: {{topic}}"
```
### Providers
Provider files in `providers/<name>.toml` define LLM providers and their models with pricing, context windows, and capability flags. See [schema.toml](schema.toml) for the full field reference.
## How LibreFang Uses This Registry
LibreFang ships with built-in content compiled into the binary. This repository serves as the upstream source for updates and community contributions.
```bash
# Update all registry content
librefang catalog update
# Install a specific hand
librefang hand install browser
# Install a specific integration
librefang integration install github
```
### Custom Local Content
You can also create custom content locally without submitting a PR:
```bash
# Create a custom agent
mkdir -p ~/.librefang/agents/my-agent
# Edit ~/.librefang/agents/my-agent/agent.toml
# Add custom models to your config
# ~/.librefang/model_catalog.toml
```
## Validation ## Validation
@@ -103,13 +150,28 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed instructions.
python scripts/validate.py python scripts/validate.py
``` ```
This checks all TOML files for correctness: required fields, valid tiers, non-negative costs, no duplicate IDs. This validates all provider TOML files for correctness: required fields, valid tiers, non-negative costs, no duplicate IDs.
## How to Contribute
1. Fork this repository
2. Add or edit content in the appropriate directory
3. Run validation: `python scripts/validate.py`
4. Submit a Pull Request
See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed instructions for each content type.
## Current Stats ## Current Stats
- **30+ providers** including Anthropic, OpenAI, Google, DeepSeek, Groq, Mistral, xAI, and more | Type | Count |
- **190+ models** with pricing, context windows, and capability flags |------|-------|
- **80+ aliases** for quick model selection | Agents | 33 |
| Hands | 14 |
| Integrations | 25 |
| Skills | 2 |
| Providers | 46 |
| Models | 190+ |
| Aliases | 80+ |
## License ## License
+213 -116
View File
@@ -1,19 +1,16 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Validate all TOML files in the model catalog against the schema. """Validate all TOML files in the LibreFang registry.
Usage: Usage:
python scripts/validate.py python scripts/validate.py
Checks: Validates:
- All provider TOML files parse correctly - Provider TOML files (required fields, valid tiers, non-negative costs, no duplicates)
- Required provider fields exist - Agent TOML files (required fields: name, description, module)
- Required model fields exist for each [[models]] entry - Hand TOML files (required fields: id, name, description, category)
- Tier values are one of: frontier, smart, balanced, fast, local - Integration TOML files (required fields: id, name, [transport])
- Cost values are non-negative - Skill TOML files (required fields: [skill].name, [runtime].type)
- context_window and max_output_tokens are positive integers - aliases.toml parsing
- No duplicate model IDs within the same provider file
- No duplicate model IDs across ALL provider files (same provider)
- aliases.toml parses correctly
Exit code 0 on success, 1 on any validation error. Exit code 0 on success, 1 on any validation error.
""" """
@@ -32,31 +29,36 @@ except ImportError:
sys.exit(1) sys.exit(1)
VALID_TIERS = {"frontier", "smart", "balanced", "fast", "local"} VALID_TIERS = {"frontier", "smart", "balanced", "fast", "local"}
VALID_HAND_CATEGORIES = {
"communication", "content", "data", "development",
"devops", "finance", "productivity", "research", "social",
}
VALID_SKILL_RUNTIMES = {"promptonly", "python", "node", "shell"}
REQUIRED_PROVIDER_FIELDS = {"id", "display_name", "api_key_env", "base_url", "key_required"} REQUIRED_PROVIDER_FIELDS = {"id", "display_name", "api_key_env", "base_url", "key_required"}
REQUIRED_MODEL_FIELDS = { REQUIRED_MODEL_FIELDS = {
"id", "id", "display_name", "tier",
"display_name", "context_window", "max_output_tokens",
"tier", "input_cost_per_m", "output_cost_per_m",
"context_window",
"max_output_tokens",
"input_cost_per_m",
"output_cost_per_m",
} }
def validate_provider_file(filepath: Path) -> list[str]: def load_toml(filepath: Path) -> tuple[dict | None, str | None]:
"""Validate a single provider TOML file. Returns list of error messages.""" """Load a TOML file, return (data, error)."""
errors = []
try: try:
with open(filepath, "rb") as f: with open(filepath, "rb") as f:
data = tomllib.load(f) return tomllib.load(f), None
except Exception as e: except Exception as e:
return [f"{filepath.name}: Failed to parse TOML: {e}"] return None, f"{filepath}: Failed to parse TOML: {e}"
# Validate [provider] section
def validate_provider_file(filepath: Path) -> list[str]:
"""Validate a single provider TOML file."""
data, err = load_toml(filepath)
if err:
return [err]
errors = []
provider = data.get("provider") provider = data.get("provider")
if provider is None: if provider is None:
errors.append(f"{filepath.name}: Missing [provider] section") errors.append(f"{filepath.name}: Missing [provider] section")
@@ -65,66 +67,141 @@ def validate_provider_file(filepath: Path) -> list[str]:
if field not in provider: if field not in provider:
errors.append(f"{filepath.name}: Missing provider field '{field}'") errors.append(f"{filepath.name}: Missing provider field '{field}'")
# Validate [[models]] entries
models = data.get("models", []) models = data.get("models", [])
seen_ids = set() seen_ids = set()
for i, model in enumerate(models): for i, model in enumerate(models):
model_label = model.get("id", f"models[{i}]") label = model.get("id", f"models[{i}]")
# Check required fields
for field in REQUIRED_MODEL_FIELDS: for field in REQUIRED_MODEL_FIELDS:
if field not in model: if field not in model:
errors.append(f"{filepath.name}: Model '{model_label}' missing field '{field}'") errors.append(f"{filepath.name}: Model '{label}' missing field '{field}'")
# Validate tier
tier = model.get("tier") tier = model.get("tier")
if tier is not None and tier not in VALID_TIERS: if tier is not None and tier not in VALID_TIERS:
errors.append( errors.append(f"{filepath.name}: Model '{label}' invalid tier '{tier}'")
f"{filepath.name}: Model '{model_label}' has invalid tier '{tier}' "
f"(must be one of: {', '.join(sorted(VALID_TIERS))})"
)
# Validate costs
for cost_field in ("input_cost_per_m", "output_cost_per_m"): for cost_field in ("input_cost_per_m", "output_cost_per_m"):
val = model.get(cost_field) val = model.get(cost_field)
if val is not None and (not isinstance(val, (int, float)) or val < 0): if val is not None and (not isinstance(val, (int, float)) or val < 0):
errors.append( errors.append(f"{filepath.name}: Model '{label}' invalid {cost_field}: {val}")
f"{filepath.name}: Model '{model_label}' has invalid {cost_field}: {val} "
f"(must be >= 0)"
)
# Validate context_window and max_output_tokens
for int_field in ("context_window", "max_output_tokens"): for int_field in ("context_window", "max_output_tokens"):
val = model.get(int_field) val = model.get(int_field)
if val is not None and (not isinstance(val, int) or val <= 0): if val is not None and (not isinstance(val, int) or val <= 0):
errors.append( errors.append(f"{filepath.name}: Model '{label}' invalid {int_field}: {val}")
f"{filepath.name}: Model '{model_label}' has invalid {int_field}: {val} "
f"(must be a positive integer)"
)
# Check for duplicate IDs within the file
model_id = model.get("id") model_id = model.get("id")
if model_id: if model_id:
if model_id in seen_ids: if model_id in seen_ids:
errors.append( errors.append(f"{filepath.name}: Duplicate model ID '{model_id}'")
f"{filepath.name}: Duplicate model ID '{model_id}' within file"
)
seen_ids.add(model_id) seen_ids.add(model_id)
return errors return errors
def validate_aliases_file(filepath: Path) -> list[str]: def validate_agent_file(filepath: Path) -> list[str]:
"""Validate aliases.toml. Returns list of error messages.""" """Validate an agent.toml file."""
if not filepath.exists(): data, err = load_toml(filepath)
return [f"aliases.toml: File not found at {filepath}"] if err:
return [err]
try: errors = []
with open(filepath, "rb") as f: rel = filepath.relative_to(filepath.parent.parent)
data = tomllib.load(f)
except Exception as e: for field in ("name", "description", "module"):
return [f"aliases.toml: Failed to parse TOML: {e}"] if field not in data:
errors.append(f"{rel}: Missing required field '{field}'")
if "model" in data:
model = data["model"]
if "system_prompt" not in model and "provider" not in model:
errors.append(f"{rel}: [model] section should have 'provider' or 'system_prompt'")
return errors
def validate_hand_file(filepath: Path) -> list[str]:
"""Validate a HAND.toml file."""
data, err = load_toml(filepath)
if err:
return [err]
errors = []
rel = filepath.relative_to(filepath.parent.parent.parent)
for field in ("id", "name", "description"):
if field not in data:
errors.append(f"{rel}: Missing required field '{field}'")
category = data.get("category")
if category and category not in VALID_HAND_CATEGORIES:
errors.append(f"{rel}: Invalid category '{category}' (valid: {', '.join(sorted(VALID_HAND_CATEGORIES))})")
if "agent" not in data:
errors.append(f"{rel}: Missing [agent] section")
return errors
def validate_integration_file(filepath: Path) -> list[str]:
"""Validate an integration TOML file."""
data, err = load_toml(filepath)
if err:
return [err]
errors = []
for field in ("id", "name"):
if field not in data:
errors.append(f"{filepath.name}: Missing required field '{field}'")
if "transport" not in data:
errors.append(f"{filepath.name}: Missing [transport] section")
else:
transport = data["transport"]
if "type" not in transport:
errors.append(f"{filepath.name}: Missing transport.type")
if "command" not in transport:
errors.append(f"{filepath.name}: Missing transport.command")
return errors
def validate_skill_file(filepath: Path) -> list[str]:
"""Validate a skill.toml file."""
data, err = load_toml(filepath)
if err:
return [err]
errors = []
rel = filepath.relative_to(filepath.parent.parent)
skill = data.get("skill")
if skill is None:
errors.append(f"{rel}: Missing [skill] section")
else:
if "name" not in skill:
errors.append(f"{rel}: Missing skill.name")
runtime = data.get("runtime")
if runtime is None:
errors.append(f"{rel}: Missing [runtime] section")
else:
rt = runtime.get("type")
if rt and rt not in VALID_SKILL_RUNTIMES:
errors.append(f"{rel}: Invalid runtime type '{rt}' (valid: {', '.join(sorted(VALID_SKILL_RUNTIMES))})")
return errors
def validate_aliases_file(filepath: Path) -> list[str]:
"""Validate aliases.toml."""
if not filepath.exists():
return [f"aliases.toml not found at {filepath}"]
data, err = load_toml(filepath)
if err:
return [err]
errors = [] errors = []
aliases = data.get("aliases", {}) aliases = data.get("aliases", {})
@@ -139,67 +216,93 @@ def validate_aliases_file(filepath: Path) -> list[str]:
def main(): def main():
# Find the catalog root
script_dir = Path(__file__).resolve().parent script_dir = Path(__file__).resolve().parent
catalog_root = script_dir.parent root = script_dir.parent
providers_dir = catalog_root / "providers"
if not providers_dir.is_dir():
print(f"ERROR: providers/ directory not found at {providers_dir}")
sys.exit(1)
all_errors = [] all_errors = []
total_models = 0 stats = {}
provider_counts = {}
global_model_ids = {} # model_id -> (provider_id, filename)
# Validate each provider file # --- Providers ---
toml_files = sorted(providers_dir.glob("*.toml")) providers_dir = root / "providers"
if not toml_files: if providers_dir.is_dir():
print("ERROR: No .toml files found in providers/") toml_files = sorted(providers_dir.glob("*.toml"))
sys.exit(1) total_models = 0
global_model_ids = {}
for filepath in toml_files: for fp in toml_files:
errors = validate_provider_file(filepath) all_errors.extend(validate_provider_file(fp))
all_errors.extend(errors) data, _ = load_toml(fp)
if data:
# Count models if file parsed successfully
if not any("Failed to parse" in e for e in errors):
try:
with open(filepath, "rb") as f:
data = tomllib.load(f)
provider_id = data.get("provider", {}).get("id", filepath.stem)
models = data.get("models", []) models = data.get("models", [])
count = len(models) total_models += len(models)
total_models += count provider_id = data.get("provider", {}).get("id", fp.stem)
provider_counts[provider_id] = count for m in models:
mid = m.get("id")
# Track global model IDs for cross-file duplicate detection
for model in models:
mid = model.get("id")
mprov = model.get("provider", provider_id)
if mid: if mid:
key = (mid, mprov) key = (mid, provider_id)
if key in global_model_ids: if key in global_model_ids:
prev_file = global_model_ids[key] prev = global_model_ids[key]
# Only flag if same provider in different files if prev != fp.name:
if prev_file != filepath.name:
all_errors.append( all_errors.append(
f"Cross-file duplicate: model '{mid}' (provider '{mprov}') " f"Cross-file duplicate: model '{mid}' (provider '{provider_id}') "
f"found in both {prev_file} and {filepath.name}" f"in both {prev} and {fp.name}"
) )
else: else:
global_model_ids[key] = filepath.name global_model_ids[key] = fp.name
except Exception:
pass
# Validate aliases.toml stats["providers"] = len(toml_files)
aliases_file = catalog_root / "aliases.toml" stats["models"] = total_models
all_errors.extend(validate_aliases_file(aliases_file))
# Print results # --- Agents ---
agents_dir = root / "agents"
if agents_dir.is_dir():
agent_dirs = sorted([d for d in agents_dir.iterdir() if d.is_dir() and not d.name.startswith(".")])
for d in agent_dirs:
agent_toml = d / "agent.toml"
if agent_toml.exists():
all_errors.extend(validate_agent_file(agent_toml))
else:
all_errors.append(f"agents/{d.name}: Missing agent.toml")
stats["agents"] = len(agent_dirs)
# --- Hands ---
hands_dir = root / "hands"
if hands_dir.is_dir():
hand_dirs = sorted([d for d in hands_dir.iterdir() if d.is_dir() and not d.name.startswith(".")])
for d in hand_dirs:
hand_toml = d / "HAND.toml"
if hand_toml.exists():
all_errors.extend(validate_hand_file(hand_toml))
else:
all_errors.append(f"hands/{d.name}: Missing HAND.toml")
stats["hands"] = len(hand_dirs)
# --- Integrations ---
integrations_dir = root / "integrations"
if integrations_dir.is_dir():
int_files = sorted(integrations_dir.glob("*.toml"))
for fp in int_files:
all_errors.extend(validate_integration_file(fp))
stats["integrations"] = len(int_files)
# --- Skills ---
skills_dir = root / "skills"
if skills_dir.is_dir():
skill_dirs = sorted([d for d in skills_dir.iterdir() if d.is_dir() and not d.name.startswith(".")])
for d in skill_dirs:
skill_toml = d / "skill.toml"
if skill_toml.exists():
all_errors.extend(validate_skill_file(skill_toml))
else:
all_errors.append(f"skills/{d.name}: Missing skill.toml")
stats["skills"] = len(skill_dirs)
# --- Aliases ---
all_errors.extend(validate_aliases_file(root / "aliases.toml"))
# --- Report ---
print("=" * 60) print("=" * 60)
print("LibreFang Model Catalog Validation") print("LibreFang Registry Validation")
print("=" * 60) print("=" * 60)
print() print()
@@ -209,16 +312,10 @@ def main():
print(f" - {error}") print(f" - {error}")
print() print()
print(f"Provider files: {len(toml_files)}") print("Content summary:")
print(f"Total models: {total_models}") for key in ("providers", "models", "agents", "hands", "integrations", "skills"):
print() if key in stats:
print(f" {key:20s} {stats[key]:>4}")
print("Per-provider model counts:")
for provider_id in sorted(provider_counts.keys()):
count = provider_counts[provider_id]
if count > 0:
print(f" {provider_id:30s} {count:>3}")
print() print()
if all_errors: if all_errors: