n4nAI

A GitHub Actions workflow for n4n-powered apps

Build a practical CI pipeline that tests and smoke-checks LLM apps using n4n.ai's OpenAI-compatible gateway, with GitHub Actions and live fallback validation.

n4n Team3 min read631 words

Audio narration

Coming soon — every post will get a voice note here.

A reliable GitHub Actions workflow for n4n apps does more than run unit tests—it validates that your LLM calls still succeed when a provider is degraded. This tutorial builds a pipeline that lints code, runs pytest, and executes a live smoke test against the OpenAI-compatible endpoint on n4n.ai, which addresses 240+ models and falls back automatically on provider errors.

Prerequisites

  • A GitHub repository containing a Python LLM app that uses the OpenAI SDK pointed at a gateway.
  • Python 3.11+ and pytest installed locally for testing.
  • An API key for the gateway stored as N4N_API_KEY in GitHub Actions secrets.
  • Familiarity with YAML and GitHub Actions syntax.

We’ll assume the app code lives in src/ and tests in tests/. The pattern works for any language that can call an OpenAI-compatible HTTP API, but the samples are Python.

Step 1: Structure the app

Create a minimal client that talks to the gateway. The OpenAI Python library accepts a base_url, so no custom HTTP code is needed.

# src/client.py
import os
from openai import OpenAI

def get_client(api_key: str) -> OpenAI:
    return OpenAI(
        api_key=api_key,
        base_url="https://api.n4n.ai/v1",  # OpenAI-compatible endpoint
        default_headers={"X-App": "ci-smoke-test"},
    )

def chat(prompt: str, model: str = "mistralai/mistral-7b-instruct") -> str:
    client = get_client(os.environ["N4N_API_KEY"])
    resp = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}],
        max_tokens=32,
    )
    return resp.choices[0].message.content

The gateway forwards any provider cache-control hints you send via headers, so the default_headers are passed through untouched. Keep the default model small and cheap for CI.

Step 2: Write a smoke test

The smoke test should hit the live endpoint with a tiny model and assert we get tokens back. This catches credential, routing, and network regressions before they reach production.

# tests/test_smoke.py
import os
import pytest
from src.client import chat

def test_live_completion():
    if not os.environ.get("N4N_API_KEY"):
        pytest.skip("N4N_API_KEY not set")
    out = chat("Say hello in one word.")
    assert isinstance(out, str)
    assert len(out.strip()) > 0

Run it locally with pytest tests/test_smoke.py -s. Expected output:

============================= test session starts =============================
collected 1 item

tests/test_smoke.py .                                                        [100%]

============================== 1 passed in 0.42s ==============================

If you omit the key locally, the test skips instead of failing. That same skip behavior will protect forks in CI.

Step 3: Define the GitHub Actions workflow

Create .github/workflows/ci.yml. The pipeline has three jobs: lint, test, and smoke. We separate smoke from unit tests because it requires secrets and network egress.

name: CI for n4n apps
on:
  push:
    branches: [main]
  pull_request:

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - run: pip install ruff
      - run: ruff check src tests

  test:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - run: pip install pytest openai
      - run: pytest tests/ -k "not live"

  smoke:
    runs-on: ubuntu-latest
    needs: test
    if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
    env:
      N4N_API_KEY: ${{ secrets.N4N_API_KEY }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - run: pip install pytest openai
      - run: pytest tests/test_smoke.py -s

The if condition prevents running the live test on forks without the secret. This GitHub Actions workflow for n4n apps keeps unit tests fast and isolates external calls to a single guarded job. To speed up installs, add actions/cache for pip, but for a tiny dependency set it’s optional.

Step 4: Verify routing directives and cache hints

The gateway honors client routing directives and forwards provider cache-control hints. If your app sets Cache-Control in headers, the upstream provider receives it. You can extend the client to send those hints and confirm the call still succeeds.

# src/client.py (add)
def chat_with_cache(prompt: str, model: str = "mistralai/mistral-7b-instruct") -> str:
    client = OpenAI(
        api_key=os.environ["N4N_API_KEY"],
        base_url="https://api.n4n.ai/v1",
        default_headers={"Cache-Control": "max-age=3600"},
    )
    resp = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}],
        max_tokens=16,
    )
    return resp.choices[0].message.content

Add a test that calls this and checks output. The gateway’s automatic fallback means even if the primary provider for that model is rate-limited, you still get a completion from a secondary provider—no code change required.

Step 5: Run the pipeline and read output

Push to main. GitHub Actions will show three sequential jobs. The smoke job log should contain:

============================= test session starts =============================
collected 1 item

tests/test_smoke.py .                                                        [100%]

============================== 1 passed in 0.51s ==============================

If the key is missing, the test skips rather than fails. That’s intentional—PRs from forks shouldn’t break your CI. A more advanced GitHub Actions workflow for n4n apps might run the smoke test against multiple models in a matrix to detect per-model degradation:

strategy:
  matrix:
    model: ["mistralai/mistral-7b-instruct", "meta-llama/llama-3-8b-instruct"]

Pass matrix.model into the test via an environment variable and parameterize the client call. This surfaces which model routes are healthy on every commit.

Step 6: Track per-token usage

The gateway provides per-token usage metering on every response. Capture it in the smoke test to surface cost signals in CI logs:

def test_usage_visible():
    from src.client import get_client
    client = get_client(os.environ["N4N_API_KEY"])
    resp = client.chat.completions.create(
        model="mistralai/mistral-7b-instruct",
        messages=[{"role": "user", "content": "Hi"}],
        max_tokens=8,
    )
    assert resp.usage.prompt_tokens > 0
    assert resp.usage.completion_tokens > 0
    print(f"prompt={resp.usage.prompt_tokens} completion={resp.usage.completion_tokens}")

In Actions, the print appears under the pytest run, giving you a lightweight audit trail of token spend per commit. If you run the matrix from Step 5, you’ll see usage for each model separately.

Final notes

Keep the live smoke test small. A 32-token completion against a cheap model costs fractions of a cent, but a bloated prompt can add up across hundreds of PRs. The GitHub Actions workflow for n4n apps described here is deliberately minimal: lint, isolated unit tests, and one guarded live call. From here, add model matrices, response-schema validation, or latency guards as your app demands.

Tagsn4ngithub-actionsci-cdllm-apps

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All ci/cd pipelines for llm apps posts →