A production-quality CLI tool for B2B companies to measure how often their brand appears in AI model responses across different buyer personas and prompt intents — and to compute actionable Share of Voice metrics.
- Overview
- Project Structure
- Setup
- Configuration
- Free and Zero-Cost Options
- Prompts
- Running the Tool
- Output Files
- How to Add Models
- How to Add Prompts
- Metrics Reference
- Example Output
AI SOV Analyzer answers the question: "When buyers ask AI models about our category, does our company show up?"
It runs a configurable set of prompts against any AI model endpoint, parses the responses for brand mentions, and computes:
- Presence Rate — how often your company appears
- AI Share of Voice — your mention share vs competitors
- Recommendation Rate — how often you're listed first
- Missed Prompts — high-intent queries where competitors appear but you don't
Works with any OpenAI-compatible endpoint — bring your own API key and model URL. No lock-in to any specific provider.
/
├── config.json # All variables: company, models, API keys, settings
├── prompts.json # Prompts with persona + intent tags
├── runner.py # CLI entry point
├── parser.py # Response text parsing & mention extraction
├── metrics.py # SOV metrics computation engine
├── insights.py # Rule-based insight generation
├── utils.py # Config loading, env var expansion, logging
├── model_clients/
│ ├── __init__.py # Provider registry (auto-falls back to generic)
│ ├── generic_client.py # Universal OpenAI-compatible client (default)
│ ├── openai_client.py # Native OpenAI client (optional)
│ └── anthropic_client.py# Native Anthropic client (optional)
├── output.json # Generated: structured metrics + insights
└── raw_responses.json # Generated: full responses + parsed mentions
pip install openaiOnly the
openaipackage is required — the generic client uses it to talk to any OpenAI-compatible endpoint. If you use the native Anthropic client specifically, also runpip install anthropic.
export MY_API_KEY="your-key-here"API keys in
config.jsonuse the${VAR_NAME}placeholder syntax and are resolved at runtime. Never hardcode keys directly in config.json.No API key? If you're using a local model via Ollama, skip this step entirely — local models require no authentication. See Free and Zero-Cost Options.
Edit config.json with your company name, competitors, and at least one model endpoint (see Configuration below).
config.json is the single source of truth for all runtime variables.
| Field | Type | Description |
|---|---|---|
company |
string | Your company's primary name |
aliases |
string[] | Alternate names / abbreviations to also detect |
competitors |
string[] | Competitor names to track |
| Field | Type | Description |
|---|---|---|
settings.runs_per_prompt |
int | How many times to run each prompt per model |
settings.temperature |
float | Model temperature (0.0–1.0) |
settings.max_tokens |
int | Max tokens per response |
| Field | Required | Description |
|---|---|---|
name |
yes | Display name shown in output |
endpoint |
yes | Base URL of the API, e.g. https://openrouter.ai/api/v1 |
api_key |
no* | ${ENV_VAR} reference to your key. Can be omitted entirely for local models (e.g. Ollama) that require no authentication. |
model |
yes | Model ID as the endpoint expects it |
provider |
no | Hint for client selection. Omit or set to "generic" for any custom endpoint. Use "openai" or "anthropic" only if you want the native SDK behaviour. |
extra_headers |
no | Additional HTTP headers (e.g. required by OpenRouter) |
*
api_keyis required for cloud-hosted services (Groq, Gemini, OpenRouter, OpenAI, etc.) but can be omitted for local models that have no authentication.
{
"name": "my-model",
"endpoint": "${MY_ENDPOINT}",
"api_key": "${MY_API_KEY}",
"model": "my-model-id"
}{
"company": "Your Company",
"aliases": ["YourCo", "Your Co"],
"competitors": ["Competitor A", "Competitor B", "Competitor C"],
"settings": {
"runs_per_prompt": 1,
"temperature": 0.7,
"max_tokens": 1024
},
"models": [
{
"name": "my-primary-model",
"provider": "generic",
"api_key": "${MY_API_KEY}",
"model": "${MY_MODEL_ID}",
"endpoint": "${MY_ENDPOINT}"
},
{
"name": "my-second-model",
"provider": "generic",
"api_key": "${MY_SECOND_API_KEY}",
"model": "${MY_SECOND_MODEL_ID}",
"endpoint": "${MY_SECOND_ENDPOINT}",
"extra_headers": { "HTTP-Referer": "https://your-site.com" }
}
]
}OpenRouter (
openrouter.ai) is a convenient way to access GPT-4, Claude, Gemini, Perplexity Sonar, Llama, Mistral and many others under a single API key and a single OpenAI-compatible interface.
config.json includes a model_examples key alongside models. These entries are never executed — the runner only reads the models array. They exist purely as ready-to-use snippets. Copy any entry into "models" to activate it.
You do not need to pay for API access to run this tool. The options below are either entirely free or have a generous free tier.
Run open-source models on your own machine. No account, no API key, no cost.
Install Ollama: https://ollama.com
# pull a model once
ollama pull llama3
# start the server (runs on http://localhost:11434)
ollama serveAdd to config.json — note there is no api_key field:
{
"name": "ollama-llama3",
"provider": "ollama",
"model": "llama3",
"endpoint": "http://localhost:11434/v1"
}Trade-off: Ollama models are open-source (Llama, Mistral, etc.), not the same as ChatGPT or Claude. Results reflect what those models say, not what commercial AI assistants say.
Groq provides a free tier with generous rate limits. Sign up at https://console.groq.com.
export GROQ_API_KEY="your-groq-key"{
"name": "groq-llama3",
"provider": "groq",
"api_key": "${GROQ_API_KEY}",
"model": "llama3-8b-8192",
"endpoint": "https://api.groq.com/openai/v1"
}Google's Gemini Flash has a free tier via AI Studio. Get a key at https://aistudio.google.com.
export GEMINI_API_KEY="your-gemini-key"{
"name": "gemini-flash",
"provider": "gemini",
"api_key": "${GEMINI_API_KEY}",
"model": "gemini-1.5-flash",
"endpoint": "https://generativelanguage.googleapis.com/v1beta/openai"
}OpenRouter gives access to many models under one API key. Some models are free. Sign up at https://openrouter.ai.
export OPENROUTER_API_KEY="your-openrouter-key"{
"name": "openrouter-mixtral",
"provider": "openrouter",
"api_key": "${OPENROUTER_API_KEY}",
"model": "mistralai/mixtral-8x7b-instruct",
"endpoint": "https://openrouter.ai/api/v1"
}If you do use paid APIs, a full SOV run is inexpensive:
| Service | Typical cost per full run (10 prompts × 5 models) |
|---|---|
| OpenAI GPT-4o | ~$0.10–$0.30 |
| Anthropic Claude | ~$0.10–$0.30 |
| Perplexity Sonar | ~$0.05–$0.20 |
| Groq / Gemini Flash | Free (within tier limits) |
| Ollama (local) | $0.00 |
A weekly automated run across all major models costs roughly $5–$15/month.
prompts.json is an array of prompt objects:
[
{
"id": "disc-001",
"persona": "CTO",
"intent": "discovery",
"prompt": "What are the best B2B payment platforms for a SaaS company?"
}
]| Field | Values | Description |
|---|---|---|
id |
string | Unique identifier for tracking |
persona |
CTO, Engineer, Finance, Founder, ... |
Buyer role |
intent |
discovery, comparison, decision, integration |
Funnel stage |
prompt |
string | The exact question sent to the AI model |
python runner.pypython runner.py --config path/to/config.json --prompts path/to/prompts.jsonpython runner.py --output-dir results/run-$(date +%Y%m%d)/python runner.py --dry-runpython runner.py [-h]
[--config CONFIG]
[--prompts PROMPTS]
[--output-dir OUTPUT_DIR]
[--log-level {DEBUG,INFO,WARNING,ERROR}]
[--dry-run]
Structured metrics and insights:
{
"meta": {
"company": "Your Company",
"run_timestamp": "2024-01-15T10:30:00+00:00",
"total_results": 40,
"models_used": ["my-primary-model", "my-second-model"],
"prompts_count": 20
},
"summary": {
"total_prompts_run": 40,
"company_presence_rate": 0.425,
"ai_share_of_voice": 0.183,
"recommendation_rate": 0.075,
"company_mention_count": 22,
"competitor_mention_count": 98,
"competitor_breakdown": {
"Competitor A": { "presence_rate": 0.875, "mention_count": 51 }
}
},
"by_model": { "...per model breakdown..." },
"by_persona": { "...per persona breakdown..." },
"missed_prompts": [ "...prompts where competitors appear but company does not..." ],
"insights": [
"Your Company has moderate AI visibility (42.5% presence rate).",
"Competitor 'Competitor A' dominates with a 87.5% presence rate.",
"Your Company is absent from 12 prompts where competitors are mentioned."
]
}Full responses with parsed mention data per call:
[
{
"model": "my-custom-model",
"persona": "CTO",
"intent": "discovery",
"prompt_id": "disc-001",
"prompt": "What are the best B2B payment platforms...",
"run": 1,
"response": "Here are the top platforms: 1. ...",
"error": null,
"parsed_mentions": {
"company_mentioned": false,
"competitors_mentioned": ["Competitor A", "Competitor B"],
"rank": null,
"mention_count": 0
}
}
]Any endpoint that speaks the OpenAI Chat Completions format works with zero code changes — just add an entry to config.json:
{
"name": "any-display-name",
"endpoint": "https://your-endpoint.com/v1",
"api_key": "${YOUR_ENV_VAR}",
"model": "model-id-as-your-endpoint-expects"
}The provider field defaults to "generic" when omitted. The generic client handles retry logic, temperature, and max_tokens automatically.
- Create
model_clients/myprovider_client.pyimplementing:
def run_prompt(model_config: dict, prompt: str, system_prompt: str, settings: dict) -> str | None:
# call your API, return response text or None on failure
...- Register it in
model_clients/__init__.py:
from model_clients.myprovider_client import run_prompt as myprovider_run
PROVIDER_MAP = {
...
"myprovider": myprovider_run,
}- Set
"provider": "myprovider"in yourconfig.jsonmodel entry.
Add entries to prompts.json — no code changes required:
{
"id": "dec-006",
"persona": "VP Sales",
"intent": "decision",
"prompt": "What CRM tools do you recommend for enterprise B2B sales teams?"
}Intent values: discovery | comparison | decision | integration
Persona values: any string matching your buyer personas.
| Metric | Formula | Interpretation |
|---|---|---|
| Presence Rate | prompts with company / total prompts | Visibility breadth |
| AI Share of Voice | company mentions / (company + all competitor mentions) | Competitive share |
| Recommendation Rate | times ranked #1 / total prompts | Top-of-mind strength |
| Missed Prompts | competitor present, company absent | Visibility gaps |
============================================================
Results for: Your Company
============================================================
Prompts run: 40
Presence Rate: 42.5%
AI Share of Voice: 18.3%
Recommendation Rate: 7.5%
Missed Prompts: 12
============================================================
Key Insights:
• Your Company has moderate AI visibility (42.5% presence rate). There is clear room for improvement.
• Low recommendation rate (7.5%) despite moderate visibility — Your Company is mentioned but rarely positioned first.
• Significant persona gap detected: strong presence in 'CTO' (70.0%) but weak in 'Engineer' (20.0%).
• Competitor 'Competitor A' dominates with a 87.5% presence rate — significantly outpacing Your Company.
• Your Company is absent from 12 prompt(s) where competitors are mentioned. These are high-priority visibility gaps.
Full results saved to: output.json
Raw responses saved to: raw_responses.json
This project is licensed under the Apache License 2.0.