Adding a Provider¶
This guide walks through every step of adding a new LLM provider preset, from picking the right preset kind through to landing the PR. It collects in one place what was previously scattered between docs/design/providers.md, the logo provenance README, and tribal knowledge.
When you do not need a preset¶
SynthOrg auto-derives a soft preset for every chat-capable provider in litellm.model_cost that is not already covered by a featured (hand-curated) preset and not denied by _LITELLM_NAMESPACE_DENYLIST / _LITELLM_NAMESPACE_DENY_PREFIXES in src/synthorg/providers/preset_softlist.py. Soft presets render in the wizard's "More providers via LiteLLM" collapsible section with the generic Lucide Server fallback icon.
So before starting: check whether your provider is already surfaced as a soft preset. Run:
uv run python -c 'from synthorg.providers.presets import list_soft_presets; print(*[p.name for p in list_soft_presets()], sep="\n")' | grep '<namespace>'
If it's already there, you only need a branded preset if you want to add: a brand logo, a curated description, or default_models fallback for when LiteLLM's model_cost is empty for that namespace.
If your provider is in the LiteLLM denylist (IAM-bound, OAuth-bound, deprecated, niche), you'll need to either remove it from the denylist (with justification) or ship a featured preset that handles its specific requirements.
Step 1 -- Pick the preset kind¶
Two preset kinds are expressed as a discriminated union (kind field):
| Kind | When to use | Carries |
|---|---|---|
CloudPreset |
hosted, paid-API providers | supported_auth_types, default_models, is_featured |
LocalPreset |
self-hosted / local LLM servers | candidate_urls (auto-detect probes), supports_model_pull, supports_model_delete, supports_model_config |
Both kinds inherit from _BasePreset and share: name, display_name, description, driver, litellm_provider, auth_type, default_base_url, requires_base_url, is_featured.
If the user installs your provider on their own machine, you want LocalPreset. If it's a hosted API the user pays per-token for, you want CloudPreset. Anything in between (managed-cloud variants of an otherwise self-hosted server) ships as a separate CloudPreset even if the underlying server is the same software, because the auth shape differs.
Step 2 -- Pick the preset name¶
Three rules:
- The
namefield is the machine-readable identifier and the SVG filename. Use the LiteLLM namespace exactly. Underscored namespaces (e.g.example_provider,another_example_ai) stay as the preset name verbatim. This avoids surprising users who paste a model string from LiteLLM's docs. - The
display_nameis the brand name, optionally with a clarifying parenthetical. Examples:"Example Provider","Another Provider (Product)". - The
descriptionis one short sentence that names what is distinctive about the provider. Don't mention "models" -- it's redundant. Examples:"Long-context inference from example-provider","Wafer-scale open-model serving".
The litellm_provider field is almost always the same as name. Occasionally a provider's chat-completion namespace in LiteLLM differs from the bare brand namespace; one curated preset uses litellm_provider="<brand>_chat" because the bare <brand>/ namespace in LiteLLM is the deprecated completions endpoint while <brand>_chat/ is the chat completions API SynthOrg uses. When in doubt, check the LiteLLM provider page for the routing string used in chat-completion examples.
Step 3 -- Wire auth_type + supported_auth_types¶
Most cloud providers are API-key only:
The exceptions are documented in docs/research/llm-provider-auth-survey.md. As of the most recent survey, one mainstream provider's consumer subscription doubles as an API credential, and so its preset declares both API-key and subscription auth:
The auth_type is the wizard's default; supported_auth_types is the menu of options the wizard offers.
Before adding a new auth-type combination: re-read the research survey to verify the provider actually supports it. The market converged on API-key-only for third-party clients; do not infer new auth surfaces from blog posts or rumours -- only from primary docs. If you find a meaningful new auth surface, update the survey first, then ship the preset.
AuthType.OAUTH already routes through ConnectionCatalog and reads access_token from resolved credentials, with PKCE handled internally by synthorg.integrations.oauth.flows.AuthorizationCodeFlow. There is no separate AuthType.OAUTH_PKCE enum variant -- PKCE is an implementation detail of the OAuth flow.
Step 4 -- Decide on default_models¶
CloudPreset.default_models is a fallback list used when litellm.model_cost returns no entries for litellm_provider. Most providers have full coverage in LiteLLM's database, so the fallback is rarely needed.
Rules:
- If LiteLLM
model_costhas the provider's models, shipdefault_models=(). This is the common case for most existing presets. - If LiteLLM's coverage is empty or stale, ship 2-3 conservative entries (1 large, 1 medium, 1 small) with verified pricing from the provider's pricing page. Cite the source URL with retrieval date in the commit message. Match the precedent of the few existing presets that ship a non-empty
default_modelslist (searchpresets.pyfordefault_models=(). - Never invent pricing numbers. If pricing isn't verifiable from a primary source, ship
default_models=()and note the gap.
Step 5 -- Source a brand logo¶
Default source: lobe-icons, MIT licensed (Copyright 2023 LobeHub). Fetch path:
The slug is usually the same as the preset name. Some preset names differ from their lobe-icons slug (an underscored preset name like <brand>_ai may map to a bare <brand> slug); consult web/public/provider-logos/README.md for the existing mapping before fetching.
Verify the SVG before committing:
- Monochrome
currentColoronly (no inline colour values) viewBox="0 0 24 24",width="1em"height="1em"- No
<script>or event-handler attributes (XSS surface)
Fallback: if the brand isn't on lobe-icons, find the official brand kit and verify their guidelines permit "integrates with X" usage. Document the source in the README provenance table. If you can't find a permitted source, skip the logo: SynthOrg's ProviderLogo component falls back to the Lucide Server icon when the preset name is not in KNOWN_LOGOS. A missing logo is not a blocker; an unlicensed logo is.
Logo file path: web/public/provider-logos/<preset_name>.svg (filename matches the preset's name). Save with the Write tool, never via Bash redirect.
Step 6 -- Register the logo¶
Two places need updating:
web/src/components/providers/ProviderLogo.tsx-- add the preset name to theKNOWN_LOGOSReadonlySet(alphabetical). The component uses this set to decide between the brand mark and theServerfallback;mask-imagecannot fireonError.web/public/provider-logos/README.md-- add a row to the provenance table (alphabetical) with the preset filename and the lobe-icons slug (or alternative source).
Step 7 -- Add the preset definition¶
In src/synthorg/providers/presets.py:
- Add the preset constant alphabetically inside the
# Cloud providersblock (or# Self-hosted / localforLocalPreset). - Add the constant to the
_FEATURED_PRESETStuple, alphabetically byname.
A canonical CloudPreset example:
_EXAMPLE_PROVIDER = CloudPreset(
name="example_provider",
display_name="Example Provider",
description="Long-context inference from example-provider",
driver="litellm",
litellm_provider="example_provider",
auth_type=AuthType.API_KEY,
supported_auth_types=(AuthType.API_KEY,),
default_models=(),
is_featured=True, # Defaults to True; set explicitly for clarity.
)
A canonical LocalPreset example (for a local inference server that
exposes a known wire-protocol adapter and runs on a user-chosen port
that collides with common defaults):
_EXAMPLE_LOCAL_SERVER = LocalPreset(
name="example-local-server",
display_name="Example Local Server",
description="High-throughput local inference engine",
driver="litellm",
# Set litellm_provider to whichever LiteLLM adapter namespace
# matches the local server's wire protocol; consult the LiteLLM
# docs for the correct identifier.
litellm_provider="example-adapter",
auth_type=AuthType.NONE,
default_base_url="http://localhost:8000/v1",
requires_base_url=True,
# candidate_urls intentionally empty when the default port is a
# known collision risk; users configure manually.
candidate_urls=(),
is_featured=True, # Defaults to True; set explicitly for clarity.
)
Step 8 -- Tests¶
In tests/unit/providers/test_presets.py:
- Add the preset name to the class-level
_CLOUD_PRESETS(or_LOCAL_PRESETS) tuple onTestProviderPresets, alphabetical. These tuples are the test ledger thattest_all_featured_presets_categorizedcompares against the runtime registry to detect drift; they are distinct from the production_FEATURED_PRESETSconstant updated in Step 7. - Extend the
@pytest.mark.parametrizecases fortest_cloud_preset_does_not_require_base_url(if the preset doesn't require a base URL). - Extend the
test_other_cloud_presets_api_key_onlyenumeration (if API-key only). - Extend the
@pytest.mark.parametrizecases fortest_new_branded_preset_routes_via_litellm. If the preset has a divergentlitellm_provider(a provider whose chat namespace differs from its bare brand namespace), add theexpected = "<override>"branch.
The featured/soft tier invariants are already tested generically: test_featured_presets_are_marked_featured, test_soft_presets_skip_denylist_namespaces, etc. You don't need to add per-preset variants of those.
Step 9 -- Verify¶
uv run python -m pytest tests/unit/providers/test_presets.py -m unit -n 8 -v
uv run mypy --num-workers=4 src/synthorg/providers/presets.py tests/unit/providers/test_presets.py
uv run ruff check src/ tests/ --fix
uv run ruff format src/ tests/
npm --prefix web run lint
npm --prefix web run type-check
Then a manual smoke:
- Navigate to Settings -> Providers
- Confirm the new preset appears in the Cloud grid (or "More providers" if it's a soft override)
- Confirm the logo renders correctly (mask-image + currentColor, theme-aware)
- Click the preset, confirm the credential form opens with the right
auth_typeandlitellm_provider - Cancel the form (no provider creation needed for verification)
Step 10 -- Commit + PR¶
Use /pre-pr-review to land the PR. gh pr create is blocked by hookify; the skill runs review agents and creates the PR itself.
Commit message style (per CLAUDE.md git conventions):
If the preset has a non-trivial divergence (subscription auth, OAuth, base-URL requirement), document it in the commit body with a link to the relevant section of the auth survey.
Reference¶
docs/design/providers.md-- design spec for the provider layerdocs/research/llm-provider-auth-survey.md-- the auth survey informing which providers ship as branded presetsweb/public/provider-logos/README.md-- logo provenance tablesrc/synthorg/providers/presets.py-- preset definitions + auto-derive layersrc/synthorg/providers/enums.py--AuthTypeenumsrc/synthorg/integrations/oauth/-- OAuth 2.1 + PKCE flow infrastructure (already wired; no new code needed for OAuth presets)