AI agents
LLM-provenance-audit: 14 tekenen van een merged checkpoint
Het is juni 2026. Je hebt acht weken voordat artikel 50 van de EU AI Act van toepassing wordt, en het bestand dat je leverancier afleverde als eigen fine-tune ruikt verdacht.

Het is een dinsdagochtend in juni. Je bent CTO bij een Nederlandse fintech, en artikel 50 van de EU AI Act gaat over acht weken in. Je leverancier heeft net het bestand opgeleverd waar je zestigduizend euro voor betaalde: de "eigen fine-tune van ons interne 12B foundation model" die de salesdeck beloofde. Je pakt de zip uit. Je opent config.json. Vijf seconden later heb je het antwoord, en het is niet het antwoord dat je wilde.
Dit is een veldgids voor de veertien manieren waarop een self-hosted LLM-provenance-audit uit elkaar valt zodra het "eigen" model eigenlijk een stille merge blijkt van twee checkpoints die iemand van Hugging Face plukte. We ordenen ze op de moeite die ze je kosten. De eerste zeven vang je met een teksteditor. De volgende drie vragen om een diff tegen een upstream repository. De laatste vier kosten een GPU en een middag.
Waarom deze audit nu jouw probleem is
Tot voor kort was "waar komen de gewichten vandaan" een vraag voor de nieuwsgierigen. Artikel 50 van Verordening 2024/1689 verandert dat. Als deployer van een general-purpose AI-systeem ben je specifieke transparantie verschuldigd aan de mensen die ermee werken, en die kun je niet eerlijk afgeven zonder te weten wat er in de doos zit. De aanbiedersverplichtingen onder de artikelen 53 en 55 zijn nog strenger, en de GPAI Code of Practice trekt de keten van vastlegging door tot aan de samenvatting van de trainingsdata.
Het risico is niet theoretisch. Procurement-teams die "we hebben hem zelf getraind" vroeger op vertrouwen aannamen, hebben nu tools die de leverancier in minuten tegenspreken. Hetzelfde open ecosysteem dat model-merging triviaal maakte, maakte detectie ook goedkoop: iedereen met het bestand, een teksteditor en een weekend kan controleren of de disclosure klopt met de gewichten. De forensische methodes zijn openbaar. Als het bestand de marketing tegenspreekt, vallen de artikel 50-verplichtingen op jou, niet op de integrator die hem opleverde.
Het goede nieuws: fakes laten vingerafdrukken achter. Hier zijn er veertien.
De config.json-laag: zeven signalen, vijf minuten per stuk
Open het bestand. Elk Hugging Face-vormig model heeft een config.json in de root, en checkpoint-vervalsingen lekken erdoorheen als een nat dak.
Het _name_or_path-restje. Het meest voorkomende slipje. Wanneer je een model opslaat na from_pretrained() schrijft de transformers library het originele repo-ID in dit veld, tenzij iemand het schoonveegt. We hebben afgelopen kwartaal vier leveranciersmodellen geaudit waar dit veld nog gewoon op meta-llama/Llama-3.1-8B-Instruct of mistralai/Mistral-7B-v0.3 stond. Einde audit.
De architectuur-signatuur. architectures is een array met één entry, en die entry is de classname van een gepubliceerde familie: LlamaForCausalLM, Qwen2ForCausalLM, MistralForCausalLM. Een echt eigen architectuur zou ofwel subclassen onder een nieuwe naam, ofwel een auto_map meedragen die naar leverancier-code in de repo wijst. Zie je geen van beide, dan is de claim "eigen architectuur" een leugen.
De hyperparameter-vorm. hidden_size, intermediate_size, num_attention_heads, num_key_value_heads en num_hidden_layers vormen samen een vingerafdruk. Vergelijk ze met de tien grootste open releases op dat parameteraantal. Matchen alle vijf byte voor byte met een publiek model, dan heeft de leverancier niet gepretraind. Hij heeft gedownload.
diff <(jq -S '.architectures, .hidden_size, .intermediate_size, .num_attention_heads, .num_key_value_heads, .num_hidden_layers' vendor/config.json) \
<(jq -S '.architectures, .hidden_size, .intermediate_size, .num_attention_heads, .num_key_value_heads, .num_hidden_layers' upstream/config.json)
Het RoPE-vermoeden. rope_theta en rope_scaling worden alleen bijgesteld wanneer iemand het contextvenster daadwerkelijk heeft verlengd. Claimt de leverancier een uitgebreid contextvenster, maar staat rope_theta nog op 500000.0 en rope_scaling op null, dan bestaat die contextuitbreiding alleen in de marketing-PDF.
De vocab_size die niet groeide. Echte fine-tunes voegen tokens toe. Medische modellen voegen Latijnse stammen toe, juridische modellen citatietokens, en Nederlandse fine-tunes diakrieten en samengestelde morfemen die een standaard-BPE in stukken hakt. Een vocab_size identiek aan het basismodel, plus een tokenizer met dezelfde special-token-IDs, is een sterk signaal dat er boven de embedding-laag niets is gebeurd.
De transformers_version-blooper. Als een leverancier een eigen trainingsstack claimt, hoort het opgeslagen transformers_version-veld inconsistent of afwezig te zijn. Staat er 4.44.2, precies overeenkomend met de releasedatum van upstream, dan is het model waarschijnlijk geladen, lichtjes aangeraakt en opnieuw opgeslagen via de gewone transformers library.
Het kwantisatie-verhaal. De README claimt AWQ voor inference. quantization_config ontbreekt. torch_dtype staat op bfloat16. Het verhaal overleeft het bestand niet.
Geen van deze signalen is op zichzelf doorslaggevend. Een vermoeide engineer bij een echt lab kan _name_or_path per ongeluk laten staan na legitiem doorgaan met pretraining. Het gaat om het cluster. Drie of meer signalen uit deze lijst en je kijkt vrijwel zeker naar een dun jasje rond andermans gewichten.
De tokenizer-laag: drie signalen die om een diff vragen
De tokenizer is het onderdeel dat leveranciers het vaakst vergeten te verkleden, omdat ze ervan uitgaan dat niemand kijkt. Download de upstream-tokenizer en vergelijk.
De SHA-256 van tokenizer.json. Een echte fine-tune die tokens toevoegt of de chat template aanpast, verandert deze hash. Hasht de tokenizer.json van de leverancier precies naar de upstream-waarde, dan is de tokenizer onaangeraakt gebleven. De combinatie "onaangeraakte tokenizer plus fine-tuning-claim" is denkbaar, maar moet dan in de disclosure staan.
sha256sum vendor/tokenizer.json upstream/tokenizer.json
De BPE-merge-volgorde. Open beide tokenizer.json-bestanden, scroll naar model.merges en vergelijk de eerste vijftig entries. Die volgorde is deterministisch ten opzichte van de corpus waarop de BPE getraind is. Identieke volgorde over de eerste paar honderd entries betekent identieke trainingsdata, wat betekent dat de leverancier de upstream-BPE gebruikte en als zijn eigen verkocht.
Het special-token-ID-blok. tokenizer_config.json bevat bos_token, eos_token, pad_token en eventuele chat-template-tokens met hun IDs. Matchen de IDs woord voor woord met upstream en is de chat template de upstream Jinja-string met één komma verschil, dan heeft de leverancier "instruction-getuned" door een string te bewerken.
De gewichten-laag: vier signalen die een GPU en een zondag kosten
Laten de config- en tokenizer-laag nog twijfel toe, dan ga je naar de gewichten zelf. Hier leeft de merge-forensiek.
Cosinus-gelijkenis van embeddings. Laad het vendormodel en een kandidaat-basismodel. Bereken voor elk token in het vocabulair de cosinus-gelijkenis tussen de twee embedding-vectoren. Een echte fine-tune trekt de verdeling omlaag naar een lange staart tussen 0,85 en 0,95. Een gemergede checkpoint-vervalsing laat een strakke piek boven 0,999 zien, omdat geen enkele gradient ooit de embedding-tabel raakte.
import torch
from transformers import AutoModelForCausalLM
vendor = AutoModelForCausalLM.from_pretrained("./vendor", torch_dtype=torch.float32)
upstream = AutoModelForCausalLM.from_pretrained("upstream/model-id", torch_dtype=torch.float32)
v = vendor.get_input_embeddings().weight
u = upstream.get_input_embeddings().weight
cos = torch.nn.functional.cosine_similarity(v, u, dim=-1)
print(cos.mean().item(), cos.median().item(), (cos > 0.999).float().mean().item())
De bimodale layer-delta. Bereken het elementgewijze verschil tussen de leveranciersgewichten en twee verdachte upstream-bronnen. Plot de norm per laag. Een schone fine-tune levert een vloeiende gradiënt op. Een SLERP- of TIES-merge uit mergekit laat een karakteristiek twee-cluster-patroon zien, waarbij lagen rond de embedding en de head naar de ene ouder leunen en de middelste lagen naar de andere. Heb je die vorm eenmaal gezien, dan herken je 'm meteen.
Verdeling van layer-norms. De gamma- en beta-vectoren in RMSNorm en LayerNorm zijn klein, statistisch rijk en worden vrijwel nooit aangepast door lichte fine-tuning. Draai een Kolmogorov-Smirnov-test tussen vendor en kandidaat-basis. Komt p > 0.5 uit op elke laag, dan zijn de norms gekopieerd. Echte fine-tuning verschuift ze, al is het minimaal.
Reproduceerbaarheid van generatie. Fixeer een seed. Decodeer greedy vijftig vakprompts op het vendormodel en op de kandidaat-basis. Matchen de output-token-IDs op meer dan dertig van de vijftig, dan is het vendormodel functioneel hetzelfde als de basis. Dit is de test die overeind blijft tegenover een toezichthouder, omdat ze reproduceerbaar is uit het bestand alleen.
Hoe een echte merge er in de praktijk uitziet
Mergen is op zichzelf geen probleem. Het is legitiem onderzoek, en uitstekende open modellen zoals de SLERP-gemergede Marcoro14 worden zo gebouwd. Het probleem is mergen zonder disclosure, vooral wanneer het wordt verkocht als eigen fine-tuning. Openbare recepten laten restsignaturen achter: mergekit publiceert de YAML waarmee deze artefacten ontstaan, en de open source community publiceert de technieken om ze te herkennen. Beide kanten van de audit zijn openbaar, wat betekent dat elke gemotiveerde CTO ze beide kan draaien voordat de AI Act-papierwinkel binnen moet zijn.
Provenance is een keten van bestanden, niet het woord van een leverancier. Zeggen config.json, tokenizer.json en de embedding-matrix samen "Llama-3.1-8B met de serienummers eraf gevijld", dan begint daar de disclosure die je onder artikel 50 verschuldigd bent.
De audit die je nog voor de lunch draait
Open het bestand. Draai jq op config.json en loop de zeven signalen hierboven na. Draai sha256sum over de tokenizer-bestanden tegen de drie meest waarschijnlijke publieke basismodellen. Is er iets verdachts, plan de GPU-job voor het weekend. Je weet binnen een uur meer over het model dan de leverancier je in drie maanden vertelde.
Toen we afgelopen winter de documentclassificatie-agent bouwden voor een Rotterdamse logistieke groep, bleek het "fine-tuned" model van hun vorige integrator een doodgewone Qwen2 te zijn met een system prompt en een hernoemde map. We hebben hem opnieuw opgebouwd als eerlijke in-house AI-agents met een gedocumenteerd trainingsspoor, omdat de artikel 50-papierwinkel moet kloppen met het bestand, en het bestand met de waarheid.
Kern
Matchen config.json, tokenizer.json en de embeddings allemaal met een publieke basis, dan begint daar je artikel 50-disclosure, niet bij de marketing-PDF van de leverancier.
FAQ
Wat eist artikel 50 van de EU AI Act eigenlijk?
Artikel 50 legt transparantieverplichtingen op aan aanbieders en deployers van bepaalde AI-systemen, waaronder openbaarmaking wanneer iemand met AI praat en wanneer content door AI is gegenereerd. De belangrijkste bepalingen worden van toepassing vanaf augustus 2026.
Is het mergen van twee open checkpoints illegaal?
Nee. Model-merging is een legitieme onderzoekstechniek. Het probleem zit in een gemerged model verkopen als eigen fine-tuning zonder disclosure, wat de transparantieverplichtingen onder artikel 50 en de meeste inkoopcontracten breekt.
Kan ik deze audit ook zonder GPU draaien?
De eerste tien signalen vragen alleen om jq, sha256sum en een teksteditor. De laatste vier signalen op gewichtenniveau hebben wel een GPU en een paar uur nodig, maar dat zijn ook de doorslaggevende signalen als het bewijs op config- en tokenizer-niveau dubbelzinnig blijft.
Wat als de leverancier weigert config.json te delen?
Dan is dat het auditresultaat. Een leverancier die de metadata van het bestand niet wil laten zien, kan ook niet aan de transparantievereisten van artikel 50 voldoen, en de inkoopbeslissing volgt al uit dat ene feit.