DEV Community

Peter Abolins
Peter Abolins

Posted on

What /analyze actually returns for Romanian — and what it gets wrong

If you need Romanian morphology in production — lemma, part of speech, case, number, gender, person, tense, for arbitrary text — the first decision is not which tool to use, but what kind of answer you want back.

A dictionary lookup is fast and predictable and, by construction, cannot use context: the same surface form gets the same answer wherever it appears. A hand-rolled rule set works until your corpus changes shape. A general-purpose model does use context, and answers as an oracle — you get a result, not a record of how it was reached, and no way to tell a lookup from a guess.

LexicRo is a fourth shape: a hosted HTTP contract you can call without shipping a model yourself, that stamps the model version it answered with, and that tells you, per token, whether the answer came from a lexicon or from a prediction.

The thing context buys you

Two Romanian sentences containing the identical word form sare:

Pune sare în mâncare.   →  "Put salt in the food."
Pisica sare pe masă.    →  "The cat jumps on the table."
Enter fullscreen mode Exit fullscreen mode

/analyze returns lemma sare, UPOS NOUN for the first, and lemma sări, UPOS VERB for the second. You do not need any Romanian to check that: the two lemmas are visibly different words. A dictionary lists both readings; only the sentence says which one you are looking at.

The same holds for port. In un port mare — "a large harbour" — it is the noun port. In Eu port o cămașă — "I wear a shirt" — it is the verb purta.

Every token says where its answer came from

For each token, /analyze returns lemma, UPOS, Universal Features, and a source field carrying exactly one of three values: lexicon, suffix, model. A lexicon hit, a suffix rule, or a prediction from the model.

source is provenance, not confidence. The guide says so in its own words: "source is not a trust signal." A model answer is not less trustworthy than a lexicon one — it is a different kind of fact, arrived at a different way. When a lemma comes back wrong, the first question you have is which of those three produced it, and here that is a field to read rather than an investigation to run.

The readings it did not pick

Where the lexicon knows more than one reading for a form, the response does not quietly pick one and drop the rest. It returns the competing readings in candidates, alongside the one the model chose. About 35.68% of tokens in running text carry a candidates list.

If your pipeline needs to know a token was ambiguous — to flag it, to route it to a human — you can read that off the response instead of inferring it.

What you are pinned to

Every response stamps model_version. The weights, the lexicon snapshot and the MSD→UD conversion table version together as one unit, and the engine itself is pinned by tag. So when you pin against a model_version, you know exactly what you are pinned to, and a change to any of the three artefacts is a change to the version string you are reading.

That is the mechanism, stated as a mechanism: it lets you build against a stated version rather than against whatever answered you today.

The second endpoint, and what it refuses to do

GET /conjugate/{verb} returns a verb's conjugation table: seven moods, condițional included. The conditional is one LexicRo synthesises rather than one the upstream library ships, and the response says so — those forms carry source: "derived".

Conjugation itself comes from verbecc, an open-source library that does a great deal of work across several languages. It also contains a contradiction worth walking through, because what the API does about it is the same argument as everything above.

Take a ninge, "to snow". Its indicativ prezent correctly marks every person absent except the third singular — nobody snows. Its compound tenses then supply forms for all of them anyway, so the same paradigm offers eu am nins: "I have snowed."

Seventeen Romanian templates in verbecc have that shape, covering 267 lemmas out of roughly 6,864: the present marks five of six persons absent while the compound tenses supply forms for exactly those persons. It is filed upstream as verbecc#53.

The obvious fix is to mirror the present into the compound tenses and suppress the impossible forms. I checked five of the affected verbs before implementing that, and it went both ways. The 17 templates are not one kind of verb. For a ninge the compound tenses are the wrong side. But a aprova, a aporta, a abeceda and a agrega are ordinary transitive verbs that have simply been marked third-person-only — for those, the present is the wrong side, and suppressing the compound forms would delete correct ones. Their two largest templates cover 146 of the 267 lemmas between them: the size of the population that needs looking at, not a count of confirmed errors. Five verbs is all anyone has triaged.

So /conjugate does not adjudicate. It reports the contradiction and names the persons affected, in a note that reads:

This verb's present tense marks these persons as non-existent, while its compound tenses supply forms for them. One of the two is wrong, and this response cannot tell you which.

An API that corrected the paradigm quietly would be making a claim about everything it left uncorrected, and that claim would be false.

Three things I would rather you heard from me first

Skewed ambiguities collapse to the frequent reading. The disambiguation above is not uniform. The form era was measured across five contexts on 2026-08-21 and resolved to the lemma fi — the verb — in five of five, including one where a finite verb is syntactically impossible. That is a real limitation, and it is the case candidates exists for.

The accuracy figures are gold-tokenisation figures. On the UD Romanian RRT test split — 16,311 tokens, gold tokenisation — /analyze scores 98.14% UPOS, 98.43% FEATS-F1, 95.50% lemma and 93.31% all three correct on the same token. That 95.50% is the full pipeline, not the model on its own: the guide slices it by dictionary coverage, and lemma accuracy is 96.34% for tokens the lexicon knows against 93.25% for tokens it does not. End-to-end accuracy on raw text is a different quantity, and it is not measured. And this is a tagger, not a parser: it returns no syntax tree.

Three endpoints my older posts list are gone. /lookup, /inflect and /difficulty went offline on 2026-08-17 and have not come back. All three read third-party dictionary data whose terms of use I had not settled, and the surface is deliberately two endpoints until that is resolved.

Which brings me to those older posts, because this one is about provenance and it would be poor form to overstate my own. There are two: April 2026 and May 2026.

Both carried claims I would not make today: client libraries that do not exist, a licence for the model weights that had been withdrawn and never replaced, "self-hostable" attached to a project whose weights are not distributed, a latency comparison against LLMs in which neither number had ever been measured, and the three endpoints above presented as live months after they went.

In August I posted correction comments under both, and that was not enough. A comment is annotation, not correction. The bodies still said what they said, and someone reading a post does not necessarily read the comments beneath it. Those comments had aged badly on their own account, too: both explained the withdrawn endpoints as awaiting a permission request that had in fact been withdrawn the same day the comments went up.

On 2026-08-30 I edited both bodies and deleted the comments. The claims above are gone from the posts themselves, the withdrawn endpoints are named as withdrawn with the date they went, and what each post now says about versioning matches what this one says. The correction lives in the thing that was wrong rather than underneath it.

I am spelling this out because a post arguing that provenance should be checkable, which quietly tidied its own history, would be making the opposite case.

Try it before you sign up for anything

The demo is at demo.lexicro.com. It puts the two readings side by side and shows the response behind each, candidates included, and its second tab does the same for /conjugate, note included. No key, and no form in front of it.

When you want to call the API yourself, the free tier is real and permanent, not a trial: 1,000 requests/day with a key, 10/day anonymous. The full request and response shapes, the rate limits and the known limitations are in the guides — api.lexicro.com/guide for /analyze, and api.lexicro.com/guide/conjugate for /conjugate.

If you find a token it gets wrong, send me the sentence — a wrong answer with its context attached is the most useful thing anyone can send me.

Top comments (0)