Skip to main content

Laminar alternative: SQL over traces vs a prompt gateway

· 22 min read
LLM-ops platform

Laminar (lmnr) is a Y Combinator S24 project that calls itself observability purpose-built for AI agents — a Rust ingest server, ClickHouse for spans, Quickwit for search, and an OpenTelemetry SDK that instruments fifteen-plus agent frameworks from one line of code — Vercel AI SDK, Claude Agent SDK, OpenAI Agents SDK, LangChain DeepAgents, Mastra, Pydantic AI, Browser Use, Stagehand, Playwright, LiteLLM, OpenCode and OpenHands among them. It is the most differently shaped tool we have compared AcruxCore against. Its project sidebar has dashboards, traces, evaluations, datasets, labeling, a SQL editor, playgrounds, a debugger and settings — and no prompt registry anywhere. That is not an oversight; it is the product being about agent runs rather than about the calls inside them. Both projects are Apache 2.0, so this is a comparison of shape, not of who is open.

Same example, both sides

Every paired screenshot below comes from the same prompt — vip-support-triage, a support agent that changes tone for VIP customers and lists their open tickets — and the same customer message. Both SDK runs end at the same downstream model, gpt-4o-mini on OpenAI, so the generated text is comparable. Laminar's playground was configured with a direct OpenAI key and run for real, so its screenshot below is a completion rather than an empty state. The latency benchmark uses SDK paths on all three legs, every one of them ending at api.openai.com. Licence, pricing, team structure and community stats live on the compare page rather than here.

At a glance

AspectLaminarAcruxCoreWinner
Tracing an agent runOTel-native, nested spans, tree and transcript views, cost heatmap, 15+ framework integrationsNested OTel spans on an OTLP receiver, five frameworks from one register() call, plus an automatic gateway span carrying prompt-version lineageLaminar, on breadth and views
Where the platform sitsBeside the request path, on purpose — works with any provider, no proxy to configureIn the request path — routing, caching, budgets and virtual keys apply before the providerDepends
Latency overhead, measured−8 to +18 ms across three 100-round runs+7 to +27 ms across the same three runsLaminar, barely
Querying your trace dataReal SQL editor over spans, plus a drag-and-drop custom dashboard builderFixed analytics page, four group_by options, nothing composableLaminar
Prompt authoring and versioningNone — a playground is one mutable row of messages, no versions, aliases or variablesImmutable versions, production/staging aliases, a diff tab, real {% if %} and {% for %}AcruxCore
Sending a live callPlayground runs, but only against seven named providers, and reports tokens without costAny OpenAI-compatible provider, with cost, cache and latency shown inlineAcruxCore
How tools are handledTool schemas are a JSONB field on a playground row; tool calls show up as spansVersioned Tool Catalog that the gateway actually executes, with its own analyticsAcruxCore
Evaluation and datasetsCode-first: your data, your executor, your scorers, run locally or in CIServer-side runs, LLM judges, plus sampled rules that score live production spansDepends
SDK and developer experienceOne line auto-instruments your client; a CLI and skill onboard a coding agentFetch a stored prompt and send it through the gateway with no tracing code, or one register() call to instrument a frameworkDepends

Full breakdown, screenshots, and the verdict below.

Tracing an agent run

This is what Laminar is for, so it goes first. Laminar.initialize() patches the OpenAI client through OpenTelemetry, and an @observe() decorator nests the model call underneath a parent span — so the run's shape lands in the trace, not just the call.

from lmnr import Laminar, observe

Laminar.initialize(project_api_key=os.environ["LMNR_PROJECT_API_KEY"], base_url="http://localhost:8000")

@observe(name="vip_support_triage")
def triage(message: str) -> str:
return client.chat.completions.create(model="gpt-4o-mini", messages=[...]).choices[0].message.content

The span view carries the full system and user messages, token counts, cost, and an "Experiment in playground" button that lifts the call straight into their playground.

Laminar span detail: a tree with vip_support_triage above an openai.chat child, and a right panel showing System, User and Assistant messages with 158 tokens and $0.0001

AcruxCore has two ways in. The gateway writes a span server-side, with no tracing code in the script at all, and links it back to the prompt version that produced it.

AcruxCore trace detail: a single LLM span with token counts, cost, and a link to the prompt version that produced it

For the run's shape there is an OTLP/HTTP receiver — POST /api/v1/traces/otlp, protobuf or JSON, gzip accepted — that takes nested OTel spans from your process, the same way Laminar's does. Both SDKs collapse the provider, processor and exporter wiring into one register() call, which can also switch on a framework's own OpenInference instrumentor:

from acruxcore.otel import register

register(api_key=KEY, base_url=BASE_URL, instrument=["crewai", "openai"])

Five frameworks are named today — OpenAI, OpenAI Agents SDK, CrewAI, LangChain and LlamaIndex — and any other OTel or OpenInference instrumentor can be wired to the same provider by hand. A CrewAI crew traced this way arrives as a real tree: a Crew.kickoff root, one agent span per crew member, with tool and LLM spans nested underneath.

AcruxCore trace tree from a CrewAI crew: a Crew.kickoff chain root, an agent span for Destination Researcher containing an LLM span and three Tavily Search tool spans, then a sibling agent span for Itinerary Planner

So the difference is breadth and reading, not whether the run is visible. Laminar ships three times the integrations and reads them back as a transcript, a cost heatmap and a realtime feed. We ship five named frameworks, a span tree with a latency waterfall, and prompt-version lineage on the gateway span — and unlike theirs, ours exists even when the caller sends no spans at all.

FeatureLaminarAcruxCore
Span sourceYour process, via OpenTelemetryThe gateway server-side, or your process over OTLP
Code needed to get a traceOne initialize() call, plus decorators for structureNone for a gateway call; one register() for a framework
Nested agent structureYes, arbitrary depthYes, arbitrary depth, over the OTLP receiver
Prompt-version lineage on the spanNo prompt registry to link toAutomatic
ViewsTree, transcript, cost heatmap, realtimeSpan tree with a latency waterfall, payloads per span
Framework integrations15+, including Vercel AI SDK, Claude Agent SDK, Browser Use, Stagehand, Mastra, Pydantic AI5 via register(), plus any OTel instrumentor wired by hand

Where the platform sits — in the request path, or beside it

Laminar sits beside the request path. Its Rust app-server receives spans; it never receives your model call. We checked the server's routes: the only /chat/completions handling in the codebase is its outbound client for its own AI features, not an inbound proxy.

That is a real design choice with real advantages. Any provider works immediately, there is no proxy to configure or keep up, and nothing Laminar does can fail your production call. The cost is that a budget, a cache hit or a virtual key has no call to act on — by the time Laminar sees the request, the money is already spent.

AcruxCore takes the other side. The call goes through us first, which is what makes a spend cap enforceable: reserveBudgets() runs before the provider is contacted and returns 402 BUDGET_EXCEEDED, and a per-virtual-key RPM/TPM limit returns 429 RATE_LIMITED on the same pre-call path. The cost is the hop, measured in the next section, and the fact that we have to support your provider.

FeatureLaminarAcruxCore
PositionBeside the request pathIn the request path
Works with any provider, unmodifiedYesOnly providers the gateway supports
Can enforce a spend cap before the callNoYes, 402 BUDGET_EXCEEDED
Can rate-limit a key before the callNoYes, 429 RATE_LIMITED, per virtual key
Can serve a cached responseNoYes
A platform outage breaks your callsNoYes, unless you fail open

Latency overhead — measured

100 rounds per run, interleaved in rotating order so a network blip lands on all three paths equally, three warm-up rounds discarded, every leg ending at gpt-4o-mini on api.openai.com with the same key and the same body. A gateway model is only a public name pointing at a credential, so the script resolves ours before it starts and refuses to run unless it refers to a native OpenAI credential — that way all three legs provably share one upstream.

A single run is not a result here: the median gap moves enough between runs that one number would be misleading. So it ran three times.

RunDirect-call medianLaminar gapAcruxCore gap
1623 ms−8 ms, CI [−27, +6]+7 ms, CI [−12, +22]
2603 ms+18 ms, CI [+2, +46]+27 ms, CI [+11, +50]
3643 ms−4 ms, CI [−28, +15]+7 ms, CI [−19, +29]

Laminar's overhead lands between −8 ms and +18 ms, AcruxCore's between +7 ms and +27 ms. In two of the three runs every confidence interval crosses zero, which means neither path is statistically distinguishable from calling the provider directly at this sample size. Laminar's median gap was the lower of the two in all three runs, by roughly 10 ms — consistent enough to be real, small enough that no application would notice it.

Both are cheap, and they are different kinds of cost: Laminar's is client-side instrumentation that batches spans off the hot path, ours is a real extra network hop that buys routing, budget enforcement, virtual keys and caching.

Full script: latency_bench.py.

Querying your trace data

Laminar's SQL editor is the feature we most wish we had. It is not a filter builder — it is SQL over the ClickHouse span store, with table, JSON and chart output, saved queries, an "Ask AI" button that writes the query for you, and CSV export.

Laminar SQL editor running SELECT name, model, input_tokens, output_tokens, total_cost, duration FROM spans, returning the openai.chat and vip_support_triage rows

The dashboard is composable in the same spirit: drag-and-drop cards, resizable, each one backed by a metric or a custom SQL query. After the benchmark run it had 135 spans to chart.

Laminar dashboards page with twelve resizable cards — top spans, top model cost, latency by model p90, trace status, total tokens and total cost

AcruxCore has neither. Our analytics page groups by exactly one of day, model, session or prompt_version and renders a fixed set of tiles and charts; the state lives in the URL, so you can share a view as a link, but you cannot compose one. There is no way to ask our trace store an arbitrary question.

FeatureLaminarAcruxCore
Ad-hoc SQL over spansYes, with saved queries and CSV exportNo
AI-written queriesYesNo
Custom dashboard panelsYes, drag-and-drop, SQL-backedNo, fixed charts
Grouping optionsAnything SQL expressesday, model, session, prompt_version

Prompt authoring and versioning

Here the shapes diverge completely. Laminar has no prompt registry — we checked the database schema rather than only the sidebar, and there is no prompts table at all. The nearest thing is playgrounds, one row per playground holding prompt_messages as a mutable JSONB blob. Editing it overwrites it. There are no version numbers, no aliases, no diff, and no template variables: their only templating dependency, mustache, is used for rendering span previews, not prompts.

So the fixture prompt had to be flattened by hand for Laminar — the {% if is_vip %} branch resolved to its VIP text, and the {% for ticket in tickets %} loop expanded into a literal list of two tickets. That flattened string is what lm_trace_run.py inlines, because there is nothing on the platform to fetch it from.

On AcruxCore the same prompt is three immutable versions with production on v3 and staging on v2, a unified diff between any two, and the conditionals and loop rendered by nunjucks at request time.

AcruxCore diff tab showing a unified diff between v2 and v3 of vip-support-triage, with the nunjucks conditional and for-loop intact

FeatureLaminarAcruxCore
Prompt registryNoneYes
Version historyNone — the playground row is overwrittenImmutable, numbered
Environment aliasesNoneproduction, staging, promotable
Diff between versionsNoneUnified diff, any two versions
Template logicNone — no variables in prompts at all{% if %}, {% for %}, filters
Runtime "render this stored prompt" callNoneprompts.render(name, alias, variables)

Sending a live call

Laminar's playground stores messages, a model, tools and an output schema, and can be opened directly from a traced span — a genuinely nice loop for debugging a call you just saw. It needs a provider key saved in project settings first, and that page offers a fixed list: OpenAI, Gemini, Groq, Anthropic, Mistral, Azure and Bedrock, with no field for a custom base URL. We confirmed the list in their source (MODEL_PROVIDER_TO_API_KEYS in frontend/lib/env/utils.ts); keys are stored encrypted per project rather than read from the environment.

With an OpenAI key saved, it runs. Below is the flattened prompt returning a real completion on GPT-4o mini. Two things to notice. The messages are typed in flat, because a playground row has no variables — the VIP sentence and the ticket list are pre-rendered by hand, the same flattening described above. And the footer reports input, output and total tokens, but no cost — no latency and no cache status either.

Laminar playground running the flattened vip-support-triage prompt on GPT-4o mini, with the completion in the Output pane and a footer reading Input Tokens 99, Output Tokens 50, Total Tokens 149

The limitation that stands is the provider list: seven named vendors and no custom base URL, so your own gateway, a relay or a local model server cannot be called from it at all.

AcruxCore's playground runs the stored prompt against any model the gateway knows, and shows cost, cache status and latency inline with the response.

AcruxCore playground running the stored vip-support-triage prompt, with a Gateway Telemetry panel showing cost, cache status and latency

FeatureLaminarAcruxCore
Runs a stored promptNo stored prompts to runYes, by name and alias
Provider choiceSeven named providers, no custom base URLAny OpenAI-compatible provider
Variables in the playgroundNone — messages are typed in flat{{ }} filled from a variables panel
Tokens shown inlineYes — input, output, totalYes
Cost shown inlineNoYes
Cache status shown inlineNo cacheYes
Open from a trace spanYes, "Experiment in playground"Re-run from the trace

How tools are handled

Laminar records tool calls as spans and stores a tool schema as a tools JSONB field on a playground row. There is no tool catalog page, no version history for a tool, and nothing in Laminar executes one — your agent does, and Laminar watches. That is consistent with sitting beside the request path.

AcruxCore's Tool Catalog is the opposite: a tool is a first-class, versioned resource that the gateway itself calls, with a page measuring per-tool volume, error rate and P50/P95.

AcruxCore tool analytics showing per-tool call volume, error rate and P50/P95 latency

FeatureLaminarAcruxCore
Tool definitions storedJSONB field on a playground rowFirst-class versioned resource
Tool executed by the platformNoYes
Per-tool analyticsNoYes
Tool calls visible in tracesYes, as spansYes, as spans

Evaluation and datasets

Laminar's evaluations are unopinionated and code-first: you supply the data, an executor function and scorer functions, and it runs them locally or in CI, tracing every call and tracking scores across runs. We ran one over three triage cases.

evaluate(
data=DATA,
executor=executor,
evaluators={"mentions_topic": mentions_topic, "under_five_sentences": under_five_sentences},
name="vip-triage-v1", group_name="triage",
)

The results view puts the per-row scores beside the trace of each datapoint, with the evaluator functions themselves showing up as spans under the executor.

Laminar evaluation results for vip-triage-v1: average scores of 1 for both evaluators, three scored rows, and a trace tree with executor and evaluator spans

Datasets are built from the traces you already have — "Add to dataset" on a span turns it into a Data/Target row with no manual copying.

Laminar dataset vip-triage-cases holding one datapoint, its Data column the system and user messages and its Target column the assistant response, both taken from the span

AcruxCore runs evaluations server-side instead — datasets, LLM judges and experiment runs from the dashboard, plus rules that score live production spans automatically. A rule is plain-English criteria plus a judge model, matched against incoming llm spans by prompt, alias, model or tags, sampled (10% by default) and capped at a daily limit, producing a 0–100 score with a reason and emailing owners when a score drops below a threshold.

The honest difference: Laminar's scorers are arbitrary code and can assert anything, but somebody has to run them. Ours run themselves against real traffic, but they are LLM judges producing a sampled numeric score, not arbitrary assertions.

FeatureLaminarAcruxCore
Where an eval runsYour machine or CI, via SDK/CLIServer-side, from the dashboard
Scorer definitionAny Python functionLLM judge with plain-English criteria
Define an eval in the UINoYes
Scoring live production trafficSignals, separatelyYes, sampled rules with alerts
Dataset from a traceYes, one click from a spanYes
Compare two runsYes, side by sideYes, run history

SDK and developer experience

Laminar's onboarding is the most coding-agent-first we have seen. The empty traces page hands you a prompt to paste into your coding agent; npx lmnr-cli setup authenticates, writes a project key to .env and installs a "Laminar skill"; and the CLI can query your traces in SQL so the agent can verify its own instrumentation.

Laminar onboarding: a grid of integrations including Vercel AI SDK, Claude Agent SDK, OpenAI Agents SDK, Browser Use and Stagehand, beside a "Get started in one prompt" panel for a coding agent

AcruxCore's SDK is aimed at a different job — the prompt lives on the platform, so the script fetches and fills it, sends it through the gateway, and never mentions tracing:

async with AcruxCore(api_key=KEY, base_url=BASE_URL) as hub:
rendered = await hub.prompts.render("vip-support-triage", "production", VARIABLES)
result = await hub.gateway.chat(rendered.model, rendered.messages, prompt_version_id=rendered.version_id)

Both scripts are committed and were run before this post linked them: lm_trace_run.py and acx_sdk_run.py.

FeatureLaminarAcruxCore
Lines to start tracingOne initialize()Zero for a gateway call, one register() for a framework
Framework auto-instrumentation15+ frameworks5 — OpenAI, OpenAI Agents SDK, CrewAI, LangChain, LlamaIndex
CLIYes, including SQL over tracesNo
MCP access for a coding agentYesNot shipped today
Fetch a stored prompt at runtimeNo prompts to fetchYes

What Laminar does that AcruxCore doesn't

The two biggest ones — SQL over spans and the custom dashboard builder — have their own section above. Four more, each checked against our own codebase before being written down.

Labeling queues. A span can be pushed into a named queue where a reviewer works items one at a time against a defined annotation schema, then pushes the labelled result to a dataset. AcruxCore has trace feedback — a human can score a trace from −1 to 5 with a label and comment — but there is no queue: no assignment to a reviewer, no next-unlabelled-item workflow, no rubric, no completion tracking.

Laminar labeling queue triage-quality-review with one item, showing the Data panel from the span, a Target panel and an annotation schema

Signals. You describe a behaviour in plain English, give it a structured output schema, and an LLM watches traces for it — with templates for failure, logic, task, user friction, hallucination and intent, a trigger on trace or span completion, filters, and sampling. Matching events are grouped into clusters and can page you in Slack.

Laminar's Create Signal panel: name, six behaviour templates, a prompt, a structured output schema, trigger options, filters and a sampling toggle

This one needs a caveat we will not soften. AcruxCore's online evaluation rules cover much of the same ground — plain-English criteria, an LLM judge, automatic matching against live spans, sampling, daily caps and email alerts — so this is not a capability we lack. What we lack is the event-and-cluster model on top: their output is a structured event that gets clustered into behavioural patterns, ours is a 0–100 score with a reason. Also worth saying plainly: on the lite self-hosted stack, after enabling Signals and running a fresh trace through it, no event was produced within our observation window. The builder is real and we screenshotted it; the end-to-end result we could not confirm.

The debugger. LMNR_DEBUG=1 lets a coding agent run your agent, read the resulting trace, change the code and re-run with cached state — the iteration loop owned by the agent rather than by you. AcruxCore has nothing equivalent, and our own MCP server is unmerged and unpublished, so today the answer is simply no.

Laminar debugger sessions page explaining the LMNR_DEBUG=1 loop where a coding agent runs, inspects and re-runs your agent with cached state

PII redaction. A project-level toggle runs every ingested span through a PII redactor before storage, replacing detected names, emails and phone numbers. AcruxCore always scrubs a fixed set of secret patterns — API keys, bearer tokens, AWS access keys and email addresses — and has a team-level switch to stop capturing payloads at all, but nothing that detects names, phone numbers or addresses.

Two more worth naming without a screenshot, because they are structural: Laminar delivers alerts to Slack, while AcruxCore's only notification channel is email; and Laminar records browser-agent sessions for Browser Use, Stagehand and Playwright, which we have no answer for at all. Our "Sessions" means traces grouped by a caller-supplied session id — a conversation thread, not a recording.

Is AcruxCore a Laminar alternative?

For part of what Laminar does, no — and that is the most useful thing this comparison found. Both of us take nested OTel spans, so seeing an agent run is not the dividing line. Digging into one is: SQL over your own span store, a composable dashboard, labeling queues, a transcript view, and three times the framework integrations. We do not replace that depth, and this post does not pretend otherwise.

Where we are a genuine alternative is everything that happens before the call. Laminar has no prompt registry, no request path, and therefore no way to enforce a spend cap or serve a cached response. If those are the things going wrong for you, that is the swap worth making.

They also compose, which is the answer most people actually want: point your client at our gateway and keep Laminar's SDK instrumenting the run. Nothing about the two conflicts.

Verdict

LaminarAcruxCore
Strongest atDigging into an agent run — SQL, dashboards, labeling, transcript viewsManaging the prompts and calls that produce the run
Weakest atAnything before the call — no prompt registry, no request path, no spend control — and a playground that reaches only seven named providers, with no custom base URLAsking arbitrary questions of your own trace data
Pick it ifYou are shipping an agent on a framework, and your hard problem is understanding its behaviourYour prompts change often, several people edit them, and cost or access needs a control point

These two tools barely overlap, which is the most useful thing this comparison found. Laminar starts at the trace and builds upward — SQL, dashboards, labeling, signals, a debugger — and it beats us clearly on querying trace data, and on the breadth and reading of traces — both platforms take nested OTel spans, but they have three times the integrations and far more ways to read them back. On measured latency the two are close — both sit within tens of milliseconds of a direct call, with Laminar consistently the lower of the two by about 10 ms. AcruxCore starts at the prompt and the request — versions, aliases, templates, a gateway that can refuse a call that would blow a budget — and Laminar has no answer for any of that, by design.

If you already run Laminar and your prompts are drifting across a team, the two compose fine: point your client at our gateway, keep Laminar's SDK instrumenting the run.

Licence, pricing, team model, security and community stats for both — with a source link and a checked-on date for every fact — are on the compare page.

Start here if you want to try the other side: Quickstart.