Sorts an array by asking Claude Opus to do it.
import { claudeSort } from "claude-sort";
const result = await claudeSort([3, 1, 2]);
// { outcome: "sorted", items: [1, 2, 3], attempts: 1, usage: { inputTokens: 212, outputTokens: 51 } }Array.prototype.sort does this in about 40 nanoseconds and no dollars. This
package does it in about four seconds and a fraction of a cent, over the
network, with retries. It is a joke about how much of that trade we have been
making lately, and it is built like it is not one: strict TypeScript, a pure
core, schema-validated I/O, assertions on both sides of every boundary, and 73
tests that never touch the network.
npm install claude-sortCredentials resolve the way the Anthropic SDK resolves them: ANTHROPIC_API_KEY,
then ANTHROPIC_AUTH_TOKEN, then an ant auth login profile. Pass apiKey to
override that for one call.
Every ending is a named outcome, so nothing has to be caught for a sort that simply did not happen:
import { claudeSort } from "claude-sort";
const result = await claudeSort(["Neptune", "Mercury", "Earth", "Jupiter"], {
criterion: "distance from the Sun, nearest first",
});
switch (result.outcome) {
case "sorted":
console.log(result.items); // ["Mercury", "Earth", "Jupiter", "Neptune"]
break;
case "limitExceeded":
console.log(`too many: ${result.limit} was ${result.actual}`);
break;
case "unsorted":
console.log(`no valid ordering in ${result.attempts} attempts`);
break;
case "refused":
console.log(result.detail);
break;
case "apiError":
console.log(`api error ${result.httpStatus}`);
break;
}The comparator is a sentence, which is the one thing this does that
Array.prototype.sort cannot:
await claudeSort(bugReports, { criterion: "how angry the reporter sounds" });
await claudeSort(names, { criterion: "Vietnamese alphabetical order" });
await claudeSort(commits, { criterion: "most likely to have caused the outage" });Objects are labelled before they are sent, and the sorted array holds the original references — the model never sees or returns your objects, only their labels and a permutation of their indices:
const albums = [
{ title: "Kid A", year: 2000 },
{ title: "The Bends", year: 1995 },
];
const result = await claudeSort(albums, {
criterion: "release year, oldest first",
describeItem: (album) => `${album.title} (${album.year})`,
});
// result.items[0] === albums[1], by reference| Option | Default | What it does |
|---|---|---|
criterion |
ascending, numbers numerically and strings lexicographically | The comparator, in English |
model |
claude-opus-5 |
Any model id the Messages API accepts |
effort |
low |
low through max. Sorting is shallow work |
describeItem |
JSON-ish labels | Renders one item as the label the model sees |
attemptsMax |
3 | Total tries. A non-permutation answer costs one |
timeoutMs |
120000 | Deadline for a single request |
apiKey |
from the environment | Overrides credential resolution |
client |
built per call | Supplies your own Anthropic client |
- 100 items per call. A longer list is yours to page. It is not truncated.
- 512 characters per label. One pathological item cannot fill the context window on everything else's behalf.
- The returned array is frozen.
Array.from(result.items)if you need to mutate it.
Both limits come back as { outcome: "limitExceeded", limit, actual, allowed }
before anything is spent.
result.usage reports the tokens each sort took, summed across attempts. At
Opus 5 rates, sorting three integers costs somewhere around one fiftieth of a
cent, which is roughly infinity times what .sort() costs. The comparison is
the point of the package.
- Guarantee an answer. After
attemptsMaxrejected orderings you get{ outcome: "unsorted" }. The model is asked again each time with the reason its last answer was refused. - Guarantee a correct answer. Every result is a genuine permutation of your input — validated, never trusted — so nothing is dropped, duplicated, or invented. Whether that permutation is sorted is the model's opinion.
- Sort stably. Ask for a tiebreaker in the criterion if you need one.
- Log anything. No credential, item, or label is written anywhere.
A missing credential throws rather than returning an outcome: that is a configuration error, not a sort that failed.
Functional core, imperative shell. describe, prompt, ordering, plan,
config, and attempt are pure and hold every decision — including what to do
with a rejected answer; anthropic/request.ts and sort.ts are the only code
that touches a network, and sort.ts only sends, appends, and returns. The split is what lets the
core be tested exhaustively with no key, no mocks, and no spy assertions — the
tests assert on returned values, never on which calls were made.
The model answers with indices, never with your data. An answer is accepted
only after validateOrdering proves it a permutation of the input's indices,
so a hallucinated, dropped, or duplicated element is a rejected attempt rather
than a corrupted array.
Read src/anthropic/AGENTS.md before changing anything that talks to the API.
pnpm install
pnpm test # 73 unit tests, no network
pnpm typecheck
pnpm lint
pnpm build
pnpm example # sorts [3, 1, 2] for real; needs credentialstest/sort.integration.test.ts exercises the shell against the live Messages
API and skips itself unless ANTHROPIC_API_KEY is set. It is the only
coverage the wiring has, by choice: once the decisions were extracted into pure
functions, what was left holds no decision worth asserting against a fake.
MIT