Adapt prompt probe schema to Gemini subset with full local validation

This commit is contained in:
Nico committed 2026-10-17 12:55:34 +02:00
1 parent 7c168182c1
commit 2eaba70083
4 files changed
+99 -18

No files matched your search

+36 -13
View File
@@ -4,30 +4,53 @@
.\.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
abgebildete komplexe Referenz-/Bedingungsschemata werden lokal abgelehnt.
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:
```powershell
.\.venv\Scripts\python.exe -m viano_bug_analysis.prompt_probe --run --output service-data/benchmarks/compact-investigation-20261017/result-schemafix-1.json
```
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.
@@ -58,7 +58,7 @@ def measure(provider,case,limit,post=None):
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['payload']['generationConfig']['responseJsonSchema']).is_valid(json.loads(text))
try: valid=Draft202012Validator(case.get('validation_schema',case['payload']['generationConfig']['responseJsonSchema'])).is_valid(json.loads(text))
except ValueError: valid=False
result['schema_valid']=valid
if not valid: result['status']='INVALID_JSON'
+36 -3
View File
@@ -12,6 +12,37 @@ from .telemetry.safety import safe_code
DEFAULT = Path('service-data/benchmarks/compact-investigation-20261017/request.json')
def gemini_schema(schema):
"""Provider projection only; full original schema remains the local validator.
Gemini's responseJsonSchema subset omits const and several validation
keywords. In particular, a const-only kind has no usable type after const
is ignored. Translate it rather than handing the internal contract through.
"""
allowed = {'type','format','title','description','enum','items','prefixItems',
'minItems','maxItems','minimum','maximum','anyOf','oneOf',
'properties','additionalProperties','required'}
if not isinstance(schema,dict):
raise LLMError('PROMPT_PROBE_SCHEMA_UNSUPPORTED')
if any(k in schema for k in ('$ref','$defs','allOf','not','if','then','else')):
raise LLMError('PROMPT_PROBE_SCHEMA_UNSUPPORTED')
result = {}
for key,value in schema.items():
if key not in allowed: continue
if key == 'properties': result[key] = {k:gemini_schema(v) for k,v in value.items()}
elif key in {'items','additionalProperties'} and isinstance(value,dict): result[key] = gemini_schema(value)
elif key in {'anyOf','oneOf','prefixItems'}: result['anyOf' if key=='oneOf' else key] = [gemini_schema(v) for v in value]
else: result[key] = value
if 'const' in schema:
value=schema['const']
if not isinstance(value,str): raise LLMError('PROMPT_PROBE_SCHEMA_UNSUPPORTED')
result['type']='string';result['enum']=[value]
if isinstance(result.get('type'),list):
types=result.pop('type')
result['anyOf']=[{'type':value} for value in types]
return result
def prepare(provider,request):
if (set(request)!={'prompt_version','messages','output_schema'} or
not isinstance(request['prompt_version'],str) or not request['prompt_version'].strip()):
@@ -22,12 +53,12 @@ def prepare(provider,request):
raise LLMError('PROMPT_PROBE_REQUEST')
Draft202012Validator.check_schema(request['output_schema'])
generation={'maxOutputTokens':provider.max_output_tokens,'responseMimeType':'application/json',
'responseJsonSchema':request['output_schema']}
'responseJsonSchema':gemini_schema(request['output_schema'])}
if provider.reasoning_effort is not None:generation['thinkingConfig']={'thinkingLevel':provider.reasoning_effort}
payload={'systemInstruction':{'parts':[{'text':messages[0]['content']}]},
'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),
'payload':payload}
'payload':payload,'validation_schema':request['output_schema']}
def main(argv=None):
@@ -49,7 +80,9 @@ def main(argv=None):
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}
'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'}
if not args.run:
print(json.dumps({'status':'PREPARED','network_used':False,**metadata,
'message_characters':case['prompt_characters']}));return 0
+26 -1
View File
@@ -16,7 +16,8 @@ def test_native_schema_sent_once_without_wrapper_or_tools():
payload=case['payload']
assert set(payload)=={'systemInstruction','contents','generationConfig'}
assert payload['contents'][0]['parts'][0]['text']=='Test context'
assert payload['generationConfig']['responseJsonSchema']==r['output_schema']
assert payload['generationConfig']['responseJsonSchema']['properties']['query']=={'type':'string','enum':['database.schema']}
assert case['validation_schema']==r['output_schema']
assert payload['generationConfig']['thinkingConfig']=={'thinkingLevel':'low'}
assert r==before
@@ -34,3 +35,27 @@ def test_validates_the_prepared_schema_not_the_minimal_test_schema():
def test_rejects_invalid_envelopes(field,value):
r=request();r[field]=value
with pytest.raises(LLMError):prepare(provider(),r)
def test_gemini_projection_types_const_and_keeps_strict_local_validation():
from viano_bug_analysis.prompt_probe import gemini_schema
from jsonschema import Draft202012Validator
r=request()
r['output_schema']['properties']['query']={'const':'database.schema','minLength':5,'pattern':'^database'}
original=copy.deepcopy(r)
c=prepare(provider(),r)
wire=c['payload']['generationConfig']['responseJsonSchema']
Draft202012Validator.check_schema(wire)
assert wire['properties']['query']=={'type':'string','enum':['database.schema']}
assert r==original and c['validation_schema']==r['output_schema']
assert gemini_schema({'type':['string','number','boolean','null']})=={
'anyOf':[{'type':v} for v in ['string','number','boolean','null']]}
def test_local_constraints_not_silently_dropped_from_response_check():
r=request();r['output_schema']['properties']['query']={'type':'string','pattern':'^database'}
c=prepare(provider(),r)
def post(*args):return {'candidates':[{'finishReason':'STOP','content':{'parts':[{'text':'{"query":"invented"}'}]}}],
'usageMetadata':{'promptTokenCount':200}}
result=measure(provider(),c,10000,post)
assert result['status']=='INVALID_JSON' and result['schema_valid'] is False