Custom endpoints & auth
A model that is an HTTP endpoint is one you configure here. (It need not be —
if your model runs in-process, keep the same client and swap the
transport; base_url and headers are then
just values your own callable receives and can ignore.)
Auth is a header template: you write "Bearer ${OPENAI_API_KEY}", and the ${ENV_VAR} is
expanded from the process environment at the moment the request is sent — never
before. The secret’s value never lives on a client object, in a config, in a
repr, or in a log. Only the ${NAME} placeholder is ever held.
First-class auth on any model client
Section titled “First-class auth on any model client”${ENV} header auth is the same mechanism in all three ports: the template is
held on the client, and the HTTP layer expands it at the request boundary — Python
HttpClient, Go models.HTTPClient.ResolveHeaders, JavaScript
HttpClient.resolveHeaders. Same embedding endpoint, one tab per language:
from citenexus import OpenAICompatibleEmbedding
embedder = OpenAICompatibleEmbedding( base_url="https://api.jina.ai/v1", model="jina-embeddings-v3", headers={ "Authorization": "Bearer ${JINA_API_KEY}", # expands at call time "X-Tenant": "legal", # arbitrary provider header },)vectors = embedder.embed_many(["hello"])import "github.com/muthuishere/citenexus/golang/models"
// A real net/http transport; timeout 0 means the 60s default.http := models.NewHTTPClient(nil, 0)
embedder := models.NewOpenAIEmbedding( "https://api.jina.ai/v1", "jina-embeddings-v3", http.Transport(), models.WithHeaders(map[string]string{ "Authorization": "Bearer ${JINA_API_KEY}", // expands at call time "X-Tenant": "legal", }),)vectors, err := embedder.Embed([]string{"hello"})import { HttpClient, OpenAIEmbedder } from "@muthuishere/citenexus";
const http = new HttpClient();
const embedder = new OpenAIEmbedder( { base_url: "https://api.jina.ai/v1", model: "jina-embeddings-v3", headers: { Authorization: "Bearer ${JINA_API_KEY}", // expands at call time "X-Tenant": "legal", }, }, (url, body, headers) => http.send(url, body, headers),);const vectors = await embedder.embed(["hello"]);The same option carries a bare token under whatever header name a provider wants
— {"x-api-key": "${ACME_TOKEN}"} — so a non-Bearer scheme needs no special
support.
Chat generation is the same shape in all three:
from citenexus import OpenAICompatibleGenerator
generator = OpenAICompatibleGenerator( base_url="https://api.openai.com/v1", model="gpt-4o-mini", headers={"Authorization": "Bearer ${OPENAI_API_KEY}"},)generator := models.NewOpenAIChatGenerator( "https://api.openai.com/v1", "gpt-4o-mini", 0.0, nil, // temperature, max_tokens (nil → omitted from the request) http.Transport(), models.WithHeaders(map[string]string{"Authorization": "Bearer ${OPENAI_API_KEY}"}),)import { HttpClient, OpenAIChatGenerator } from "@muthuishere/citenexus";
const generator = new OpenAIChatGenerator( { base_url: "https://api.openai.com/v1", model: "gpt-4o-mini", headers: { Authorization: "Bearer ${OPENAI_API_KEY}" }, }, (url, body, headers) => new HttpClient().send(url, body, headers),);# Python only — vision and reranking clients take the same headers= templates.from citenexus import OpenAICompatibleVision
vision = OpenAICompatibleVision( base_url="https://api.internal.acme/v1", model="acme-vlm", headers={ "x-api-key": "${ACME_TOKEN}", # bare token, custom auth header "X-Tenant": "legal", # arbitrary provider header },)Typed provider endpoints — Python only
Section titled “Typed provider endpoints — Python only”Everything from here down belongs to the Python facade: HttpEndpoint, the typed
subclasses, and from_config are the config layer, and the ports have no
config layer to wire them into. In Go and JavaScript you construct the client
directly, exactly as the tabs above show.
For config-driven wiring, the typed HttpEndpoint subclasses carry the same
${ENV} headers (and reusable defaults like the right base URL and auth style):
from citenexus import ( OpenAIHttpEndpoint, GeminiHttpEndpoint, AnthropicHttpEndpoint, OpenRouterHttpEndpoint, OllamaHttpEndpoint,)
openai = OpenAIHttpEndpoint(headers={"Authorization": "Bearer ${OPENAI_API_KEY}"})gemini = GeminiHttpEndpoint(headers={"Authorization": "Bearer ${GEMINI_API_KEY}"})claude = AnthropicHttpEndpoint(headers={"x-api-key": "${ANTHROPIC_API_KEY}"}) # no Bearerlocal = OllamaHttpEndpoint(base_url="http://localhost:11434/v1") # no keyA private gateway is just the general HttpEndpoint — URL, ${ENV} token in a
header of your choosing, and arbitrary per-request headers:
from citenexus import HttpEndpoint
endpoint = HttpEndpoint( base_url="https://llm.internal.acme/v1", headers={ "Authorization": "Bearer ${ACME_TOKEN}", "X-Tenant": "legal", "X-Env": "prod", }, timeout_s=60,)Wiring endpoints into the client
Section titled “Wiring endpoints into the client”Endpoints are applied through from_config — CiteNexus builds each model client
with a transport that expands the ${ENV} headers at call time:
from citenexus import CiteNexus, GeminiHttpEndpoint, OpenAIHttpEndpointfrom citenexus.config.schema import ( CiteNexusConfig, EmbeddingConfig, LLMConfig, RerankerConfig, StorageConfig,)from citenexus.config.signals import Signal
jina = OpenAIHttpEndpoint( base_url="https://api.jina.ai/v1", headers={"Authorization": "Bearer ${JINA_API_KEY}"},)config = CiteNexusConfig( storage=StorageConfig(bucket="./citenexus-data"), embedding=EmbeddingConfig(endpoint=jina, model="jina-embeddings-v3"), reranker=RerankerConfig(endpoint=jina, model="jina-reranker-v2-base-multilingual"), llm=LLMConfig( endpoint=GeminiHttpEndpoint(headers={"Authorization": "Bearer ${GEMINI_API_KEY}"}), model="gemini-2.5-flash", ), signals=(Signal.embedding, Signal.text),)
rag = CiteNexus.from_config(config) # each client resolves ${ENV} headers at the edgeDriving the HTTP layer yourself
Section titled “Driving the HTTP layer yourself”If you would rather not use a shipped client, the HTTP layer is public on its own
— the ${ENV} expansion lives there, not in the clients:
from citenexus.http import HttpClient
client = HttpClient() # the client *is* the transport: __call__raw = client( "https://api.jina.ai/v1/embeddings", b'{"model": "jina-embeddings-v3", "input": ["hello"]}', {"Authorization": "Bearer ${JINA_API_KEY}"}, # expanded inside the call)client := models.NewHTTPClient(nil, 0)raw, err := client.Do( "https://api.jina.ai/v1/embeddings", []byte(`{"model":"jina-embeddings-v3","input":["hello"]}`), map[string]string{"Authorization": "Bearer ${JINA_API_KEY}"},)const http = new HttpClient();const raw = await http.send( "https://api.jina.ai/v1/embeddings", JSON.stringify({ model: "jina-embeddings-v3", input: ["hello"] }), { Authorization: "Bearer ${JINA_API_KEY}" },);In every port the merge order is the same — User-Agent < client defaults <
per-call headers, auth wins — and the templates stay unexpanded until the one
call that sends them (resolve_headers / ResolveHeaders / resolveHeaders).
Transport is (url, body, headers) => string | Promise<string>, so an async
transport needs no cast — a consumer awaits either. HttpClient.send is async
(Node fetch); resolveHeaders is the synchronous primitive underneath it, so a
synchronous transport (the hermetic test fakes) stays valid.
Writing a model that is not an HTTP endpoint at all — in-process, a local daemon, an adapter for a third library, a fixture? Keep these same clients and replace one callable: Bring your own model — swap the transport.