Skip to content
Development
Skill

/add-provider

Checklist for adding a new AI provider (image, video, text, audio) that satisfies all 5 integration principles — ACL, bulkhead, idempotency, observability, and contract test. Load when integrating any new external AI API. Prevents the most common mistake of pasting httpx calls

From plugin
vibe-coding-rules
468 skills
Install
$ npx -y skills add yerdaulet-damir/vibe-coding-rules --skill add-provider --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/add-provider

Context preview

The summary Claude sees to decide when to auto-load this skill.

Checklist for adding a new AI provider (image, video, text, audio) that satisfies all 5 integration principles — ACL, bulkhead, idempotency, observability, and contract test. Load when integrating any new external AI API. Prevents the most common mistake of pasting httpx calls

SKILL.md

add-provider.SKILL.md
name: add-provider
description: Checklist for adding a new AI provider (image, video, text, audio) that satisfies all 5 integration principles — ACL, bulkhead, idempotency, observability, and contract test. Load when integrating any new external AI API. Prevents the most common mistake of pasting httpx calls directly into a service.

add-provider

A provider added wrong leaks its API shape into your business logic. One API change from the vendor → rewrite half your service. Follow this checklist.

---

Step 1 — Decide: file or folder?

| Provider supports | Structure | |------------------|-----------| | One modality (text only, image only) | `app/providers/<name>.py` | | Multiple modalities (image + video) | `app/providers/<name>/` with `image.py`, `video.py`, `__init__.py` |

Principle A4: one file per format when a provider handles multiple formats.

---

Step 2 — Create the provider file from this template

# app/providers/<name>.py  (or app/providers/<name>/image.py)
from __future__ import annotations

import logging
from decimal import Decimal

import httpx

from app.core.bulkhead import get_provider_client   # Principle B5
from app.core.context import provider_ctx           # Principle B7
from app.providers.base import AIProvider, JobRequest, JobResult
from app.providers.exceptions import (
    ProviderError,
    ProviderInvalidResponseError,
    ProviderRateLimitError,
    ProviderTimeoutError,
)

logger = logging.getLogger(__name__)


class <Name>Provider:
    provider_name = "<name>"                        # used in logs + bulkhead key

    async def generate(self, request: JobRequest) -> JobResult:
        provider_ctx.set(self.provider_name)        # Principle B7: set before any I/O
        client = get_provider_client(self.provider_name)  # Principle B5: isolated client

        try:
            response = await client.post(
                "/v1/generate",
                json=self._build_payload(request),
                headers={"X-Idempotency-Key": request.idempotency_key},  # Principle B6
                timeout=30.0,
            )
            response.raise_for_status()
        except httpx.TimeoutException as e:
            raise ProviderTimeoutError(
                message=str(e), provider=self.provider_name, retryable=True
            ) from e
        except httpx.HTTPStatusError as e:
            self._map_http_error(e)

        return self._parse_response(response.json(), request)  # Principle B3: ACL here

    def _build_payload(self, request: JobRequest) -> dict:
        return {"prompt": request.prompt, "model": request.model_id, **request.params}

    def _parse_response(self, data: dict, request: JobRequest) -> JobResult:
        # ACL: validate and map to our domain type. Never return raw data.
        try:
            url = data["output"]["url"]          # adjust to actual provider shape
            cost = Decimal(str(data.get("cost", "0")))
        except (KeyError, TypeError) as e:
            raise ProviderInvalidResponseError(
                message=f"Unexpected response shape: {e}",
                provider=self.provider_name,
                retryable=False,
                raw_response=data,
            ) from e
        return JobResult(
            url=url,
            cost_usd=cost,
            provider=self.provider_name,
            model_id=request.model_id,
        )

    def _map_http_error(self, e: httpx.HTTPStatusError) -> None:
        if e.response.status_code == 429:
            retry_after = int(e.response.headers.get("Retry-After", 60))
            raise ProviderRateLimitError(
                message="Rate limited",
                provider=self.provider_name,
                retryable=True,
                retry_after=retry_after,
            ) from e
        raise ProviderError(
            message=f"HTTP {e.response.status_code}",
            provider=self.provider_name,
            retryable=e.response.status_code >= 500,
        ) from e

---

Step 3 — Register in bulkhead (if not auto-registered)

`get_provider_client("<name>")` auto-creates an isolated `httpx.AsyncClient` on first call with the limits from `settings.HTTP_MAX_CONNECTIONS`. No extra step needed unless the provider needs custom limits:

# app/core/bulkhead.py — only if non-default limits needed
_PROVIDER_LIMITS = {
    "<name>": httpx.Limits(max_connections=5, max_keepalive_connections=2),
}

---

Step 4 — Add contract test snapshot

Create a fixture with a real (or realistic) provider response:

touch reference/tests/fixtures/<name>_response.json
{
  "output": { "url": "https://cdn.example.com/result.png" },
  "cost": "0.004",
  "id": "job_abc123"
}

Add a parser test in `tests/integration/test_provider_contracts.py`:

def test_<name>_response_parses_to_job_result(load_fixture):
    raw = load_fixture("<name>_response.json")
    provider = <Name>Provider()
    request = JobRequest(model_id="m", prompt="p", modality="image",
                         idempotency_key="k")
    result = provider._parse_response(raw, request)
    assert isinstance(result, JobResult)
    assert result.url.startswith("https://")
    assert result.cost_usd >= Decimal("0")

This test breaks in CI when the provider changes their response shape. That's the point.

---

Step 5 — Export from providers package

# app/providers/__init__.py
from app.providers.<name> import <Name>Provider

__all__ = [..., "<Name>Provider"]

---

Verification checklist

| Principle | Check | |-----------|-------| | B3 — ACL: returns JobResult not dict | `_parse_response` returns `JobResult` | | B5 — Bulkhead: isolated client | uses `get_provider_client(self.provider_name)` | | B6 — Idempotency: key forwarded | `headers={"X-Idempotency-Key": request.idempotency_key}` | | B7 — Observability: context set | `provider_ctx.set(self.provider_name)` before I/O | | B9 — Contract test exists | fixture + parser test in

Read more
Ships withvibe-coding-rules

54 production architecture principles your AI coding agent (Claude Code, Cursor) follows automatically. Drop-in CLAUDE.md, .cursor/rules/, and .claude/skills/ for FastAPI, Next.js 15, and Go 1.22+. MIT.

Get the whole plugin
Stats
46
Stars
0
Forks
Maintained
Maintenance
TypeScript
Language
MIT
License
4mo ago
Last commit
4mo ago
Created

Repo: yerdaulet-damir/vibe-coding-rules

Other skills on vibe-coding-rules.