Skip to content

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:

  1. The name field 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.
  2. The display_name is the brand name, optionally with a clarifying parenthetical. Examples: "Example Provider", "Another Provider (Product)".
  3. The description is 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:

auth_type=AuthType.API_KEY,
supported_auth_types=(AuthType.API_KEY,),

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:

auth_type=AuthType.API_KEY,
supported_auth_types=(AuthType.API_KEY, AuthType.SUBSCRIPTION),

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_cost has the provider's models, ship default_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_models list (search presets.py for default_models=().
  • Never invent pricing numbers. If pricing isn't verifiable from a primary source, ship default_models=() and note the gap.

Default source: lobe-icons, MIT licensed (Copyright 2023 LobeHub). Fetch path:

https://raw.githubusercontent.com/lobehub/lobe-icons/master/packages/static-svg/icons/<slug>.svg

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 currentColor only (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.

Two places need updating:

  1. web/src/components/providers/ProviderLogo.tsx -- add the preset name to the KNOWN_LOGOS ReadonlySet (alphabetical). The component uses this set to decide between the brand mark and the Server fallback; mask-image cannot fire onError.
  2. 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:

  1. Add the preset constant alphabetically inside the # Cloud providers block (or # Self-hosted / local for LocalPreset).
  2. Add the constant to the _FEATURED_PRESETS tuple, alphabetically by name.

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:

  1. Add the preset name to the class-level _CLOUD_PRESETS (or _LOCAL_PRESETS) tuple on TestProviderPresets, alphabetical. These tuples are the test ledger that test_all_featured_presets_categorized compares against the runtime registry to detect drift; they are distinct from the production _FEATURED_PRESETS constant updated in Step 7.
  2. Extend the @pytest.mark.parametrize cases for test_cloud_preset_does_not_require_base_url (if the preset doesn't require a base URL).
  3. Extend the test_other_cloud_presets_api_key_only enumeration (if API-key only).
  4. Extend the @pytest.mark.parametrize cases for test_new_branded_preset_routes_via_litellm. If the preset has a divergent litellm_provider (a provider whose chat namespace differs from its bare brand namespace), add the expected = "<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:

npm --prefix web run dev
  • 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_type and litellm_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):

feat(providers): add <Provider Name> preset (#<issue>)

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