LangChain’s OpenAI-compatible client accepts a base URL and API key at initialization, but hardcoding credentials in source control is a liability. The standard pattern — used across the LangChain ecosystem — is to configure the client through environment variables so the same code runs locally, in CI, and in production without changes. This guide walks through wiring LangChain to n4n.ai using only environment variables, verifying the connection, and handling common pitfalls.
Step 1: Install the required packages
Start with a clean virtual environment. You need langchain-openai for the ChatOpenAI wrapper and python-dotenv if you want to load a .env file locally.
python -m venv .venv
source .venv/bin/activate
pip install langchain-openai python-dotenv
If you’re already in a project with a pyproject.toml or requirements.txt, add those two dependencies there instead.
Step 2: Create a .env file (local development only)
Never commit secrets. Create a .env file at your repository root and add it to .gitignore.
echo ".env" >> .gitignore
Now populate .env with the two values LangChain’s OpenAI client reads by default:
OPENAI_API_KEY=sk-n4n-XXXXXXXXXXXXXXXXXXXXXXXX
OPENAI_BASE_URL=https://api.n4n.ai/v1
The key format above is illustrative — use the actual key issued from your n4n.ai dashboard. The base URL must include the /v1 path suffix; the client appends /chat/completions and other endpoints to it.
If you prefer not to use a .env file, export the variables directly in your shell profile or CI environment:
export OPENAI_API_KEY=sk-n4n-XXXXXXXXXXXXXXXXXXXXXXXX
export OPENAI_BASE_URL=https://api.n4n.ai/v1
Step 3: Load environment variables in your application
LangChain’s ChatOpenAI class reads OPENAI_API_KEY and OPENAI_BASE_URL automatically at instantiation time. You don’t need to pass them explicitly — but you do need to ensure they’re loaded before the first import of langchain_openai.
For scripts and notebooks, load .env at the very top of your entry point:
# app.py
from dotenv import load_dotenv
load_dotenv() # reads .env into os.environ
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini")
response = llm.invoke("Reply with the word 'ok' and nothing else.")
print(response.content)
Run it:
python app.py
# ok
If you see ok printed, the client successfully routed through n4n.ai and returned a completion.
Step 4: Verify the request actually hit n4n.ai
A successful response doesn’t prove the request went where you think it went. Confirm the network path in two ways.
Option A: Enable HTTP logging
Add this block before importing ChatOpenAI to see the full request line and headers on stdout:
import logging
import http.client
http.client.HTTPConnection.debuglevel = 1
logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True
Run the script again. You should see a POST https://api.n4n.ai/v1/chat/completions line in the debug output, confirming the base URL was respected.
Option B: Inspect response headers for routing hints
n4n.ai returns a x-n4n-model header indicating which upstream provider served the request, and forwards provider cache-control hints. Capture it programmatically:
from langchain_openai import ChatOpenAI
import httpx
# Monkey-patch to capture the last response
original_init = ChatOpenAI.__init__
def capturing_init(self, *args, **kwargs):
original_init(self, *args, **kwargs)
self._last_response_headers = {}
original_request = self.client.request
def capturing_request(*a, **kw):
resp = original_request(*a, **kw)
self._last_response_headers = dict(resp.headers)
return resp
self.client.request = capturing_request
ChatOpenAI.__init__ = capturing_init
llm = ChatOpenAI(model="gpt-4o-mini")
response = llm.invoke("Say 'verified'")
print("Response:", response.content)
print("x-n4n-model:", llm._last_response_headers.get("x-n4n-model"))
print("cache-control:", llm._last_response_headers.get("cache-control"))
Output example:
Response: verified
x-n4n-model: anthropic/claude-3.5-sonnet
cache-control: public, max-age=0, must-revalidate
The presence of x-n4n-model confirms the request transited n4n.ai’s gateway.
Step 5: Override the model at runtime (optional)
The model parameter you pass to ChatOpenAI is forwarded as-is to the gateway. n4n.ai accepts any model identifier it recognizes — including provider-prefixed names like anthropic/claude-3.5-sonnet or google/gemini-1.5-pro.
llm = ChatOpenAI(model="anthropic/claude-3.5-sonnet")
response = llm.invoke("What is 2+2?")
print(response.content)
You can also set a default model via environment variable if you want zero code changes across environments:
# .env
OPENAI_API_KEY=sk-n4n-XXXXXXXXXXXXXXXXXXXXXXXX
OPENAI_BASE_URL=https://api.n4n.ai/v1
DEFAULT_MODEL=anthropic/claude-3.5-sonnet
import os
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model=os.getenv("DEFAULT_MODEL", "gpt-4o-mini"))
Step 6: Configure timeouts and retries for production
The default httpx client has no timeout. In production, always set explicit timeouts and a retry policy. ChatOpenAI exposes timeout and max_retries parameters that map to the underlying httpx.Client configuration.
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="gpt-4o-mini",
timeout=httpx.Timeout(connect=5.0, read=60.0, write=10.0, pool=5.0),
max_retries=3,
)
For more granular control — per-endpoint timeouts, custom retry predicates, or a shared connection pool across multiple ChatOpenAI instances — construct your own httpx.Client and pass it via the http_client parameter:
import httpx
from langchain_openai import ChatOpenAI
shared_client = httpx.Client(
timeout=httpx.Timeout(connect=5.0, read=60.0),
limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
transport=httpx.HTTPTransport(retries=3),
)
llm = ChatOpenAI(model="gpt-4o-mini", http_client=shared_client)
Reuse shared_client across your application to benefit from connection pooling.
Step 7: Handle streaming responses
Streaming works identically to the official OpenAI client. Use stream() for token-by-token iteration or astream() in async contexts.
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", streaming=True)
for chunk in llm.stream("Count to 5 slowly."):
print(chunk.content, end="", flush=True)
print()
The gateway forwards server-sent events from the upstream provider without buffering, so latency to first token matches a direct provider call.
Step 8: Structured output with Pydantic models
LangChain’s with_structured_output() works through n4n.ai because the gateway passes through the response_format parameter to providers that support it (OpenAI, Anthropic, Google). Define a schema and bind it:
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
class Extraction(BaseModel):
name: str = Field(description="Person's full name")
email: str = Field(description="Email address")
confidence: float = Field(ge=0.0, le=1.0, description="Confidence score")
llm = ChatOpenAI(model="gpt-4o-mini")
structured_llm = llm.with_structured_output(Extraction)
result = structured_llm.invoke(
"Extract contact info: John Doe, john.doe@example.com, very confident."
)
print(result)
# name='John Doe' email='john.doe@example.com' confidence=0.95
If the selected upstream model doesn’t support structured output, the gateway returns a 400 with a clear error — handle it with a fallback model or parsing logic.
Step 9: CI/CD and production deployment
In CI and production, do not use .env files. Inject OPENAI_API_KEY and OPENAI_BASE_URL as protected secrets in your platform:
- GitHub Actions: Repository Settings → Secrets → Actions →
OPENAI_API_KEY,OPENAI_BASE_URL - GitLab CI/CD: Settings → CI/CD → Variables → protected/masked
- Kubernetes:
Secretresource mounted as environment variables - Vercel/Netlify/Render: Project settings → Environment Variables
Your application code remains unchanged — it reads os.environ at startup.
# .github/workflows/test.yml
jobs:
test:
runs-on: ubuntu-latest
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install -r requirements.txt
- run: python -m pytest
Step 10: Troubleshooting common issues
“AuthenticationError: Invalid API key”
The key is malformed, revoked, or not scoped for the requested model. Verify in the n4n.ai dashboard that the key is active and has access to the model you’re requesting. Rotate and re-paste if in doubt — invisible whitespace is a frequent culprit.
“APIConnectionError: Cannot connect to host api.n4n.ai:443”
DNS or egress firewall. From the same network, run:
curl -v https://api.n4n.ai/v1/models \
-H "Authorization: Bearer $OPENAI_API_KEY"
If curl fails, the issue is network-level, not LangChain.
“RateLimitError” or 429 responses
n4n.ai automatically falls back to healthy providers when one is rate-limited or degraded. If you still see 429s, you’ve exceeded your account-level quota. Implement client-side backoff:
from langchain_openai import ChatOpenAI
from tenacity import retry, stop_after_attempt, wait_exponential_jitter
llm = ChatOpenAI(model="gpt-4o-mini")
@retry(
wait=wait_exponential_jitter(initial=1, max=30),
stop=stop_after_attempt(5),
)
def safe_invoke(prompt: str):
return llm.invoke(prompt)
“Model not found” for a model you know exists
The model identifier must match exactly what n4n.ai’s catalog exposes. List available models programmatically:
import httpx
import os
resp = httpx.get(
f"{os.getenv('OPENAI_BASE_URL')}/models",
headers={"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}"}
)
resp.raise_for_status()
for m in resp.json()["data"]:
print(m["id"])
Use the exact id string from that list.
Streaming hangs or times out
Some corporate proxies buffer SSE streams. Test with curl first:
curl -N -X POST https://api.n4n.ai/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Hi"}],"stream":true}'
If curl -N (no buffer) works but your Python stream hangs, configure httpx with http2=True or adjust proxy settings.
Step 11: Verify end-to-end with a smoke test
Add a lightweight integration test to your test suite that runs against the real gateway (optionally gated by an environment flag so it doesn’t run on every PR).
# tests/test_n4n_integration.py
import os
import pytest
from langchain_openai import ChatOpenAI
@pytest.mark.skipif(
not os.getenv("RUN_INTEGRATION_TESTS"),
reason="Set RUN_INTEGRATION_TESTS=1 to run"
)
def test_n4n_gateway_reachable():
llm = ChatOpenAI(model="gpt-4o-mini", timeout=30.0)
response = llm.invoke("Reply with the exact string: SMOKE_TEST_OK")
assert "SMOKE_TEST_OK" in response.content
Run it explicitly:
RUN_INTEGRATION_TESTS=1 pytest tests/test_n4n_integration.py -v
This catches credential drift, network policy changes, and catalog updates before they hit production.
Summary checklist
-
OPENAI_API_KEYandOPENAI_BASE_URLset in environment (.envlocally, secrets in CI/prod) -
OPENAI_BASE_URLincludes/v1suffix -
load_dotenv()called before first LangChain import (if using.env) - Explicit
timeoutandmax_retriesconfigured onChatOpenAI - Verified
x-n4n-modelheader present on responses - Smoke test added to CI gate
That’s it. Your LangChain application now routes through n4n.ai with zero vendor-specific code — just standard OpenAI-compatible environment variables.