Cole McIntosh

Founder @Mellow AI

|
|Resume

I Ported a Python LLM Library to TypeScript in One Evening

I have a Python library called openextract that turns documents, images, audio, and video into validated structured data. You hand it a Pydantic model, it hands back a typed instance. I wanted the same thing in TypeScript, and not a wrapper that shells out to Python.

PythonTypeScript
Pydantic
Zod
Pydantic AI
Vercel AI SDK
uv, Ruff, pytest
npm, tsc, Vitest
Python 3.12, 3.13
Node 20, 22
One core, five surfaces:LibraryCLIMCP serverTerminal UIWeb app

The port took an evening. Not because Grok 4.6 writes good code, though it does. Because I could run a lot of it at once and get answers back fast enough to stay in the loop.

Those two things are the whole story, and they depend on each other.

I split the library into pieces that did not touch and gave each one its own agent. Core extraction API. Media loading and URL safety. Error types and retry rules. The CLI. Separate branches, separate concerns.

Then I pointed Grok at the pull requests too. It reviewed the diffs and resolved the merge conflicts when branches drifted. That mattered more than I expected it to. Parallelism manufactures integration work. Four branches against a moving main is four conflicts waiting to happen, and if I absorb all of that by hand then the agents have not removed the bottleneck, they have just moved it downstream to me.

That is orchestration, not prompting. The work is cutting the problem so agents can run without stepping on each other, then deciding what survives. It looks less like pair programming and more like running a small team where everyone is faster than you and nobody has any judgment.

Speed is what makes it usable, and I think this is the underrated half. If an agent takes twenty minutes to come back, parallelism does not save you. You are waiting in four places instead of one, and by the time the diffs land you have lost the thread on all of them. When they come back in a couple of minutes, it turns into a loop. Dispatch, read, redirect, dispatch again. The bottleneck stops being how fast the model writes and becomes how fast I can decide.

Throughput is agents times iterations. Slow inference kills both terms.

The other thing that made it work is that the repository already knew what it wanted to be. The Python library was the spec: types, tests, exit codes, error classes, retry rules, a 100 percent coverage threshold. Agents invent when the target is vague, and parallel inventions collide. There was very little room to invent here, so there was very little to reconcile.

By the end of the evening the port was done and passing. Then it kept going. An MCP server. A terminal app. A web interface that streams a generated schema and fills a table. None of that was planned. It was just cheap once the core was native and the loop was already running.

Writing the code got cheap. Reviewing it got cheap. Merging it got cheap. What did not get cheap was deciding what should exist. Whether search and code modes still made sense in a runtime with different primitives. Whether the web app should be a chatbot, which it should not. Whether a rewrite that quietly drops SSRF protection is a rewrite or a demo. Four agents will happily produce four confident answers to a question nobody should have asked.

The orchestration gave me the throughput. The speed made the throughput usable. Neither one gave me an opinion.

The result is in openextract-ts.