mrkeyoor.com_
Wed 28 Oct 23:31 UTC
npmAI / MLupdated 28 Oct 2026

@earendil-works/pi-ai review

A model's tool request still needs code that decides whether to run it. @earendil-works/pi-ai 1.4.0 gives TypeScript applications a shared model-call interface with provider collections, authentication resolution, and streaming responses. You register providers, build a conversation, and read text or tool-call blocks. Your application validates and executes requested tools, records their results, and decides whether to ask the model again. Choose pi-ai when you want a model connection layer and intend to own the surrounding workflow.

Verdict

Choose @earendil-works/pi-ai when you want a shared model-call interface and can own the conversation and tool execution around it. Before adding a tool that changes real data, implement a read-only round trip with validation and explicit failure handling.

Answers from our run

@earendil-works/pi-ai or ai: which should you use?

ai: The ai package offers a single interface through Vercel AI Gateway or direct provider connections. Its supplied registry description makes it a candidate for comparing shared provider interfaces. The evidence does not establish performance rankings or feature parity. Choose @earendil-works/pi-ai when you want a shared model-call interface and can own the conversation and tool execution around it.

When should you not use @earendil-works/pi-ai?

You want a finished coding assistant. See the separate Pi review for the terminal workflow.

API stability3/5The 1.4.0 registry metadata supplies ESM exports and TypeScript declarations, giving an integration a concrete import surface to pin. The matching changelog lists breaking changes, including the telemetryContext rename without an alias. Under the documentation rubric, 3 credits a usable versioned interface with migration caveats. It does not establish compatibility across minor upgrades.
Docs4/5The supplied tagged README excerpts cover provider registration, conversation context, streaming, tool calls, and argument validation. The supplied validation implementation shows tool-name lookup and argument checks. Under the documentation rubric, 4 credits those inspectable instructions and boundaries. It does not mean these illustrations were compiled or provider requests were tested.
Maintenance3/5The versioned changelog dates 1.4.0 to October 21, 2026 and records identity options, telemetry changes, and retry fixes. Under the documentation rubric, 3 credits recorded maintenance on this release while retaining the caveat that it includes breaking changes. Release notes do not measure support response time or establish future upkeep.
Ecosystem3/5The tagged README documents individual provider factories and an all-providers entrypoint, supporting integrations with different model services. Under the documentation rubric, 3 credits that documented integration surface. The supplied evidence contains no adoption measurement, compatibility matrix, or cross-provider execution results. A shared conversation format does not establish identical upstream behavior.

Use it if

  • You need to call different model providers through one conversation and event format while keeping your application's workflow explicit.
  • You want to register only the providers your application uses instead of importing every built-in catalog.
  • You're comfortable managing conversation messages and writing code that validates tool requests, executes allowed operations, and returns results.
  • You're building around the Pi packages and need to understand the model connection before adding an agent loop.
Skip it if

Setup reality

Install the documented release with npm install --save-exact @earendil-works/pi-ai@1.4.0 and keep the lockfile. Version 1.4.0 is ESM, provides TypeScript declarations, and declares Node >=22.19.0. Its registry metadata lists root and providers/* exports; the tagged README documents the openai provider import used below. Type is re-exported by pi-ai, so these illustrations do not require a separate direct TypeBox import.

These are documentation-based, unexecuted TypeScript illustrations for 1.4.0. The supplied evidence contains tagged README excerpts, registry metadata, changelog excerpts, and validation source. It contains no tarball README comparison or recorded installation, compilation, provider request, benchmark, or security test. No corrected example was executed for this revision.

Run the initial register-provider snippet before any other illustration. Each subsequent illustration needs its imports, models collection, and model. build-context defines context for read-completion and stream-text; run those two exercises independently if you want to start from the same conversation. execute-tool-round also requires define-tool, and continue-after-tools requires both tool snippets in order. The examples require provider access configured by your application. Registering a provider does not grant account access or establish that a catalog model is enabled for you.

Start with one provider. The all-providers entrypoint imports every built-in catalog and factory. Individual factories avoid importing other providers, and the documentation says SDK implementations can remain in lazy chunks with bundler code splitting. That is a packaging design, not a measured size or loading-time promise for your build. Check your deployment's bundle before budgeting for browser delivery. Keep credentials in your trusted backend.

Your application designs the conversation lifecycle. A completion returns an assistant message containing typed content blocks. A tool-call block requests an action. Validate it, enforce permissions where the operation runs, add a toolResult with the matching call ID, and then request a continuation. Schema validation does not authorize a database write or file read. The example uses a fixed in-memory catalog and performs one tool round.

A stream's partial response is shared live state, not a historical snapshot. Read event deltas for immediate output, or copy the fields you need for a historical UI record. Consume the final result and inspect stopReason before saving a turn as successful. Partial text and a resolved result promise alone do not establish that the requested task completed. Production code also needs exception handling around setup and application tool operations.

Read the exact release notes before upgrading: 1.4.0 renamed telemetryContext to telemetry without a compatibility alias. The release also changes request identity configuration. Pinning the dependency identifies the documentation target; remote provider behavior and account access remain separate concerns.

For the surrounding products, see Pi's terminal workflow and pi-agent-core's agent loop. This guide explains the model-call boundary beneath them.

Ratings use a documentation-based 1 to 5 rubric: 1 means insufficient support for the integration task, 2 means substantial gaps, 3 means usable with explicit caveats, and 4 means clearly documented behavior with inspectable supporting detail. A 5 requires operational evidence absent from this guide. Each rationale states the evidence credited. These editorial scores do not measure reliability, adoption, or cross-provider compatibility.

Patterns

Register one provider and check the model lookup register-provider

import { createModels, Type, validateToolCall,
  type Context, type Tool } from '@earendil-works/pi-ai';
import { openaiProvider } from '@earendil-works/pi-ai/providers/openai';

const models = createModels();
models.setProvider(openaiProvider());
const model = models.getModel('openai', 'gpt-4o-mini');
if (!model) throw new Error('Model missing from registered catalog');

Unexecuted setup illustration required by every subsequent snippet. It checks catalog membership, not account entitlement or provider availability. Keep these imports and variables in scope.

Make the conversation explicit build-context

const context: Context = {
  systemPrompt: 'Answer briefly. Do not invent catalog entries.',
  messages: [{
    role: 'user',
    content: 'Explain what a model provider does.',
    timestamp: Date.now(),
  }],
};

Run after register-provider. Context carries the messages sent to the model. Your application decides when to add or remove turns; this object is not a durable session store.

Inspect the final message before keeping it read-completion

const reply = await models.complete(model, context);
if (reply.stopReason === 'error' || reply.stopReason === 'aborted') {
  throw new Error(reply.errorMessage ?? reply.stopReason);
}
if (reply.stopReason !== 'stop') {
  throw new Error(`Handle stop reason explicitly: ${reply.stopReason}`);
}
for (const block of reply.content) {
  if (block.type === 'text') console.log(block.text);
}
context.messages.push(reply);

Requires register-provider and build-context. This text-only exercise accepts only stop and rejects other stop reasons instead of treating them as a finished answer. An application still needs an outer error handler.

Print text deltas and retain the terminal result stream-text

const stream = models.stream(model, context);
for await (const event of stream) {
  if (event.type === 'text_delta') process.stdout.write(event.delta);
  if (event.type === 'error') console.error(event.error.errorMessage);
}
const final = await stream.result();
console.log('Stop reason:', final.stopReason);

Requires register-provider and build-context. Run independently of read-completion to use the original conversation. This displays a response without appending it to the transcript. Inspect the terminal stop reason before deciding what to retain. A stream's partial response is shared live state, not a historical snapshot.

Build a picker from the registered chat catalog list-chat-models

const choices = models.getModels('openai').map((entry) => ({
  id: entry.id,
  name: entry.name,
  acceptsImages: entry.input.includes('image'),
}));
console.log(choices);

Requires register-provider. getModels() reads the last-known chat catalog synchronously. An entry does not establish configured credentials or upstream account access. Image-generation and classifier catalogs have separate typed reads.

Describe a read-only catalog lookup define-tool

const lookup: Tool = {
  name: 'lookup_entry',
  description: 'Read an entry from the fixed example catalog',
  parameters: Type.Object({ key: Type.String() }, {
    additionalProperties: false,
  }),
};
const toolContext: Context = {
  messages: [{
    role: 'user', content: 'Look up the public entry.',
    timestamp: Date.now(),
  }],
  tools: [lookup],
};
const catalog = new Map([['public', 'Example published help text']]);

Requires register-provider. This defines a schema, not an executable callback. The catalog is fixture data. execute-tool-round supplies validation and execution.

Validate requests and return matching tool results execute-tool-round

const requested = await models.complete(model, toolContext);
if (requested.stopReason === 'error' ||
    requested.stopReason === 'aborted' ||
    requested.stopReason === 'length') {
  throw new Error(requested.errorMessage ?? requested.stopReason);
}
toolContext.messages.push(requested);
const calls = requested.content.filter((b) => b.type === 'toolCall');
for (const call of calls) {
  let text: string;
  let isError = false;
  try {
    const args = validateToolCall([lookup], call);
    const value = catalog.get(args.key);
    if (value === undefined) throw new Error('Unknown catalog key');
    text = value;
  } catch (error) {
    isError = true;
    text = error instanceof Error ? error.message : 'Lookup failed';
  }
  toolContext.messages.push({
    role: 'toolResult', toolCallId: call.id, toolName: call.name,
    content: [{ type: 'text', text }], isError, timestamp: Date.now(),
  });
}

Requires register-provider and define-tool. validateToolCall rejects unknown tool names and validates arguments against the matching schema; its implementation can coerce values. Execute the returned arguments, not the raw call. Application authorization remains separate. This illustration runs only a fixed read-only lookup and represents lookup or validation errors as tool results.

Ask for one response to the tool results continue-after-tools

if (calls.length > 0) {
  const continuation = await models.complete(model, toolContext);
  console.log('Continuation stop reason:', continuation.stopReason);
  for (const block of continuation.content) {
    if (block.type === 'text') console.log(block.text);
  }
}

Requires register-provider, define-tool, and execute-tool-round in that order. This requests one continuation and displays it without declaring the task finished or retaining the response. Inspect its stop reason. If it requests more tools, validate and handle another round or stop deliberately. Add explicit budgets before turning this into a loop.

Expose configuration status without logging credentials inspect-auth-status

const auth = await models.getAuth('openai');
console.log(auth ? 'Provider configured' : 'Provider not configured');

Requires register-provider. This resolves provider authentication without a model request. The result can contain credentials or headers, so do not log the object. Resolution may refresh OAuth credentials where applicable and may reject on a configuration or refresh failure. Configured does not mean a request will succeed.

Alternatives

PackageRegistryPick it when
ainpmThe ai package offers a single interface through Vercel AI Gateway or direct provider connections. Its supplied registry description makes it a candidate for comparing shared provider interfaces. The evidence does not establish performance rankings or feature parity.
openainpmThe openai package is the official TypeScript library for the OpenAI API. Compare it when OpenAI is your provider requirement. The supplied registry description establishes that scope without establishing a bundle-size or integration-effort advantage.

More ai / ml guides

openai · mcp · huggingface-hub · @modelcontextprotocol/sdk · tiktoken · scikit-learn · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.