Remote — UTC−4 — Open to full-time roles

I ship LLM features
you can trust in production.

I am Alex, an applied AI engineer working in Python. Most of my recent work is the integration layer between applications and LLM providers: streamed tool calls, token and cost accounting, and code that breaks when a provider changes or retires a model. I fix it upstream, in the SDKs themselves, and I build retrieval and agent systems with the evaluation that decides whether they survive real users.

01

Selected work

Every repo is public and every demo is one click, no signup. Read the code.

01

tandaA fleet of coding agents around one shared queue, with a review gate before anything ships.

A small Python tool, with no dependencies, that runs roles of any agent CLI (Claude Code, opencode, Codex or your own command) around one SQLite queue. A scout fills the queue, builders take one item at a time, a reviewer sends work back or approves it, and the run ends by itself when every scout has finished and nothing is left to claim. Waiting is done by tanda, not by the agents: an agent only starts when there is an item for it, so idle roles cost no tokens. Each role may only set the verdicts it is allowed, an agent that exits without one leaves its item on HOLD for a human, and a local dashboard shows the queue and each item's history.

The open-source work below comes from the in-house version of this setup, a set of shell scripts I run every day; tanda is the clean, general rewrite of it, not yet the thing running in production. Running the in-house one taught me more than building it did. The queue never starves for builders; it starves for good candidates. Auditing a receptive repo module by module, comparing each provider adapter with its siblings, finds far more real bugs than hunting for unclaimed issues, which popular repos close within hours. Most rejected work fails on checks nobody writes down: a renamed repo hiding your own earlier PR, a PR template that asks for something only a human can provide, a project whose purpose is one you should not put your name on. Those checks now run before any code is read, and anything that needs a person (a CLA, a sign-off, a design call) waits for one.

Role: Design and implementation

PythonSQLiteClaude CodeAgent orchestrationCLIHTML dashboard
02

llm-drift-lintA linter for LLM API calls that break when a provider changes or retires a model.

Static checks for Python code that calls the Anthropic, OpenAI and Gemini SDKs. It finds the calls, resolves the model id when it is a literal or a constant, and checks each call against what that model accepts today: sampling parameters newer models reject, reading content[0].text when the first block is now a thinking block, a forced tool_choice under thinking, a default model past its shutdown date on OpenAI, the Claude API, the Gemini API or Vertex. An upcoming retirement is a warning and a passed one is an error, and every message links the page the date comes from.

Every rule comes from a bug I fixed upstream, and the test corpus holds the real files from those fixes as they were just before and just after, with the findings a correct linter must report. False positives count as bugs: when a rule cannot tell which model a call uses, it stays silent. On sixteen large code bases (langchain, llama_index, crewAI, dspy, agno, semantic-kernel and others) the first six rules reported exactly two findings, and both were real bugs. The newer rules came out of the same loop: a pattern that keeps turning up in fixes becomes a rule, and the rule then finds the next repos to fix.

Role: Rules, model tables, test corpus

PythonStatic analysisASTCLIpytestGitHub Actions
03

Weather-Aware Truck RoutingThe safest truck route, judged by the forecast for the hour the truck reaches each point.

A React + Django app built as a full-stack take-home. It asks Valhalla for three truck routes with the real gross weight, so roads with weight limits are respected, samples each route every 10, 25 or 50 miles, and gives every checkpoint an ETA from the router's own maneuver timings. Each checkpoint reads the hourly wind, rain and snow forecast for that ETA and gets a risk level from the brief's thresholds plus load rules: strong wind with a heavy load becomes Severe or No Travel. Routes are ranked by No Travel, Severe and High miles, then average risk, then travel time, and the map colours the chosen route by risk next to a 0–48 h forecast heatmap of the corridor.

Most of the judgment went into edge cases the brief left open. Its ranges share endpoints, so a value on a boundary goes to the higher band, because this is a safety tool; a stretch between two checkpoints takes the worse of the two, since over-counting dangerous miles is safer than under-counting them. The free services pushed back too: Open-Meteo rate-limits the shared IP of a free Render instance, so on a 429 the backend asks the browser to fetch the forecast points itself and send them back, and the ranking still runs on the server. The rules are pure functions with table-driven tests.

Role: Design, full build, deployment

ReactTypeScriptMapLibre GLDjangoPythonValhallaOpen-MeteoRenderGitHub Actionspytest
04

Business Ops AgentAn agent that can only act through a protocol, and a harness that scores how it acts.

An MCP server exposing a small business's operations — catalog, booking capacity, stock, catering quotes, orders — as tools any MCP client can call: Claude Desktop, Cursor, or the LangGraph agent that ships with it. The agent holds no business rules; it does not know the catering minimum, because the quoting tool tells it. Add a tool to the server and the agent can use it with no change on the agent side. It runs on AWS Lambda behind a public Function URL, backed by DynamoDB, with the infrastructure in Terraform and a deploy on every push to main.

The evaluation scores the trajectory, not the answer — which tools were called, in what order, with what arguments, and whether every figure in the reply traces back to a tool result. That last check needs no judge model: pull the numbers out of the answer and confirm they appear in something a tool returned. It stays correct when an invented number happens to be right, because the question is whether the agent looked it up. It caught the agent claiming "I don't have that information" with zero tool calls, and caught it intermittently — the same case passed the run before, which is why the harness can repeat a scenario. Worth saying plainly: my scorer was wrong four times before it was right, and the first draft blamed the agent for its own bugs.

Role: Protocol server, agent, evaluation harness, deployment

PythonMCPLangGraphGroqAWS LambdaDynamoDBTerraformDockerGitHub ActionsStreamlitpytest
05

Ask the GDPRAnswers over regulation, citing the provision — not the page.

A Retrieval-Augmented Generation assistant over the full text of the GDPR. Legal text imposes a constraint generic RAG ignores: nobody looks up page 14 of a regulation, they look up Article 17(1), and the page a provision lands on is an artefact of typesetting. So the corpus is not a PDF — a builder parses the Official Journal text from EUR-Lex into 414 provisions that carry their article, paragraph and chapter as metadata, and those travel with every chunk into the vector store. Ships in two modes behind one flag: an in-memory FAISS demo on a free 1 GB container with no database, and a pgvector-backed production path.

An assistant over legal text fails in an unusual direction. A miss announces itself — the user reads a vague answer and goes to the source. An invention is fluent, confident, and indistinguishable from a correct answer; attach a citation the model reasoned its way to rather than read, and it becomes more convincing, not less. So the harness measures refusal, not just recall: seven questions the Regulation does not answer but every model has read about — adequacy decisions by country, the wording of the standard contractual clauses, Schrems II, a CCPA penalty. Retrieval is scored on the cited provision rather than on matching text, which is stricter and removes the chunk-boundary false misses the old substring ground truth suffered. The measured failure is specific and worth naming: the right article, the wrong paragraph. Asked which administrative fine is the maximum, retrieval surfaces Article 83(4) first — the lower tier — with 83(5) third. The answer comes out grounded, because the agent reads all four passages, and the citation shown beside it is still the wrong paragraph, which is the exact failure the project exists to prevent. Sibling paragraphs share vocabulary and sit close together in embedding space, so a bi-encoder blurs the difference between a 10 million ceiling and a 20 million one; reranking is the next move.

Role: Corpus construction, retrieval pipeline, evaluation, deployment

PythonLangChainLangGraphGroqFAISSpgvectorEvalsStreamlitDockerGitHub Actionspytest
06

ragciteThe measurement layer of the two projects above, extracted into a library.

Retrieval metrics, citation grounding, abstention scoring and tool-trajectory scoring, generalized out of the two harnesses above into a standalone package. It imports no LangChain, no vector store and no LLM client: you bring the retriever and, for abstention, the judge, and ragcite scores what they return. That constraint is the design — an eval library that owns your retrieval stack can only measure the stack it owns. Retrieval and grounding need no model at all, so they are deterministic and free to run on every commit, and a --min-hit-at-1 flag turns a run into a CI gate that exits non-zero on a regression. Without a threshold flag it always exits 0: a report, not a gate, by default.

The claim worth checking is that it was extracted from a production harness rather than written to look like one, so the example asserts it. It builds one FAISS index over the real GDPR corpus and scores the same 25 questions twice — once through ragcite, once through the original project's own independent metrics code — and exits non-zero if the two disagree. Sharing the index is the point: any difference can then only come from the scoring rule. They match exactly, hit@1 13/25, recall@k 17/25, MRR 0.59, which are the numbers that project prints for itself. It also refuses to run when the judge model and the model under test are the same, because that is a mistake I made and shipped: my own harness spent weeks marking a model as its own examiner before I noticed.

Role: Library, CLI, evaluation design

PythonEvalsCLIpytestGitHub Actions
07

AI Website Chatbot WidgetAn embeddable assistant that answers for a business 24/7.

A drop-in chat widget for small-business websites. It answers customer questions — menu, hours, location, FAQs — grounded only in the business's own information, so it does not invent details. Reusable for any client by editing a single config file.

Role: Design, full build, deployment

Next.jsReactTypeScriptTailwindGroqVercel
08

Semantic RecommenderRecommendations from embeddings, not hand-written rules.

A recommendation engine built on vector embeddings with a feedback loop that refines results over time. Designed as a reusable backend for content platforms and e-commerce that have outgrown rule-based filters.

Role: End-to-end implementation

PythonEmbeddingspgvectorPostgreSQL

02

Open source

Fixes to other people's codebases, reviewed by their maintainers, mostly in the layer between applications and LLM providers. Each ships a regression test that fails without the fix. Five case studies first, then the six patterns the work falls into.

merged pull requests
429
projects
101
of decided PRs merged
86%
median time to merge
16 h

LiteLLM

Streamed calls billed differently from non-streamed ones

Bug
A streamed Vertex AI pass-through call cost less than the same call non-streamed.
Cause
The stream builder priced the call without its location, so regional endpoints missed their 10% uplift and custom-priced deployments fell back to list price.
Fix
The logger prices both modes, including streams cut off midway, with the deployment's location.
Impact
Spend tracking in a 61k★ proxy now matches across streaming and non-streaming.

LLM Tornado

Three parallel tool calls executed six times

Bug
With Anthropic streaming and Microsoft.Extensions.AI, each parallel tool ran more than once and the follow-up request was rejected.
Cause
Every content_block_stop re-yielded all tool calls seen so far (1, then 1+2, then 1+2+3), and the tool-invocation middleware acted on each.
Fix
Yield the calls once, at the end of the stream, with the final usage.
Impact
Merged the same day; 69 merged in the repo, including streamed input and cache token counts.

Haystack (deepset)

Async paths that drifted from the sync ones

Bug
50 expanded queries meant 50 concurrent embedding calls whatever the limit; an asyncio.Lock was shared across event loops.
Cause
run_async re-implemented run without its guards, and the test suites only called run.
Fix
Restore the limits and per-loop locks, with async tests.
Impact
13 merged across Haystack and its integrations.

LangChain.rb

The provider and tool-calling layer

Bug
tool_choice sent in the wrong shape per API, an agent loop that could never stop calling a forced tool, streamed tool calls lost on Ollama, thinking blocks dropped from the tool-use loop.
Cause
Six provider adapters, each translating tool calls its own way.
Fix
One adapter at a time across Anthropic, OpenAI, Gemini, Ollama, Cohere and Mistral.
Impact
31 merged, most the same day.

AgentScope-Java · go-micro

Default models that stop working on a date

Bug
SDKs defaulting to Claude models that Anthropic retires, or already had.
Cause
Hard-coded model IDs, and in AgentScope a context-window table with no Claude 5 entry, so the obvious one-line swap would have resolved the window to 0.
Fix
New default plus the missing table entry, with a test that fails on the old one.
Impact
Merged the same day in both.

Patterns

When the LLM provider changes, the code built on it breaks

~12 projects · median ~8 hours to merge

Claude 5 and the 1.x Anthropic SDK broke the same few assumptions across about a dozen projects: reading a reply as content[0].text when the first block is now a thinking block, sending a temperature or a forced tool_choice that current models reject with a 400, calling the removed Text Completions API, and model catalogs that did not recognise new or Bedrock model IDs. These merged fastest of anything I sent, because users were hitting them that week.

Async paths in Haystack that drifted from the sync ones

13 merged in Haystack and its integrations

run_async implementations that ran blocking work on the event loop, held an asyncio.Lock across event loops, ignored max_workers, skipped batching or re-wrapped errors the sync path re-raises. These bugs hide from test suites that only call run(). Four of the twelve came from auditing every component's __init__ against to_dict, where a setting silently reverted when a saved pipeline was reloaded.

The provider and tool-calling layer of LangChain.rb

31 merged, usually the same day

Fixes across its Anthropic, OpenAI, Gemini, Ollama, Cohere and Mistral adapters: tool_choice sent in the shape each API expects, an agent loop that could never stop calling a forced tool, streamed tool calls lost on Ollama, thinking blocks dropped from the tool-use loop, a migration to the Cohere v2 API, and tool errors handed back to the LLM instead of ending the run.

One rare but legitimate input that brings down the whole flow

courrier 11 · perron 4 · icons 2 · LangChain.rb

An empty choices list, a nil body, a division by zero, a bare % in an email layout — very often inside a rescue or except written to catch a different exception, so the guard was there and still missed. In the courrier mail gem this also meant three new providers and cc/bcc that six providers had been silently dropping.

Flows that break for real users of a Rails app

14 merged in Sure (personal finance, 10k★)

Sure is a full Rails product, not a library, so the bugs are the ones users click into: provider setup from Add Account that bounced back to Settings or hid its own error, buttons that sent the wrong HTTP method, "Create a bill" from a transaction rendering Content Missing, and merging a pending duplicate that threw away its attachments, tags and notes.

Evaluation metrics that return a wrong number without failing

merged in Opik, Phoenix, pydantic-ai · open in llama-index, deepeval

Retrieval metrics scoring above 1.0 on rankings with repeated ids, pass@k averaged as pass/fail, a Spearman correlation computed on rankings with duplicates, an eval case that could never fail, and a judge grading binary output it received as one decimal byte per line. The work closest to what I build, and the slowest to land: most of it is still in review.

What I learned

Depth beats spread

Going back to a maintainer who already trusts the work pays more than a first PR somewhere new. LangChain.rb went from 6 merged to 31, and LLM Tornado reached 69.

Timing beats volume

A fix for what a release broke this week merges in hours. The same quality of fix for a long-standing edge case can wait weeks.

Make it cheap to review

Small PRs, a regression test I have seen fail with the fix reverted, and the local run pasted in. Where CI does not run for forks, that pasted run is the only evidence a reviewer sees.

all contributions →write-up: the pipeline you load is not the pipeline you saved →write-up: four concurrency bugs on Haystack's async path →

03

About

I started programming on my own, then went through Le Wagon's fullstack bootcamp, did a three-month internship at Oesia building an internal document-management system, and spent some time teaching Ruby to other students.

In 2025 I moved my business from Spain to Toronto on my own: leased premises in Kensington Market, imported the machinery, got every permit, and ran Churrería Calderón with no hires, from production and service to purchasing, cash and compliance, until it closed in July 2026. Running a business alone teaches you which problems are worth solving, and that something has to keep working while you are busy with everything else.

Most of my recent work is the layer between applications and LLM providers: streamed tool calls, token and cost accounting, and the code that breaks when a provider changes or retires a model. I fix it upstream, where everyone gets the fix, with a fleet of coding agents I built and run every day (tanda, above, is its public rewrite), and a reviewer in front of everything it publishes. I treat AI output the way I treat my own: measured, not assumed.

I work fully remote on UTC−4, overlapping with North American hours, from Santo Domingo. Spanish native, professional English.

04

Get in touch

I am looking for a full-time remote engineering role building with LLMs. If that is what you are hiring for, send me a short note — I usually reply within a day.