typesafe-ish
v0.1.0
Published
TypeSafe-compatible question and answer client backed by AI SDK structured output
Readme
typesafe-ish
A TypeSafe-compatible question/answer client backed by AI SDK structured output.
The current target is @typesafe-ai/sdk 0.6.0 and AI SDK 7.
systemOne() returns validated answers through the official APIPromise.
models.list() discovers provider models as official ModelCard objects.
The package supports Node.js 22.18+ and Bun, with ESM, CommonJS, and TypeScript
declarations. TypeScript is a development dependency, not a required consumer peer.
Node and JSON Schema typings required by AI SDK are included as dependencies.
The generated declarations load Node typings explicitly, so consumers can keep
types: [] and full declaration checking.
bun install --frozen-lockfile
bun run check # Lint, types, tests, build, and package smoke checks
bun run build # JavaScript and declarations in dist/
bun run test:package # Build and check Node imports and declarations
bun run lint # Lint source, tests, and examples
bun run format # Format with Biome
bun run test:compat # Official SDK export, resource, and type parityPackaging and release
bun run check verifies the built package's import and require entry points,
checks their authentication error contracts, and compiles usage examples against
its ESM and CommonJS declarations. These smoke checks use the local build without
packing, installing, or making network requests. CI runs this gate on Node.js
22.18 and 24.
npm pack builds the package automatically. Only dist/, README.md, LICENSE,
and package.json are included. For a local install before publication:
npm pack
# In your application, using the path printed by npm pack:
npm install /path/to/typesafe-ish-0.1.0.tgznpm publish runs the complete check before publishing. Building and checking
the package do not publish it. The behavioral limitations below remain outside
the MVP release gate.
Usage contract
import { TypeSafeClient, choice } from "typesafe-ish";
import type { TypeSafeClientConfig } from "@typesafe-ai/sdk";
// For example: { defaultModel: "openai:gpt-6-luna", apiKey: "..." }
async function classify(config: TypeSafeClientConfig) {
const client = new TypeSafeClient(config);
const response = await client.systemOne({
state: { document: "I was charged twice." },
questions: {
category: choice("What is this about?", {
billing: null,
technical: null,
other: null,
}),
},
});
return response.answers.category.choice; // "billing" | "technical" | "other"
}examples/basic.ts covers all three question primitives.
Cancellation
Pass the native SDK's RequestOptions.signal as the second argument to
systemOne, or the options argument to models.list:
await client.systemOne(request, { signal: AbortSignal.timeout(10_000) });
await client.models.list({ signal: AbortSignal.timeout(10_000) });Use an AbortController's signal for manual cancellation.
Cancellation covers provider requests, response body reads, inference retry waits,
and model discovery pagination. Already-aborted signals reject before fetching.
Caller cancellation rejects with the official APIUserAbortError, retaining the
underlying failure as cause; asResponse() and withResponse() share that error.
Custom fetch implementations must honor the supplied signal. An external timeout
signal also counts as caller cancellation; the SDK's timeout option remains ignored.
Optional reasoning level
Prefer omitting reasoning settings: typesafe-ish leaves them unset so AI SDK and the selected provider handle the default. To opt in for a particular request:
import { TypeSafeClient, noul } from "typesafe-ish";
const client = new TypeSafeClient({ defaultModel: "openai:gpt-6-luna" });
const request = {
state: "The invoice was paid yesterday.",
questions: { paid: noul("Has the invoice been paid?") },
};
await client.systemOne(request); // Uses the provider's default reasoning.
await client.systemOne({ ...request, reasoning: "high" }); // Explicit override.For a deliberate client-wide setting, pass defaultReasoning in the constructor.
request.reasoning overrides it; an omitted or undefined request value inherits
it. Use reasoning: "provider-default" to bypass a client default for one call.
"none" requests disabled reasoning where supported; it does not mean default.
Reasoning settings use AI SDK's LanguageModelCallOptions["reasoning"] type
directly. Accepted levels follow the
installed SDK's reasoning option.
AI SDK handles provider/model support and mapping.
The installed OpenRouter adapter needs a compatibility bridge: explicit levels
are forwarded as reasoning.effort,
while omission and "provider-default" leave that field unset. Actual support
still depends on the selected model.
Import TypeSafeClientConfig and SystemOneRequest from typesafe-ish when
using these optional extensions. Existing configurations
and requests typed with @typesafe-ai/sdk remain accepted, and answer inference
is preserved.
Structure
src/
index.ts Package entry point; public exports and SDK helpers
types.ts Compatible config/request extensions
client.ts TypeSafeClient facade
resources/models.ts models.list() facade
providers/resolve-model.ts Built-in provider instances and model overrides
system-one/
prompt.ts State/question prompt and typed answer schema
execute.ts AI SDK structured-output execution
result.ts Answer validation and usage mapping
examples/basic.ts Typechecked usage with choice, noul and score
tsdown.config.ts ESM/CommonJS and declaration build
tests/
prompt.test.ts Prompt serialization and answer schema
result.test.ts Answer, confidence, rubric, and usage mapping
errors.test.ts Inference errors and response-helper rejection contracts
abort.test.ts Cancellation, retry waits, pagination, and response helpers
reasoning.test.ts Reasoning defaults, overrides, and provider request bodies
compatibility.test.ts Official SDK exports and native Models resource
compatibility.types.ts Official SDK compatibility and shared type parity
package/ Node import and declaration smoke checksImplementation boundaries
Question types, result types, and the choice, noul, and score helpers come
from the official SDK. Configuration and request types extend the official types
with optional reasoning settings. Set defaultModel in the client configuration
or model in the request. No model is selected implicitly.
AI SDK model instances and custom resolveModel callbacks are not client options.
An AI SDK provider registry resolves anthropic:<model-id>, google:<model-id>,
openai:<model-id>, xai:<model-id>, openrouter:<model-id>,
deepseek:<model-id>, and gateway:<creator>/<model-id> without local model
allowlists. Non-gateway IDs call providers directly. Model IDs can include colons, such as
openrouter:nvidia/nemotron-3-ultra-550b-a55b:free.
Credentials come from apiKey or the provider's environment variable
(ANTHROPIC_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, OPENAI_API_KEY,
XAI_API_KEY, OPENROUTER_API_KEY, DEEPSEEK_API_KEY, or AI_GATEWAY_API_KEY), in that
order. Providers validate credentials and model availability when making requests.
Malformed IDs and unregistered providers produce AI SDK registry errors.
The pipeline is resolve model → build prompt and Zod schema → generateText
with Output.object → validate/map answers and usage → adapt to APIPromise.
The schema is passed directly to the AI SDK, not embedded in the prompt.
Choice and score probabilities must sum to one within 1e-6. Choice labels must
agree with the highest probability. Confidence, expected scores, and rubric
legends are computed locally. Confidence follows the official TypeSafe
formulas: choice
uses the top probability relative to an even split; score uses the probability-weighted
distance from the most likely level, normalized by the uniform distribution's
spread. Confidence is excluded from the LLM prompt and output schema. Noul answers
have no confidence field. Missing input or output token counts cause an error.
Response helpers expose a synthesized HTTP 200 response containing normalized result JSON, not the provider's raw response. Provider request IDs are not fabricated. Structured-output and provider errors reject the result and helpers.
Inference translates AI SDK APICallError failures with status 400 or higher into
the corresponding official APIError subclass. This also applies when a
RetryError ends with such an APICallError. The translated error retains status,
headers, the provider's parsed error data (or raw response body), and the original
error as cause. Caller cancellation rejects with APIUserAbortError; other errors,
including transport, structured-output, and Gateway-specific errors, bubble unchanged.
The client's fetch and baseURL settings apply to both inference and model
discovery. Custom endpoints must support the selected provider's API.
The official configuration and request-option types are accepted. Request-option
signal is supported; custom headers, timeout, and retry settings are currently ignored.
Score rubrics must be arrays with at least two entries; invalid rubrics throw
TypeSafeError before model resolution.
Model discovery
Models is re-exported directly from the official SDK. client.models is a native
SDK resource whose list() method is replaced with provider discovery. The resource
is created through an SDK client with its TypeSafe API transport disabled.
models.list() queries the provider selected by defaultModel, or the public
AI Gateway catalog when no default is set. Card names are namespaced model IDs
that can be passed to systemOne. Anthropic and Google pagination is followed.
Missing metadata is an empty string; release_date uses the catalog's creation
date when available, not a verified model release date.
Discovery uses provider HTTP catalogs because AI SDK providers do not expose a
common listing method. The public Gateway catalog also includes creation dates
that gateway.getAvailableModels() omits. Discovery uses the configured fetch
(or global fetch by default) and honors baseURL and request-option signal.
Custom headers, timeout and retry settings are ignored.
Response helpers expose normalized { models: [...] } JSON.
Tests
The suite checks prompt construction and result mapping, plus export, resource, and type compatibility with the installed official SDK. Reasoning tests capture provider requests to check defaults, overrides, and OpenRouter compatibility. Error tests use custom fetch responses to check HTTP error translation, retries, response helpers, and unchanged propagation of unrelated errors. Live inference and catalog discovery are outside this suite.
Live provider evaluations should be a separate, explicitly enabled suite. Compare decisions and schema conformance; predictions, probabilities, confidence, usage and model metadata are not expected to be identical across models. Returning compatible numeric fields will not by itself establish TypeSafe-style probability calibration.
