@pieai/swimmer-ai-kit
v0.11.0
Published
Small, server-only TypeScript primitives shared by Pie AI products.
Readme
SwimmerAIKit
Small, server-only TypeScript primitives shared by Pie AI products.
This package is the product-line AI socket. Product code must not call OpenRouter or model-vendor SDKs directly. Last reviewed: 2026-08-13.
Scope
Owns:
- Server environment readers and fallback resolution
- Product-owned model alias registries
- OpenRouter model configuration and attribution headers
- Provider-neutral chat-completion contract (
./chat) - OpenRouter chat completion transport (
./openrouter) - Gemini-direct chat completion transport (
./gemini) - Privacy-safe, provider-neutral AI execution evidence validation
- Local provider budget reservation/commit/release guards
- Typed generator registries with optional provider-budget wrapping
- Structured-output parse/generate on top of
./chat(./structured-output) - Named agent registry (
./agents): lookup plus chat / structured invoke - Content safety (
./content-safety): local text rules + Sightengine visual and adult-reference adapters - Dual TTS routing (
./tts): language → western (ElevenLabs) / chinese (MiniMax) - Tripo 3D text-to-model adapter (
./tripo)
Does not own prompts, product model policy, product schemas, game logic, wallet RPCs, database access, deployment topology, or a Mastra runtime. Each product deploys its own optional Mastra service and supplies its own model aliases, budgets, generators, standing instructions, and Zod (or other) schemas.
Public API contract
Import a named subpath. The root entry @pieai/swimmer-ai-kit is a
compatibility barrel for existing callers (SupaLuv). New code should not add
root imports. Runtime values on the root barrel are locked by
tests/public-exports.test.ts.
| Subpath | Stability | Source | Use it for | Do not use it for |
| --- | --- | --- | --- | --- |
| ./env | stable | src/env.ts | Reading server env and key fallbacks | Client bundles, logging secret values |
| ./models | stable | src/models.ts | Product alias registries; Mastra OpenRouter model config | Sending HTTP; owning product model policy |
| ./chat | evolving | src/chat.ts | Typing a ChatCompletionTransport. ChatMessage.content is a string or text / image parts. | Calling a provider; parsing structured output |
| ./structured-output | evolving | src/structured-output.ts | Declaring a { parse } schema and getting a validated object from chat text | Owning product schemas; wrapping Mastra; unifying transport errors |
| ./agents | evolving | src/agents.ts | Looking up named agents and invoking them through ./chat / ./structured-output | Constructing Mastra/Agent; owning prompts or model policy |
| ./openrouter | stable | src/openrouter.ts | Text and vision chat completions through OpenRouter. Optional signal cancels or times out a request. | Gemini-direct, product prompts |
| ./openrouter-authorization | evolving | src/openrouter-authorization.ts | Official HTTPS S256 authorization URL and bounded, single code exchange | User/tenant state, encrypted custody, billing or supplier licensing |
| ./pi-oauth | evolving | src/pi-oauth.ts | Pi-owned device login, refresh and bounded text for Copilot, xAI, Kimi Code and Radius | CLI credential discovery, brand login, arbitrary gateways, automatic account rotation |
| ./gemini | evolving | src/gemini.ts | Gemini-direct ChatCompletionTransport for the same request shape | OpenRouter ids (google/…), product prompts, Mastra config |
| ./execution-evidence | stable | src/execution-evidence.ts | Parsing privacy-safe usage/cost evidence | Pricing, billing decisions, raw provider payloads |
| ./provider-budget | stable | src/provider-budget.ts | In-process cents reservation | Cross-process ledgers or wallet truth |
| ./generator-registry | stable | src/generator-registry.ts | Wrapping named generators with optional budget | Owning generator implementations |
| ./content-safety | stable | src/content-safety/ | Text/visual/adult-reference review; Sightengine signal normalize | Product visual thresholds, copy, or UI policy |
| ./tts | stable | src/tts/ | Dual-route speech synthesis | Client-side key handling |
| ./tripo | stable | src/tripo.ts | Server-only Tripo text-to-model | Browser runtime; live image_to_model |
Expected consumers: server code in Pie products (Show, Collapse, SupaLuv, Sea, Anvil, TuringPact, Break, Non-Heroes). Browser bundles, edge markup, and product UI must not import this package.
Additional Pi OAuth accounts
./pi-oauth reuses the pinned Pi provider implementations, not copies of their
OAuth protocols. piOAuthCapabilities() is an offline inventory of the six
requested families: the existing Codex and OpenRouter adapters plus GitHub
Copilot, xAI, Kimi Code and Radius. Anthropic account login is excluded. Inventory
presence is neither a real login nor permission to sell hosted subscription use.
Radius billing is gateway-defined (subscription: null), not inferred as free.
import { authorizePiDevice, createPiOAuthChatTransport } from '@pieai/swimmer-ai-kit/pi-oauth';
// Run in a durable, admitted, owner-bound server operation. The host provides
// the deadline, encrypted challenge/credential storage, consent and cancellation.
const account = await authorizePiDevice({
provider: 'xai',
signal: operationSignal,
onDeviceCode: challenge => retainChallengeForThisOwner(challenge),
});
await retainCredentialForThisConnection(account);
const transport = await createPiOAuthChatTransport({
account, model: selectedModelId, signal: taskSignal,
onAttempt: request => retainPrivateTaskRequest(request),
});
// transport.complete() accepts one system message and one user message.
// The existing product task/runtime owns parsing, candidate review and adoption.Each caller supplies a credential explicitly; no global account singleton,
home-directory file, environment key fallback or framework login store is used.
The host holds a cross-process claim before refreshPiOAuthCredential, saves the
result before inference, and treats a lost outcome as unresolved. Kimi's maintained
SDK may itself retry refresh; this adapter adds no retry. Copilot's maintained login
can enable unconfigured model policies, so allowCopilotModelPolicyUpdates: true
requires an informed product consent step. Enterprise/custom gateways are not
silently admitted as arbitrary token destinations.
piOAuthTextModels() returns account-filtered model IDs; Radius reads its official
gateway catalog. Catalogs remain connection-local. Model requests use Pi's maintained
stream, with a fixed provider destination, caller deadline, one outbound attempt,
no tools, bounded request/response bytes, and exact body evidence retained before
dispatch. Errors do not expose provider response text or tokens. A displayed device
code/URL is also private short-lived authorization material, not diagnostic metadata.
Default tests use synthetic HTTP and standalone Node bundles, not real accounts or
paid model calls. Directing UI, encrypted custody and live acceptance remain product
integration work. See Pi providers and the
installed SDK source for provider-specific behavior.
./openrouter-authorization is a provider-protocol adapter, not an Auth service.
The host must pin its HTTPS callback to a user-bound, expiring, single-use attempt,
persist exchanging before dispatch, and encrypt the returned key outside work/task records.
An unknown exchange must not be retried: the provider may already have issued a key.
The key is permanent/user-controlled, not a promise of refresh, subscription media
allowances, or free model usage. Disconnect and provider-side revocation are separate actions.
The adapter never reads environment credentials or starts a localhost server.
Reuse review (2026-09-14): this follows the official PKCE API contract with standard Node crypto and Fetch. Pi 0.85.1's existing OpenRouter flow is CLI-only and owns a loopback listener/manual-code race, so it cannot be directly hosted as the public callback. No Pi or third-party implementation source was copied. The host must separately satisfy the OpenRouter terms, model terms and regional restrictions; the protocol helper is not commercial permission or real-account evidence.
Internal assembly that happens to be exported (prefer the façade):
./content-safetyvisual-asset helpers (assetLooksImage,visualAssetBlob, …)./content-safetyFetchLike/ individual Sightengine factories./content-safetyparseSightengineResponse/maxSightengineScore/sightenginePathMatches(not on the root barrel)./content-safetycreateSightengineVisualTransport/readSightengineCredentials(not on the root barrel)./ttsindividual ElevenLabs / MiniMax factories whencreateDualTtsFromEnvis enough./tripoparse/map helpers whencreateTripoAdapteris enough
createOpenRouterChatTransport lives on ./openrouter. createGeminiChatTransport
lives on ./gemini. Both implement ChatCompletionTransport. Neither is
re-exported from the root barrel. parseStructuredOutput and
createStructuredOutputClient live on ./structured-output only.
createAgentRegistry lives on ./agents only.
Frozen seams
Re-measured 2026-08-13 (round 3). These look movable and are not. Do not re-open them in a later structural pass unless a real second responsibility or a dedicated capability plan appears.
| Seam | Leave it | Why |
| --- | --- | --- |
| src/tripo.ts as one file | Yes. ~1243 lines is a review trigger, not a split rule. | One provider pack (credentials, credit budget, transport, adapter, parse). Splitting would make the next Tripo change touch more files for the same job. |
| ./models OpenRouter helpers | Yes. Keep createOpenRouterModel / createOpenRouterHeaders on ./models. | Collapse, TuringPact, and Show import ./models. Moving the path is a contract change. |
| FetchLike / OpenRouterFetch / GeminiFetch / TripoFetch | Yes. Four test seams, not one shared fetch type. | Unifying them changes published types. |
| OpenRouter's six redacted error strings | Yes. Do not rewrite them to match Gemini or a shared class. | Sea parses failed with (\d+). Collapse, Anvil, and SupaLuv catch the façade. A typed helper is a new capability and needs its own plan. |
| Shared HTTP/JSON helpers extracted from OpenRouter + Gemini | No extract. | The overlap is isUnknownRecord / isAbortLike / maxTokens checks — not a missing layer. A utils file would be a grab bag. |
| requestGeminiChatCompletion façade | Do not add. | The abstraction is already complete(). |
| Per-directory Boundary README / knip / madge as gates | Do not add. | 25 files; rg is enough. Extra docs become parallel truth. |
| Root barrel growth | Do not add ./chat, ./gemini, ./structured-output, ./agents, or new ./content-safety runtime values to .. | The barrel is locked at 63 runtime keys for existing root callers (SupaLuv). |
| Default Sightengine visual decide | Yes. Keep 0.72 / 0.65 / 0.75, exclude nudity.context, and do not map OCR/PII. | Tightening it would silently change SupaLuv visual review. Product policy reads parseSightengineResponse. |
| Whitespace-only Sightengine keys | Yes. trim() then missing → undefined. | Empty credentials are not credentials. Creating a provider that will 401 is worse than falling back to local visual-unavailable. |
ChatCompletionRequest.signal is optional, matching TtsSynthesizeRequest.
The kit does not pick a default timeout; products that need one pass
AbortSignal.timeout(ms). Without a signal the request JSON and fetch init
are unchanged.
ChatMessage.content is either a string (the original call shape) or an
array of { type: 'text', text } and { type: 'image', url } parts.
url is a data: URL or an https: URL. OpenRouter serializes image
parts to the OpenAI image_url wire shape. String-only callers do not
change.
Show camera-vision migration
Replace server/openRouterHttp.ts / summarizeImageSeed with
createOpenRouterChatTransport. Do not pass OpenRouter's nested
image_url object — that is the wire format, not the kit contract.
import { createOpenRouterChatTransport } from '@pieai/swimmer-ai-kit/openrouter';
const result = await createOpenRouterChatTransport({
apiKey,
appName: 'Show camera seed',
appUrl: 'https://show.pieaistudio.com',
fetchImpl,
}).complete({
maxTokens: 90,
messages: [
{
role: 'user',
content: [
{ type: 'text', text: visionPrompt },
{ type: 'image', url: seed.dataUrl },
],
},
],
model: 'google/gemini-2.5-flash',
signal: AbortSignal.timeout(15_000),
temperature: 0.2,
});
const summary = result.content;The same part array works for Show's OpenRouter visual-moderation path.
Kit throws on HTTP / provider failure; Show currently inspects response.ok
itself, so the catch / fail-closed mapping stays in Show. Do not change
Show in this package.
When SHOW_MODEL_PROVIDER !== openrouter, use createGeminiChatTransport
instead of stuffing Gemini into ./openrouter. Pass a Gemini model id
(gemini-2.5-flash), not an OpenRouter id (google/gemini-2.5-flash).
Gemini image parts must be data: URLs — the transport does not fetch
https: images.
Non-Heroes character-AI migration
Replace new Mastra({ agents: { …: new Agent(…) } }). Keep product schemas,
standing instructions, and the Colyseus-safe timeout wrapper in Non-Heroes.
import { createAgentRegistry } from '@pieai/swimmer-ai-kit/agents';
import { createGeminiChatTransport } from '@pieai/swimmer-ai-kit/gemini';
import { createOpenRouterChatTransport } from '@pieai/swimmer-ai-kit/openrouter';
const transport =
modelProvider === 'gemini'
? createGeminiChatTransport({ apiKey: geminiApiKey })
: createOpenRouterChatTransport({
apiKey: openRouterApiKey,
appName: 'Non-Heroes character AI',
appUrl: 'https://non-heroes.pieaistudio.com',
});
const agents = createAgentRegistry({
agents: {
'non-heroes-boss-ai': {
instructions: productOwnedBossInstructions,
model:
modelProvider === 'gemini' ? 'gemini-2.5-flash' : 'google/gemini-2.5-flash',
name: 'Non-Heroes Boss AI',
},
},
transport,
});
const result = await agents.generateStructured({
agentId: 'non-heroes-boss-ai',
instructions: perRequestInstructions,
maxTokens: 320,
prompt,
schema: bossAiOutputSchema,
signal: AbortSignal.timeout(timeoutMs),
temperature: 0.35,
});
return result.object;Pass a transport-native model id. Do not pass Mastra's openrouter/google/…
wrapper to ./openrouter, and do not pass google/gemini-2.5-flash to
./gemini. Do not set process.env.GOOGLE_API_KEY just to satisfy Mastra.
Zod schemas stay in the product — kit only calls schema.parse.
Usage
Install the exact reviewed npm version:
pnpm add @pieai/[email protected]Import only the narrow module needed by the service:
import {
createContentModerationProvider,
ADULT_COMEDY_MODERATION_POLICY,
} from '@pieai/swimmer-ai-kit/content-safety';
import { createDualTtsRouter, resolveTtsRoute } from '@pieai/swimmer-ai-kit/tts';
import { firstDefinedEnv } from '@pieai/swimmer-ai-kit/env';
import { parseAiExecutionEvidence } from '@pieai/swimmer-ai-kit/execution-evidence';
import {
createModelRegistry,
createOpenRouterModel,
} from '@pieai/swimmer-ai-kit/models';
import { requestOpenRouterChatCompletion } from '@pieai/swimmer-ai-kit/openrouter';
import { createGeminiChatTransport } from '@pieai/swimmer-ai-kit/gemini';
import type { ChatCompletionTransport } from '@pieai/swimmer-ai-kit/chat';
import { createStructuredOutputClient } from '@pieai/swimmer-ai-kit/structured-output';
import { createAgentRegistry } from '@pieai/swimmer-ai-kit/agents';Execution evidence accepts bounded identifiers, exact integer-string usage and cost fields, and explicit completeness states. Unknown fields are discarded so raw prompts, completions, provider payloads, and accidental credential fields cannot cross through the parsed result. Products still decide pricing and whether a failed attempt is billable.
Assess a real-person reference separately from harmful-content moderation:
const moderation = createContentModerationProvider({
sightengineApiUser: process.env.SIGHTENGINE_API_USER,
sightengineApiSecret: process.env.SIGHTENGINE_API_SECRET,
});
const decision = await moderation.reviewAdultReference?.({
kind: 'image',
dataUrl: uploadedImageDataUrl,
});
if (decision?.status !== 'adult') {
// Product decides how to explain minor, uncertain, or no_real_face.
}The assessment uses Sightengine face-age, requires exactly one real face, and
fails closed when credentials, the classifier, or a reliable score are
unavailable. A normal photo of a minor is not labeled child exploitation: the
result is minor_reference_detected, a feature eligibility decision.
Sightengine signals vs default decide
parseSightengineDecision is the published default decide. It does not change
when a product wants a stricter (or looser) visual rule.
parseSightengineResponse is the normalize step that default decide used to
throw away:
signals: every finite number in the JSON tree, with lowercased path keys. This includestext.email,nudity.context.indoor_other,nudity.bikini, and non-risk numbers such asrequest.operations.raw: the classifier payload as returned. OCR match arrays andtext.detected_categoriesare often strings, not numbers — they live here.
maxSightengineScore and sightenginePathMatches let a product write its own
predicates. Example — block OCR email/phone the way Show's latch test does,
without changing kit defaults:
import {
maxSightengineScore,
parseSightengineResponse,
sightenginePathMatches,
} from '@pieai/swimmer-ai-kit/content-safety';
const parsed = parseSightengineResponse(raw);
if (parsed.ok) {
const pii = maxSightengineScore(parsed.signals, (path) =>
sightenginePathMatches(path, [/email/, /phone/, /ssn/, /pii/]),
);
if (pii !== undefined && pii >= 0.7) {
// Product decision. Kit default still allows this payload.
}
}nudity.context.indoor_other is a scene-location class (indoor vs beach/pool),
not an adult-content score. Sightengine documents it for combining with
swimwear ("allow bikini at a pool, not indoors"). A product that wants that
rule should join nudity.bikini with nudity.context.*. Treating
indoor_other alone as visual_adult is expressible and is probably wrong.
Whitespace-only apiUser / apiSecret stay missing credentials.
readSightengineCredentials, createSightengineVisualTransport, and
createSightengineVisualProvider all return undefined so callers fall back
to local visual_review_unavailable. That is input validation, not a policy
threshold.
Show Sightengine migration
Do not wrap kit default decide and then re-run Show's parser. Use the
transport, then apply Show's predicates to signals / raw.
import {
createSightengineVisualTransport,
maxSightengineScore,
moderationFallback,
sightenginePathMatches,
} from '@pieai/swimmer-ai-kit/content-safety';
const transport = createSightengineVisualTransport({
apiSecret: process.env.SIGHTENGINE_API_SECRET,
apiUser: process.env.SIGHTENGINE_API_USER,
fetchImpl,
});
const checked = await transport?.check(asset);
if (!checked) {
// keys missing — same as today's kit factory
}
if (!checked?.ok) {
return moderationFallback(checked?.reasonCode ?? 'visual_review_unavailable', 'sightengine');
}
const pii = maxSightengineScore(checked.signals, (path) =>
sightenginePathMatches(path, [/email/, /phone/, /ssn/, /pii/]),
);
if (pii !== undefined && pii >= 0.7) {
return {
allowed: false,
category: 'private_info',
provider: 'sightengine',
reasonCode: 'visual_private_info',
};
}Kit still does not own Show's local text rules, reviewOutput asset
extraction, or OpenRouter vision fallback. FormData filenames stay
asset-image.png / asset-video.mp4; Sightengine does not use the name
as a signal. Do not change Show in this package.
Secrets are inputs only. This package never stores, logs, or bundles secret values.
Images API
createOpenRouterImageTransport and OpenRouterImageError live on
@pieai/swimmer-ai-kit/openrouter-images, not the root barrel. The transport
uses the dedicated Images API
reviewed on 2026-09-13, not a chat-output workaround. Existing brand headers
and usage normalization are reused; normalizeOpenRouterUsage is now also
exported from ./openrouter without changing its behavior.
Each request requires a model, provider tag, prompt and caller-owned abort
signal. It pins one image, allow_fallbacks: false, buffered PNG output and
up to four bounded inline PNG/JPEG/WebP reference data URLs. It does not
fetch reference URLs, retry, load host auth, discover models, choose prices,
reserve funds, write files or adopt media. Products must verify the selected
endpoint's current capabilities and price before obtaining consent.
The request hook runs after serialization and before sending; failure prevents dispatch. The returned-body hook receives bounded raw data before parsing for product-owned recovery. Hooks contain no headers but may contain private work or reference bytes, so products must apply their own authorized, redacted retention boundary. Error causes distinguish cancellation, rejection, storage, transport and invalid responses while financial outcome remains unknown after dispatch. Never infer a refund or retry from cancellation alone.
Provider-reported MIME and canonical base64 are transport evidence, not proof of valid media or artistic quality. Products still inspect bytes and candidates before adopting them. Video generation is not implemented by this subpath; existing TTS and vision contracts remain separate. Only synthetic HTTP tests have run; no real image, provider account or model bill has been tested.
The transport first shipped in 0.7.0. The 0.9.0 candidate adds
parseOpenRouterImageResponse(model, retainedBody) on this same subpath, so a
product can recover a previously retained response without calling generation
again. It checks the bounded response shape/base64/MIME and normalizes usage;
the product still verifies actual image bytes, owner/task binding, adoption and
settlement. Parsing is not proof of image quality or authority to charge a user.
User-owned Codex connection (server only)
@pieai/swimmer-ai-kit/openai-codex consumes the public APIs of the optional
peer @earendil-works/[email protected]. Install that peer only in servers using this
subpath; existing transports and root exports are unchanged. Node 24 is required.
Use authorizeCodexDevice({ signal, onDeviceCode }) in an isolated, bounded job.
The callback receives only the official verification URL, user-visible code and
expiry, not the private device identifier. Store the returned credential through
the host's encrypted, owner-bound custody service. No credential is discovered
from the machine, environment, browser or global Pi session. The host must obtain
appropriate account consent and verify the provider's supported deployment use;
this library does not grant commercial permission.
refreshCodexCredential(credential, signal) performs one refresh. The host holds
an exclusive cross-process connection claim, saves the rotated credential before
using it and treats a lost acknowledgement as uncertain. Never retry an uncertain
refresh or infer provider-side revocation from a local disconnect.
createCodexChatTransport({ credential, model, onAttempt, fetchImpl? }) implements
the existing chat contract for one system message and one text user message.
Choose an explicit ID from codexTextModels(); catalog presence is not proof of
account entitlement. A required abort signal, one SSE attempt, no tools, no hidden
retry and no account/API-key fallback bound the execution. The credential and
model are pinned when the transport is created.
onAttempt runs at the SDK's final fetch boundary. It receives decompressed JSON,
its SHA-256 and the actual wire-body SHA-256 (identity or zstd encoding), without
headers or credentials. This body may contain private work: retain it only through
the product's authorized evidence store, with its redaction/retention rules. A
callback failure prevents dispatch; the observer cannot modify the transmitted
request. Observation is an attempted submission, not proof the provider read it.
The transport bounds request and response bytes and requires a completed stream.
It does not claim a hard output-token cap: the Codex subscription protocol does
not send max_output_tokens, even when the generic contract has maxTokens.
Products must expose bounded calls/deadlines and the user's actual account quota.
Reported usage has no invented API cost or free-credit promise. No ChatGPT web
memory, image/video entitlement, account sharing or automatic quota rotation is
provided. Current evidence is real installed SDK plus synthetic HTTP, not a real
user login or paid model test.
Verify
pnpm typecheck, pnpm test, pnpm build, or pnpm verify.
Origins
The first extraction came from behavior already present in TuringPact and Show.
Show now consumes ./env, ./models, ./provider-budget, and
./generator-registry; its camera-vision and OpenRouter moderation paths still
call OpenRouter outside this kit.
doc/LearningFrom0/ is a historical beginner tutorial. It is not the public API
SSOT.
