@axium-lab/helix
v0.3.1
Published
Dialect translation across LLM providers (OpenAI, Anthropic, Gemini) plus a provider-agnostic SDK
Readme
Helix
Translation between LLM wire formats, plus a provider-agnostic SDK.
A single npm package, with no server and no process of its own. It gets imported. The parent project, Axium, is what exposes HTTP.
Branch
refactor. Complete rewrite of v0.1, which is frozen onmainas a reference and is neither compiled nor tested. It used to be vendored here underold_code/; consult it withgit show main:<path>instead.
What works, and what does not
The honest version. Nothing below is aspirational — every ✅ is exercised by the test suite, and most of it against a live server.
Dialects
A dialect is a wire format Helix can both read and write.
| Dialect | Status | Surfaces |
|---|---|---|
| openai | ✅ complete | /v1/chat/completions and /v1/responses |
| gemini | ✅ complete | generateContent / streamGenerateContent, and the Interactions API |
| anthropic | ❌ not implemented | — |
Gemini's Interactions API — the surface Google recommends for new work — is opt-in, so existing callers keep the behaviour they have:
new Helix({
transport: new GeminiTransport({ apiKey }),
encodeOptions: { surface: 'gemini.interactions' },
});It adds stored responses (helix.responses.get / .cancel / .delete,
store, background, previous_response_id) and reasoning.effort maps
directly onto thinking_level. Two things to know before switching:
- It exposes no sampling controls.
temperature,top_p,top_k,nand the penalties do not exist on this surface; each one warns when dropped. - Multi-turn tool use needs the whole assistant turn replayed, reasoning
included — the signature it carries is validated server-side. In exchange, the
round trip works, which it does not on
generateContent(seepor-arreglar.md).
Anthropic is not started. dialects.anthropic does not exist and will not
compile — the registry is typed as a partial record precisely so that a missing
dialect is a compile error rather than an undefined at runtime.
Endpoints
An endpoint is where a request goes. Several share one dialect, which is the point of separating the two.
| Transport | Dialect | Generation | Streaming | Models | Files |
|---|---|---|---|---|---|
| OpenAITransport | openai | ✅ | ✅ | ✅ | ✅ |
| AzureTransport | openai | ✅ | ✅ | ✅ | ✅ |
| OpenAICompatibleTransport | openai | ✅ | ✅ | ⚠️ | ⚠️ |
| GeminiTransport | gemini | ✅ | ✅ | ✅ | ✅ |
| GeminiEnterpriseTransport | gemini | ✅ | ✅ | ✅ | ⚠️ via Cloud Storage |
| Anthropic | anthropic | ❌ not implemented | | | |
| Bedrock | anthropic | ❌ not planned | | | |
⚠️ OpenAI-compatible vendors (Groq, Ollama, OpenRouter, vLLM…) implement the
generation surface reliably and the rest inconsistently. Declare what yours
supports and helix.capabilities will report it truthfully:
new OpenAICompatibleTransport({
baseUrl: 'https://api.groq.com/openai/v1',
apiKey,
capabilities: { files: false, models: false },
});⚠️ Gemini Enterprise — the product Google used to call Vertex AI — has
no Files API. GeminiEnterpriseTransport backs files.* with Google Cloud
Storage when you give it a bucket, and reports capabilities.files: false when
you do not. Ids are gs://bucket/object, which the gemini dialect already
accepts as a file_id — so you can also skip uploads entirely and reference an
object you put there yourself.
Gemini Enterprise authenticates with OAuth rather than a key, in four ways:
const where = { projectId, location: 'us-central1' };
// A service account key. Helix signs the JWT itself — no extra dependency.
new GeminiEnterpriseTransport({ ...where, credentials });
// A token you already have.
new GeminiEnterpriseTransport({ ...where, accessToken });
// Delegation, for Workload Identity Federation or impersonation.
new GeminiEnterpriseTransport({ ...where, getAccessToken });
// Nothing: the GCE/Cloud Run metadata server. How this runs on Google's compute.
new GeminiEnterpriseTransport(where);Files need credentials or ADC specifically: the Storage client mints its own
token and takes a key, so an accessToken authenticates the model endpoint but
not Storage.
Operations
| Operation | Status |
|---|---|
| responses.create | ✅ |
| responses.stream | ✅ decoding and encoding, in both directions |
| models.list / models.get | ✅ |
| files.create / get / list / delete | ✅ |
| test.connection | ✅ |
| embeddings, batches, images, audio, fine-tuning | ❌ out of scope for v1 |
Known limitations
- File uploads are held in memory.
filetakes aUint8Array; there is no streaming upload. Gemini's own limit for a single-chunk upload is around 20 MB. - Model names are not translated. Sending an OpenAI request to Gemini produces
a Gemini-shaped call for
gpt-4o-mini. Choosing an equivalent model is a policy decision and belongs to the caller. HelixResponsehas nopassthrough. Unlike requests, provider extras on a response (system_fingerprint,logprobs) survive only insidemetadata.raw.
Install
Published publicly on npm — no registry configuration and no token needed:
npm install @axium-lab/helixNode 22 or newer; ESM and CJS builds both ship.
What it is for
Two use cases from one engine:
- Dialect translation. Convert a request in one provider's format into
another's, and the response back. Helix exposes the functions; Axium mounts
them on HTTP routes so a client only changes its
baseURL. - Provider-agnostic SDK. A facade that also normalises
modelsandfiles, which LangChain does not cover.
import { Helix, OpenAITransport } from '@axium-lab/helix';
const helix = new Helix({ transport: new OpenAITransport({ apiKey }) });
const res = await helix.responses.create({
model: 'gpt-4o-mini',
instructions: 'Be concise.',
input: [{ role: 'user', content: [{ type: 'input_text', text: 'Hello.' }] }],
});Swapping the transport is the only edit needed to talk to a different provider.
Full surface in API.md; the design explained in
CONCEPTS.md. Both are also published as a browsable site under
docs/ — open docs/index.html.
The design principle
Both use cases are the same engine walked in opposite directions. Every dialect is a module with four functions:
| function | translation (used by Axium) | SDK |
|---|---|---|
| decodeRequest wire→IR | ✅ | — |
| encodeRequest IR→wire | — | ✅ |
| decodeResponse wire→IR | — | ✅ |
| encodeResponse IR→wire | ✅ | — |
With a central IR this is N+M transformers instead of N×M. And because HTTP lives in Axium, all four are public API: breaking that contract is a breaking change.
The axes are kept apart deliberately:
- A dialect knows wire formats and never touches the network.
- A transport knows URLs and credentials and never reads a payload.
Which is why azure is not a provider but an endpoint speaking the openai
dialect — and why adding it required no translation code at all.
Two documented exceptions exist, both in transports that must read a body: Azure
needs the deployment name from body.model for its URL, and Gemini's file upload
is a two-step resumable exchange. A third would mean the contract should grow
multi-step operations rather than collect exceptions.
No required runtime dependencies
package.json declares no dependencies. The provider SDKs are devDependencies
used through import type only, to type foreign wire formats without shipping
them — enforced by verbatimModuleSyntax and verified at build time by
scripts/check-dist.mjs, which fails the build if any of them reaches the emitted
.d.ts.
That check matters: a leaked type would turn a devDependency into a peer dependency for every consumer, and TypeScript gives no warning about it.
One optionalDependency: @google-cloud/storage, which backs files.* on
Gemini Enterprise. It is reached through a dynamic import() that only runs when
you configure a bucket, and tsup marks it external so it is never inlined —
leaving it out of the bundle entirely rather than shipping 1.9 MB to everyone.
Install it if you want files there; otherwise nothing changes for you. Everything
else, OAuth signing included, is plain fetch and crypto.subtle.
Development
npm install
npm run typecheck
npm test # everything
npm run build # tsup + the dist guardTests
| Tier | Against | Network | What it validates |
|---|---|---|---|
| test:unit | fixtures on disk | no | dialects, the IR, pure logic |
| test:mock | llm-mock | yes | transports, assembly, real wire shapes |
| test:integration | real providers | yes | self-skips without credentials |
| test:manual | real providers | yes | a scratchpad, not a check — excluded from npm test and from CI |
The manual tier is a mode, not a directory in the run: HELIX_MANUAL=1 makes
the run contain tests/manual/ and nothing else, and without it those files are
not collected at all — so no CI can reach a scratchpad that spends money. A
positional argument picks one file:
npm run test:manual # the whole tier
npm run test:manual -- structured-output # one fileThe unit tier is guarded: a unit test that reaches the network fails. Three did so by accident during development, and each passed for the wrong reason — an auth failure throws just as convincingly as a missing implementation.
llm-mock needs no credentials and costs nothing; it is configured in
.env.test, committed on purpose. Real provider keys go in
.env.test.local, which is git-ignored.
Why v0.1 is kept
v0.1 is frozen on main with 8 ADRs, as a map of provider quirks paid for against
real APIs. Several were carried into this rewrite and re-confirmed against a live
server:
- Azure lists deployments with
api-version=2023-03-15-previewhardcoded: newer versions return 404 on that endpoint even when valid for inference (ADR-0004). - Gemini Files enforces a fixed 48 h retention;
purposeandexpires_afterare accepted and ignored. - Gemini Enterprise has no Files API at all, and is fronted by Cloud Storage instead.
- Gemini Enterprise lists publisher models globally and only on
v1beta1— the project-qualified path 404s, and so doesv1. The same shape as Azure's pinned api-version above, re-confirmed against a live project on 2026-08-15. - Eleven error categories with
httpStatusfallbacks (502 connection, 504 timeout, 500 otherwise) so an error can be forwarded straight to a client (ADR-0006).
Consult it without leaving the branch:
git show main:src/internal/providers/azure/azure.models.ts