Allow isolated prompt probes with JSON mode and local schema validation

This commit is contained in:
Nico committed 2026-10-17 13:01:03 +02:00
1 parent f52af70646
commit e8d0602ed3
4 files changed
+78 -38

No files matched your search

+46 -28
View File
@@ -4,45 +4,45 @@
.\.venv\Scripts\python.exe -m viano_bug_analysis.prompt_probe --run
```
Ohne --run erfolgt nur eine lokale Prüfung. Standardinput ist
Ohne --run erfolgt nur eine lokale Prüfung. Standardinput ist
service-data/benchmarks/compact-investigation-20261017/request.json.
Standardoutput ist result.json im selben Ordner. Ein vorhandenes Ergebnis
verhindert unbeabsichtigte Wiederholungen; für eine bewusst neue Messung
verhindert unbeabsichtigte Wiederholungen; für eine bewusst neue Messung
--output mit einem neuen Dateinamen verwenden.
Genau ein nativer Gemini-Aufruf, keine Wiederholung, kein Failover, kein
expliziter Cache und keinerlei Ausführung der vorgeschlagenen Recherche.
expliziter Cache und keinerlei Ausführung der vorgeschlagenen Recherche.
Das vorhandene Gateway-Konfigurationsprofil bestimmt Modell, Denkstufe,
Ausgabelimit und Timeout. Produktionsprompts bleiben unverändert.
Ausgabelimit und Timeout. Produktionsprompts bleiben unverändert.
Instruktionen und Kontext werden als System-/User-Nachricht übertragen;
das Ausgabeschema wird ausschließlich im nativen generationConfig gesetzt.
Instruktionen und Kontext werden als System-/User-Nachricht übertragen;
das Ausgabeschema wird ausschließlich im nativen generationConfig gesetzt.
Erfasst werden Eingabe-, Kandidaten-, Denk-, Cache- und Gesamttokens sowie
Laufzeit, Modellversion, JSON-Schemavalidität und der Hash der vorbereiteten
Anfrage. Eine passende JSON-Struktur allein beweist keine fachliche Qualität.
Laufzeit, Modellversion, JSON-Schemavalidität und der Hash der vorbereiteten
Anfrage. Eine passende JSON-Struktur allein beweist keine fachliche Qualität.
Der vorbereitete Prompt umfasst 8.396 Nachrichtenzeichen plus 2.486 Zeichen
minifiziertes Ausgabeschema, zusammen 10.882. Das sind etwa 29 mal 375 Zeichen
(des englischen Minimaltests). Formatierung der lesbaren PROMPT.txt zählt
nicht als zusätzlicher API-Input. Keine Tokenschätzung ersetzt die API-Messung.
(des englischen Minimaltests). Formatierung der lesbaren PROMPT.txt zählt
nicht als zusätzlicher API-Input. Keine Tokenschätzung ersetzt die API-Messung.
Testgrundlage ist der erste H1-Kontext von Bug 20 vor jeder Investigation-
Recherche; die Vorbereitung enthält keine später festgestellten Spawndaten.
Recherche; die Vorbereitung enthält keine später festgestellten Spawndaten.
Messartefakte und Provenienz liegen im Benchmarkordner, nicht in der produktiven
Fallakte. Ein Einzeltest untersucht Schrittwahl und Kontextgröße, nicht die
Qualität oder Gesamtkosten einer vollständigen Untersuchung.
Fallakte. Ein Einzeltest untersucht Schrittwahl und Kontextgröße, nicht die
Qualität oder Gesamtkosten einer vollständigen Untersuchung.
## Korrektur nach HTTP 400
Der erste kompakte Aufruf wurde mit HTTP 400 abgelehnt. Kein Tokenzähler wurde
zurückgegeben. Ein genauer Anbieterfehlertext wurde vom Transport nicht erhalten;
die exakte serverseitige Ursache bleibt daher unbestätigt. Im Schema fanden sich
const-only Diskriminatoren ohne type. Diese sind für den nativen Gemini-Subset
ungeeignet. Die Messhilfe übersetzt string-const in type:string plus enum,
oneOf in anyOf und primitive Typvereinigungen in anyOf. Nicht unterstützte
Längen-/Pattern-/Unique-Regeln werden ausschließlich lokal geprüft. Nicht
Der erste kompakte Aufruf wurde mit HTTP 400 abgelehnt. Kein Tokenzähler wurde
zurückgegeben. Ein genauer Anbieterfehlertext wurde vom Transport nicht erhalten;
die exakte serverseitige Ursache bleibt daher unbestätigt. Im Schema fanden sich
const-only Diskriminatoren ohne type. Diese sind für den nativen Gemini-Subset
ungeeignet. Die Messhilfe übersetzt string-const in type:string plus enum,
oneOf in anyOf und primitive Typvereinigungen in anyOf. Nicht unterstützte
Längen-/Pattern-/Unique-Regeln werden ausschließlich lokal geprüft. Nicht
abgebildete komplexe Referenz-/Bedingungsschemata werden lokal abgelehnt.
Originalprompt und vollständiger lokaler Validierungsvertrag bleiben gleich.
Originalprompt und vollständiger lokaler Validierungsvertrag bleiben gleich.
Gemini-Referenz: https://ai.google.dev/api/generate-content#v1beta.GenerationConfig
Der bestehende fehlgeschlagene result.json bleibt erhalten. Neuer Versuch:
@@ -52,21 +52,21 @@ Der bestehende fehlgeschlagene result.json bleibt erhalten. Neuer Versuch:
```
Ausgangskostenstand laut Nutzer jetzt 8,30 EUR. Der vorherige Anstieg um 0,01 EUR
ist als nachträglich sichtbare Kontodifferenz dokumentiert, nicht als exakter
Einzelaufruf-Abrechnungsbeleg. Die Korrektur wurde nur offline geprüft.
ist als nachträglich sichtbare Kontodifferenz dokumentiert, nicht als exakter
Einzelaufruf-Abrechnungsbeleg. Die Korrektur wurde nur offline geprüft.
## Anbieterdiagnose nach erneutem HTTP 400
Auch der erste Schemafix wurde von der API mit 400 abgewiesen. Die Ursache
ist weiterhin offen. Der Probe-Aufruf liest nun bei HTTP-Fehlern opt-in höchstens
ist weiterhin offen. Der Probe-Aufruf liest nun bei HTTP-Fehlern opt-in höchstens
8 KB des Anbieterfehlers und gibt nur begrenzte Status-/Nachrichten- und
Feldhinweise aus. API-Schlüssel, URLs und zitierte Werte werden bereinigt;
Rohkörper werden nicht gespeichert. Der normale Gemini-Produktionspfad behält
Feldhinweise aus. API-Schlüssel, URLs und zitierte Werte werden bereinigt;
Rohkörper werden nicht gespeichert. Der normale Gemini-Produktionspfad behält
seine bisherigen kontrollierten Fehlercodes und verwirft weiterhin Rohfehler.
Eine bereits vorhandene Ergebnisdatei erzeugt PROMPT_PROBE_OUTPUT_EXISTS und
keinen Netzaufruf. Auch ein fehlgeschlagener API-Versuch wird nicht überschrieben.
Der nächste bewusst einzelne Diagnoseversuch lautet:
keinen Netzaufruf. Auch ein fehlgeschlagener API-Versuch wird nicht überschrieben.
Der nächste bewusst einzelne Diagnoseversuch lautet:
```powershell
.\.venv\Scripts\python.exe -m viano_bug_analysis.prompt_probe --run --output service-data/benchmarks/compact-investigation-20261017/result-diagnostic-1.json
@@ -74,3 +74,21 @@ Der nächste bewusst einzelne Diagnoseversuch lautet:
Dies ist keine weitere vermutete Schemakorrektur. Die konkrete Fehlermeldung
muss erst vorliegen, bevor der eigentliche API-Vertrag weiter angepasst wird.
## Isolierung ohne natives Antwortschema
Google meldete auch mit Diagnosehilfe lediglich INVALID_ARGUMENT ohne Feldhinweis.
Die genaue Ursache ist damit nicht belegt. Ein neuer expliziter Modus umgeht die
native Schemaübergabe: --schema-mode prompt setzt nur responseMimeType auf
application/json und fügt den vollständigen lokalen Vertrag genau einmal in den
Systemprompt ein. Alle Antwortregeln werden weiterhin lokal geprüft. Fallkontext,
Aufgabe und Modelleinstellungen bleiben erhalten; der API-Input beträgt in diesem
Modus 10.925 Nachrichtenzeichen einschließlich Vertrag und Einleitung.
```powershell
.\.venv\Scripts\python.exe -m viano_bug_analysis.prompt_probe --schema-mode prompt --run --output service-data/benchmarks/compact-investigation-20261017/result-jsonmode-1.json
```
Dies ist ein isolierender Versuch, keine bestätigte Behebung der HTTP-Ursache.
Keine automatische Wiederholung und kein automatischer Format-Fallback. Die
Metadaten kennzeichnen den Transportmodus, damit Messungen vergleichbar bleiben.
@@ -57,8 +57,8 @@ def measure(provider,case,limit,post=None):
text=''.join(part['text'] for part in candidate.get('content',{}).get('parts',[]) if 'text' in part and not part.get('thought'))
result.update(answer=text,finish_reason=candidate.get('finishReason'),model=envelope.get('modelVersion',provider.model))
result['status']='SUCCESS' if candidate.get('finishReason')=='STOP' and text else 'INCOMPLETE'
if 'responseJsonSchema' in case['payload']['generationConfig'] and result['status']=='SUCCESS':
try: valid=Draft202012Validator(case.get('validation_schema',case['payload']['generationConfig']['responseJsonSchema'])).is_valid(json.loads(text))
if ('validation_schema' in case or 'responseJsonSchema' in case['payload']['generationConfig']) and result['status']=='SUCCESS':
try: valid=Draft202012Validator(case['validation_schema'] if 'validation_schema' in case else case['payload']['generationConfig']['responseJsonSchema']).is_valid(json.loads(text))
except ValueError: valid=False
result['schema_valid']=valid
if not valid: result['status']='INVALID_JSON'
+14 -8
View File
@@ -46,7 +46,7 @@ def gemini_schema(schema):
return result
def prepare(provider,request):
def prepare(provider,request, *, schema_mode="native"):
if (set(request)!={'prompt_version','messages','output_schema'} or
not isinstance(request['prompt_version'],str) or not request['prompt_version'].strip()):
raise LLMError('PROMPT_PROBE_REQUEST')
@@ -55,12 +55,17 @@ def prepare(provider,request):
any(set(m)!={'role','content'} or not isinstance(m['content'],str) or not m['content'].strip() for m in messages)):
raise LLMError('PROMPT_PROBE_REQUEST')
Draft202012Validator.check_schema(request['output_schema'])
generation={'maxOutputTokens':provider.max_output_tokens,'responseMimeType':'application/json',
'responseJsonSchema':gemini_schema(request['output_schema'])}
if schema_mode not in {'native','prompt'}:raise LLMError('PROMPT_PROBE_SCHEMA_MODE')
generation={'maxOutputTokens':provider.max_output_tokens,'responseMimeType':'application/json'}
system_text=messages[0]['content']
if schema_mode=='native':
generation['responseJsonSchema']=gemini_schema(request['output_schema'])
else:
system_text += '\nOutput JSON contract (validated locally):\n'+json.dumps(request['output_schema'],ensure_ascii=False,separators=(',',':'))
if provider.reasoning_effort is not None:generation['thinkingConfig']={'thinkingLevel':provider.reasoning_effort}
payload={'systemInstruction':{'parts':[{'text':messages[0]['content']}]},
payload={'systemInstruction':{'parts':[{'text':system_text}]},
'contents':[{'role':'user','parts':[{'text':messages[1]['content']}]}],'generationConfig':generation}
return {'id':request['prompt_version'],'prompt_characters':sum(len(m['content']) for m in messages),
return {'id':request['prompt_version'],'prompt_characters':len(system_text)+len(messages[1]['content']),
'payload':payload,'validation_schema':request['output_schema']}
@@ -109,6 +114,7 @@ def main(argv=None):
p.add_argument('--request',type=Path,default=DEFAULT)
p.add_argument('--llm-config',default='config/llm.working-brief.local.toml')
p.add_argument('--provider')
p.add_argument('--schema-mode',choices=['native','prompt'],default='native',help='Native schema or JSON mode with the full contract once in the prompt')
p.add_argument('--output',type=Path)
p.add_argument('--run',action='store_true',help='Make exactly one potentially billable API call')
args=p.parse_args(argv)
@@ -119,13 +125,13 @@ def main(argv=None):
if provider is None or provider.adapter!='gemini':raise LLMError('PROMPT_PROBE_PROVIDER')
raw=args.request.read_bytes()
if len(raw)>config.max_request_bytes:raise LLMError('LLM_REQUEST_LIMIT')
request=json.loads(raw);case=prepare(provider,request)
request=json.loads(raw);case=prepare(provider,request,schema_mode=args.schema_mode)
if len(json.dumps(case['payload'],ensure_ascii=False).encode('utf-8'))>config.max_request_bytes:raise LLMError('LLM_REQUEST_LIMIT')
metadata={'request_sha256':hashlib.sha256(raw).hexdigest(),'provider':provider.id,'model':provider.model,
'reasoning_effort':provider.reasoning_effort,'max_output_tokens':provider.max_output_tokens,
'timeout_seconds':provider.timeout_seconds,'explicit_cache':False,'research_queries_executed':0,
'wire_schema_sha256':hashlib.sha256(json.dumps(case['payload']['generationConfig']['responseJsonSchema'],sort_keys=True,separators=(',',':')).encode()).hexdigest(),
'schema_transport':'gemini_subset_v1_local_full_validation'}
'wire_schema_sha256':hashlib.sha256(json.dumps(case['payload']['generationConfig'].get('responseJsonSchema'),sort_keys=True,separators=(',',':')).encode()).hexdigest(),
'schema_transport':'gemini_subset_v1_local_full_validation' if args.schema_mode=='native' else 'json_mode_schema_once_in_prompt_local_full_validation'}
if not args.run:
print(json.dumps({'status':'PREPARED','network_used':False,**metadata,
'message_characters':case['prompt_characters']}));return 0
+16
View File
@@ -105,3 +105,19 @@ def test_existing_output_is_named_error_without_network(tmp_path,monkeypatch,cap
result=prompt_probe.main(['--run','--request',str(path),'--output',str(output)])
assert result==2 and 'PROMPT_PROBE_OUTPUT_EXISTS' in capsys.readouterr().out
assert output.read_text()=='keep original'
def test_prompt_schema_mode_avoids_native_union_and_keeps_contract_once():
import json
r=request();case=prepare(provider(),r,schema_mode='prompt')
payload=case['payload']
assert 'responseJsonSchema' not in payload['generationConfig']
assert payload['generationConfig']['responseMimeType']=='application/json'
contract=json.dumps(r['output_schema'],ensure_ascii=False,separators=(',',':'))
assert payload['systemInstruction']['parts'][0]['text'].count(contract)==1
assert payload['contents'][0]['parts'][0]['text']==r['messages'][1]['content']
assert case['validation_schema']==r['output_schema']
def post(*args):return {'candidates':[{'finishReason':'STOP','content':{'parts':[{'text':'{"query":"database.schema"}'}]}}]}
assert measure(provider(),case,10000,post)['status']=='SUCCESS'
def bad(*args):return {'candidates':[{'finishReason':'STOP','content':{'parts':[{'text':'{"query":"invented"}'}]}}]}
assert measure(provider(),case,10000,bad)['status']=='INVALID_JSON'