@aihubmix/codegen
v0.4.0
Published
Pure, isomorphic request/code generator for the AIHubMix gateway: 4 protocols x 7 languages. One wire body feeds both the generated snippet and the real request. No transport, no DOM — capabilities and base URL are injected.
Readme
@aihubmix/codegen
Pure, isomorphic request/code generator for the AIHubMix gateway — 4 protocols × 7 languages.
The same buildBody() feeds both the generated snippet and the real request, so "what the panel shows" == "what gets sent" == "what the code example prints". No transport, no DOM, no network, no env reads.
pnpm add @aihubmix/codegenZero runtime dependencies. Ships ESM + CJS + .d.ts + .d.cts.
Quick start
import { generateCode } from '@aihubmix/codegen';
const code = generateCode('messages', 'python', {
baseUrl: 'https://aihubmix.com', // required — see below
model: { id: 'claude-opus-5' },
sys: 'You are a helpful assistant.',
user: 'Hello, how are you?',
// `max_tokens` / `temperature` / `top_p` are required fields of `p`; `paramKeys`
// decides which of them actually reach the wire.
p: { max_tokens: 1024, temperature: 0.7, top_p: 0.9 },
paramKeys: ['max_tokens'],
stream: false,
});Support matrix
| CodeLang | label | chat | messages | responses | gemini |
|------------|-------|:------:|:----------:|:-----------:|:--------:|
| python | Python | SDK | SDK | SDK | SDK |
| javascript | TypeScript | SDK | SDK | SDK | SDK |
| ruby | Ruby | SDK | SDK | SDK | SDK |
| go | Go | SDK | REST | REST | REST |
| java | Java | REST | REST | REST | REST |
| csharp | C# | REST | REST | REST | REST |
| curl | cURL | REST | REST | REST | REST |
Note the id is javascript even though the emitted code (and the display label) is TypeScript.
SDK = renders the vendor SDK (openai, anthropic, google-genai). REST = renders a raw HTTP call against the gateway. Both go through the same buildBody().
Protocol → route and auth header are the gateway contract, and live in exactly one place (src/config/protocols.ts):
| protocol | route | auth header |
|---|---|---|
| chat | /v1/chat/completions | Authorization: Bearer |
| responses | /v1/responses | Authorization: Bearer |
| messages | /v1/messages | x-api-key (+ anthropic-version) |
| gemini | /gemini/v1beta/models/{model}:generateContent | x-goog-api-key |
Four more surfaces sit beside the table above rather than inside it. They are different kinds of call, not a fifth way to write the same one, so each has its own entry point and its own renderer table — CodeProto stays a closed set of four:
| surface | entry point | shape |
|---|---|---|
| media | generateMediaCode(opts) | image: one sync POST; video: submit + poll + download |
| realtime | generateRealtimeCode(opts) | WebSocket, transcription or conversation |
| decision | generateDecisionCode(opts) | one POST /v1/systemone: a state plus typed questions, a typed answers map back — no text to parse |
| OpenAI decisions | generateOpenAIDecisionCode(opts) | one POST /v1/decisions: an input (text, or user messages with images) plus an ordered questions array; an answers array back in question order — predicate / choice / score, or refusal |
The two decision surfaces are separate endpoints with separate body shapes (state + a keyed questions{} map vs. input + an ordered questions[] array, noul vs. predicate), so each has its own body builder — buildDecisionBody() and buildOpenAIDecisionBody() — and its own renderer table. A consumer that sends the real request must use the same builder as the snippet.
For OpenAI decisions, the python and javascript cells render the official openai SDK's client.decisions.create, which only exists from openai-python 3.26.0 / openai-node 7.30.0 (both released 2026-10-06). The first line of those snippets names the minimum version, so it travels with the copied code. The other five languages make a raw HTTP call.
baseUrl is required, and there is no setter
CodeGenCtx.baseUrl is a required field. This is deliberate:
- Consumers are dual-domain builds (
aihubmix.comandapi.inferera.com). A hard-coded base would make one domain emit code pointing at the other. - Making it required means every call site fails to compile until it passes one explicitly.
- There is no
setBaseUrl()or any module-level mutable state. Consumers plan to fork on theHostheader at request time inside one process; module-level state would leak across concurrent requests.
paramKeys is the only gate
buildBody() has three generic fall-through channels that claim new keys by type:
| value type | channel | behaviour |
|---|---|---|
| number | emitExtraNumbers | skips keys already handled specially |
| enum / string | emitEnums | sends only when non-empty and ≠ schema default |
| object / array / boolean | emitObjects | skips capability-gated keys and empty containers |
All three are gated by inSchema(), i.e. ctx.paramKeys. A key that the model's schema does not declare is never sent, even if a stale value for it is still sitting in ctx.
Consequence: adding a parameter needs no change to this package. Declare it in the schema, put a value in ctx, and all 28 cells pick it up.
Omitting paramKeys disables gating entirely (backwards compatibility). Prefer passing it.
Two lists still need manual upkeep, both covered by tests:
PY_OPENAI_NATIVE— whether a key renders as a native kwarg or lands inextra_body. Fail-safe either way: the snippet still runs.GO_CHAT_KEYS/GO_OBJ_FIELDS—go-openai'sChatCompletionRequestis a closed struct: no map fallback, noExtraBody(checked against v1.41.2). So thego+chatcell is the one place where a body key can fail to reach the wire; the other six languages render whateverbuildBody()produced.Every key that does have a struct field is mapped. Anything left over is named in a comment in the emitted code rather than silently dropped, so the snippet tells you what it isn't sending and points at
net/httpas the way out.tests/extensibility.test.tsasserts both halves — the leftover key appears in a comment and does not appear in the request struct — andscripts/test-codegen-escaping.mjsruns a realgo buildover the result. When you add a parameter, check whethergo-openaihas a field for it: if yes, map it and add the key here; if no, do nothing and the comment picks it up.GO_REASONING_PREFIXES—go-openaiships a client-side validator (reasoning_validator.go) that rejects a set of parameters for models whose id starts witho1/o3/o4/gpt-5. It fires before the request is sent, so a snippet that trips it prints a panic and never reaches the gateway. This is an SDK rule, not a gateway one — the identical body sent withcurlreturns 200.Consequently the
go+chatcell rendersMaxCompletionTokensinstead ofMaxTokensfor those models, and omitstemperature,top_p,n, the two penalties, andlogprobs— again naming them in a comment, worded to distinguish "this SDK won't send it" from "no struct field exists". The trigger is the model id, notparamKeys: most models today carry no schema at all, and keying off one would put everygpt-5request on the panicking path. Mirror upstream when it changes;tests/go-reasoning-validator.test.tscovers both branches and the escaping harnessgo builds each.
One wire-level correction
buildBody() passes values through; it does not second-guess them. The single exception is the messages protocol, where Anthropic requires max_tokens to be strictly greater than thinking.budget_tokens — violating it is a hard 400, not a degradation. When extended thinking is on and max_tokens is not above the budget, buildBody() raises it to budget_tokens + 1024.
It lives here rather than in a caller because the correction has to apply to the real request, not only to the printed snippet. A parameter panel that lets the user set an 8192 budget against a 1024 limit would otherwise show working code next to a request that 400s. tests/thinking-budget.test.ts calls buildBody() directly, without going through any capability layer, for exactly that reason.
Node / CommonJS
The package must be require()-able from bare Node — inferera-web's prerender step is a consumer:
const { generateCode } = require('@aihubmix/codegen');TypeScript projects on moduleResolution: node16 | nodenext resolve ./dist/index.d.cts for require and ./dist/index.d.ts for import.
Scope: wire vocabulary only
This package speaks two things and nothing else: what the gateway's HTTP body looks like (four protocols — a closed set that only moves when an upstream API changes), and how to write that body as code in seven languages.
It deliberately does not speak knowledge-base vocabulary — capability keys (reasoning-effort, vision), verdicts (tested-effective, silent-degrade), or the knowledge base's own protocol identifiers. That vocabulary is an open set that grows on its own schedule; if it lived here, adding one capability upstream would mean a release of this package and an upgrade in every consumer. It lives in @aihubmix/model-schema, which depends on this package and translates knowledge-base records into the wire-only CodeGenCtx below. The dependency is one-way: nothing here imports that package.
tests/vocabulary-isolation.test.ts enforces this by scanning src/ for those literals, so it is a build-time fact rather than a convention.
The package also ships no UI: no chips, no strikethrough styling, no syntax highlighting.
Invariants
Enforced by tests in tests/, not by convention:
- Isomorphic — no DOM,
window, network, or env reads anywhere insrc/. - Single wire source — every renderer's body comes from
buildBody(); no bypass path. - Facts live once — auth headers,
anthropic-version, the protocol, realtime and decision routes, the API-key placeholder, SDK package names and response accessors appear only insrc/config/**.tests/fact-localization.test.tsfails if one leaks intosrc/renderers/**orscripts/**. CAP_GATED_WIRE_KEYSis derived, never hand-written twice.- Wire vocabulary only — no capability key, verdict, or knowledge-base protocol id appears in
src/.tests/vocabulary-isolation.test.tsfails if one is added back. - SDK records are self-consistent — an
Anthropicclient's response accessor may not containchoices[, and an OpenAI client's may not containcontent[0].text.
Development
pnpm build # tsup → ESM + CJS + d.ts + d.cts
pnpm test # vitest
pnpm typecheck # tsc --noEmit
pnpm test:all # typecheck + vitest + classifier + escaping
pnpm smoke # build, then require() from bare Node
pnpm verify:codegen # live verification — really runs the generated snippetsverify:codegen is the real acceptance criterion: it executes the generated code against a gateway and classifies the result. Pass the base URL in; never hard-code a domain or a key.
License
MIT
