Skip to main content

LLMUID for Python

Identifiers that resist hallucination, survive repeated LLM copying, and repair themselves when damaged — or fail honestly when they can't.

The Python implementation of LLMUID, an identifier scheme for systems where identifiers must pass through large language models — read, copied and re-emitted across many prompt hops.

K7-M3-XR-9D-Q2

Ten symbols over a 29-symbol alphabet of digits and consonants, eight of them a random payload and two of them check symbols, written as five groups of two. No vowels, so an identifier can never spell a word. No lookalikes, so it can never be misread across ambiguous glyphs.

Install

pip install llmuid

Requires Python 3.10 or later. No dependencies.

Use

from llmuid import LLMUID

r = LLMUID()

identifier = r.mint()                 # K7-M3-XR-9D-Q2

Reading is liberal. Case, delimiters and wrapping carry no information, and any single damage event is repaired silently:

r.resolve('K7-M3-XR-9D-Q2')           # K7-M3-XR-9D-Q2, pristine
r.resolve('`k7 m3 xr 9d q2`')         # K7-M3-XR-9D-Q2, delimiters and case
r.resolve('K7-M3-XB-9D-Q2')           # K7-M3-XR-9D-Q2, one substitution
r.resolve('K7-M3-RX-9D-Q2')           # K7-M3-XR-9D-Q2, one transposition

Anything further away is a hard failure, never a guess:

r.resolve('K7-M3-ZZ-ZZ-Q2')           # None
r.last_error()                        # 'Checksum failed and no issued
                                      #  identifier is within 2 edits'

Context binding

Check symbols can be bound to the slot, role or parent an identifier belongs to, so a genuine identifier pasted into the wrong place fails to resolve. The same context string must be given to mint() and to resolve().

identifier = r.mint('invoice')

r.resolve(identifier, 'invoice')      # the identifier
r.resolve(identifier, 'receipt')      # None
r.last_error()                        # 'Wrong context: ... was not issued
                                      #  under this context'

This is the defence against the most dangerous failure of all — a well-formed identifier in the wrong role, which nothing about the string itself can catch.

Persisting the registry

The registry is in-memory and append-only: it lives in the object and dies with it. registry() hands the issued set back so a caller can persist it, and the constructor takes that same list back.

issued = r.registry()                 # list of canonical renderings

r = LLMUID(issued)                    # same registry, new process

API

mint(context: str = '') -> str | None
resolve(llmuid: str, context: str = '') -> str | None
registry() -> list[str]
last_error() -> str | None
self_test() -> bool

Nothing raises. Failure returns None and explains itself through last_error(), which returns None when there is nothing to explain. The one call that can raise — the system random source — is caught and converted into a failed mint like any other.

last_error() is a method rather than a property, so that it reads the same here as in every other implementation of the scheme.

The wording of last_error() separates the two things worth watching: a repair means the channel is degrading, while a failure means the pipeline is faulty, since honest noise almost never produces multi-event damage.

Conformance

src/llmuid/vectors/ is a copy of the conformance vectors from the specification repository: 134 cases pinning the context digest, liberal reading, the bounded distance and the damage contract end to end. self_test() grades this class against every one of them, and then against the invariants minting is answerable for — which are random by design, so no fixed case can pin them.

r.self_test()                         # True
r.last_error()                        # the first failing case, if not

It reads the vectors from the installed package, mints only into throwaway objects, and leaves the registry of the object it is called on untouched.

The vectors are frozen and they are the answer key. A failure means this implementation has drifted from the specification — never that a vector needs updating.

resolve() is not an extractor

Normalization is deliberately liberal, which means it will happily eat the prose around an identifier as well:

r.resolve('see invoice K7-M3-XR-9D-Q2 today')   # None, too long

Extract identifiers from surrounding text yourself — they match r'\b[0-9BCDFGHJKMNPQRSTVWXZ]{2}(?:-[0-9BCDFGHJKMNPQRSTVWXZ]{2}){4}\b' in canonical rendering — and hand resolve() one candidate at a time.

Not a security mechanism

The random payload makes identifiers statistically unguessable, but not cryptographically so, and the check symbols are public arithmetic anyone can compute. Identifiers must never be used as secrets, capabilities or bearer tokens, and possession of a valid identifier must never grant authority. The adversary in this design is a hallucinating model, not an attacker.

Development

There is nothing to install — no dependencies, dev or otherwise, and one self-contained class. There are two checks, and the class carries the second one itself rather than the tree carrying a test framework to run it:

python3 -m py_compile src/llmuid/llmuid.py

python3 -c 'import sys; sys.path.insert(0, "src"); from llmuid import LLMUID
r = LLMUID(); print("pass" if r.self_test() else r.last_error())'

The specification wins

llmuid.md is the design document and is authoritative; this code implements it without variation. If the two disagree, this code is wrong.

It was written from the specification and graded against the vectors rather than translated from the PHP implementation, which is one rendering of the same document and not a second source of truth. Three places are where a port lands somewhere plausible and wrong, and the vectors are aimed at each: the context digest is read big-endian, an adjacent transposition costs one operation and not the two a stock Levenshtein routine charges, and case folding is restricted to the spellings the alphabet lists — str.upper() would turn U+017F into an S that is in the alphabet and fail a pristine identifier on length.

License

MIT — see LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

llmuid-1.0.0.tar.gz (24.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

llmuid-1.0.0-py3-none-any.whl (27.8 kB view details)

Uploaded Python 3

File details

Details for the file llmuid-1.0.0.tar.gz.

File metadata

  • Download URL: llmuid-1.0.0.tar.gz
  • Upload date:
  • Size: 24.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for llmuid-1.0.0.tar.gz
Algorithm Hash digest
SHA256 2d02edf9e66009747d01028c41ee5bcaef0bf1b8c292ff01d0091939cb7e4e30
MD5 47938cc9cea24efd32906146cc297d8d
BLAKE2b-256 1e98494e5a1f1f31f8d35d41738652e5f71bb1908135ae258192d5e3d47eef18

See more details on using hashes here.

File details

Details for the file llmuid-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: llmuid-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 27.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for llmuid-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ca6e121ba03d899eac45b64ac51b43baabdb861ca7cb114c56c6d06ce022c060
MD5 385ca094d50b1c42966d5d79316b9e9a
BLAKE2b-256 b8d5783fbaede75b500cec547836b84dda856367a30c159c339f333c23c1305f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page