@aihubmix/codegen
v0.1.1
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 |
Media generation (image / video × 7 languages) is available through generateMediaCode(opts).
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 four 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
