npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mexty/ai

v0.1.0

Published

Run real language models from a Mexty block, through the activity contract its author set

Readme

@mexty/ai

Run real language models from a Mexty block — Claude, GPT, Gemini and Mistral — under a contract the block's author set. The block sends a conversation; the platform decides which model runs, whether the budget allows it, and what it costs. No API key ever reaches the browser, and a block cannot pick a model its author did not allow.

This is the door for AI-literacy activities: "ask the same question to four models", "compare answers", "see what a prompt change does". If you want persistent data, that is @mexty/database; live shared state is @mexty/multiplayer. The three work together.

Before you write any code

Linking an AI activity to a block is Mexty staff only, and it does not happen in code. A staff member creates the activity (which providers and tiers it may run, its system prompt, its output cap, its budgets) and links the block to it. This package reads whatever activity id ended up in the block's props.

Two things are required:

1. Declare the property in your BlockProps interface, verbatim:

/** Mexty AI activity this block runs models through. Staff-managed. */
aiActivityId?: string;

2. Add the dependency. The block scaffold does not ship it:

"dependencies": {
  "@mexty/ai": "^0.1.0"
}

A range, deliberately, for the same reason @mexty/database uses one: the fixes that matter here are client-side (a session that stops renewing, a stream format change), and an exact pin would mean editing every block to receive them.

⚠️ What the learner sees, the model sees

Whatever text the block puts in messages is sent to a third-party model provider. Do not put other learners' data, saved answers, or anything the learner did not type or explicitly choose into a prompt. The activity's system prompt is the author's and is added server-side; a block cannot set or override it.

Usage

One answer from one model

import { useMextyAi, useAiRun } from '@mexty/ai';

function AskBlock({ aiActivityId }: BlockProps) {
  const ai = useMextyAi(aiActivityId);
  const { text, isStreaming, result, error, run } = useAiRun(ai, { provider: 'gemini' });

  return (
    <>
      <button onClick={() => run('Why is the sky blue?')} disabled={isStreaming}>Ask</button>
      <p>{text}</p>
      {result && <small>{result.metadata.modelId} · {result.metadata.usage?.totalTokens} tokens</small>}
      {error && <p role="alert">{error.message}</p>}
    </>
  );
}

text fills in as the answer streams. result is set when it finishes and carries the run id, the model that actually ran, token usage and the cost in credits.

Compare models — one hook per model

const ai = useMextyAi(aiActivityId);
const claude = useAiRun(ai, { provider: 'claude' });
const gpt = useAiRun(ai, { provider: 'gpt' });
const gemini = useAiRun(ai, { provider: 'gemini', tier: 'fast' });

const askAll = (q: string) => { claude.run(q); gpt.run(q); gemini.run(q); };

Each instance has its own text, streaming state and cost. Only providers and tiers the activity allows will run — anything else answers with a 400 the block can show.

Let the learner pick

import { useAiModels } from '@mexty/ai';

const { models, loading } = useAiModels(ai);
// models: [{ provider, tier, modelId, label, isDefault }, …] — what this block may run right now

A conversation

Pass the history instead of a string; it must end on the learner's turn.

run([
  { role: 'user', content: 'What is a neural network?' },
  { role: 'assistant', content: previousAnswer },
  { role: 'user', content: 'Explain it to a 10-year-old.' },
]);

Handling the states a block can be in

const ai = useMextyAi(props.aiActivityId);

if (ai.status === 'unauthenticated') return <SignInPrompt />;

| status | meaning | |---|---| | ready | linked and reachable — runs work | | no-activity | no activity id was given to this block | | sandbox | the block is not running with a blockId | | unauthenticated | this activity needs a signed-in viewer and there is none |

Render normally on all four. When the client is not ready, run rejects with an AiRunError whose reason is the status and useAiRun puts it in error; models() resolves empty. A block whose activity was never linked still renders — which is its normal state in the marketplace, and after anyone forks it.

When a run is refused

error.status is the HTTP status and error.reason says why:

| reason | meaning | |---|---| | owner-credits | the activity's owner is out of credits | | learner-budget | this learner has used their share of the activity | | activity-budget | the activity's own budget is exhausted | | aborted | stop() was called | | stream | the model call failed mid-answer |

Show these; do not retry them in a loop.

API

useMextyAi(activityId?)

The client for this block. Recreated only when the id changes, so it is safe to call straight from props. Re-renders when status changes.

useAiRun(ai, { provider?, tier? })

{ text, isStreaming, result, metadata, error, run, stop, reset }. run(input, overrides?) takes a string (one learner turn) or a message list and returns the result, or null if it was stopped or refused. Starting a run stops the one in flight.

useAiModels(ai)

{ models, loading, error, reload }.

The client directly

const ai = createAiClient(activityId);
await ai.models();                                   // AiModel[]
await ai.run({ provider, tier?, messages, signal?, onDelta?, onStart? }); // { text, metadata }
ai.status;                                           // see above
ai.context;                                          // blockId, shareToken… from the URL

Semantics & limits

  • Providers are claude | gpt | gemini | mistral; tiers are frontier | flagship | balanced | fast. Which pairs run is the activity's decision; the concrete model behind a pair is resolved by the platform at call time from what it currently offers, so a block never breaks because a model was retired.
  • Messages are plain text, user / assistant only, at most 40 turns, ending on user. Total characters are capped per activity (8,000 by default); the server answers 400 with the limit when exceeded.
  • Output is capped per activity (1,024 tokens by default). A short answer is the intended shape.
  • Every run is billed to the activity's owner and counted against the activity's budget and the learner's share. A refused run costs nothing.
  • Anonymous learners are admitted only when the activity allows it, under the same anonymous id @mexty/database uses — so a teacher's ledger lines up with the class's saved rows.
  • A 429 is retried with backoff. An expired session is renewed once and the request replayed; unauthenticated means the renewal failed too.
  • Do NOT call configure() on the platform. The server URL is resolved from the page's hostname; overriding it points a block at the wrong environment.

Development

npm install
npm run typecheck
npm test
npm run build

The stream fixtures under src/__fixtures__ were produced by the backend's own ai@5 and are what a block receives byte for byte. Regenerate them when the run route's stream changes.