effort-Parameter modellabhängig senden, Design-Hierarchie, iOS-Datumsfeld
All checks were successful
Build & Deploy / deploy (push) Successful in 1m14s

Fehler: claude-haiku-4-5 lehnt output_config.effort mit einem 400 ab, jede
Karte lief in einen Lesefehler. effort und der server-seitige Fallback
werden jetzt nur an Modelle geschickt, die sie annehmen; ein unbekanntes
Modell mit engerem Parametersatz wird einmal ohne Zusatzparameter
wiederholt, statt die Karte zu verlieren. Tests decken die Zuordnung ab.

Design: Grau kommt als Hierarchiestufe dazu. Drei Ebenen für Fläche
(weiß / grau gefüllt / schwarz), Text (schwarz / --ink-2 / --ink-3) und
Linie (3px / 1px schwarz / graue Haarlinie). Vorher trug alles dieselbe
3px-Kante und dasselbe Schwarz, dadurch war keine Ordnung erkennbar.
Konkret: nicht gewählte Filter treten zurück, Listenzeilen bekommen drei
Textstufen und Statuspillen statt einer gleichförmigen Zeile, das
Kopfband über Listen ist grau hinterlegt, zweitrangige und zerstörende
Aktionen sind leiser als die primäre.

iOS: Datumsfelder zentrieren ihren Wert und sacken in der Höhe ein. Die
WebKit-Pseudoelemente ziehen sie auf die Form der übrigen Felder.

Dazu die Domain in der Dokumentation auf scanner.lucas-orth.de.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Lucas Orth
2026-09-06 12:36:24 +02:00
parent bc1981f1bd
commit 1f29d5eb27
7 changed files with 324 additions and 142 deletions

View File

@@ -52,9 +52,22 @@ Regeln:
PROMPT = "Lies diese Visitenkarte aus."
# Nicht jedes Modell nimmt jeden Parameter an: die kleinen Modelle kennen
# output_config.effort nicht (400), der server-seitige Fallback gilt nur fuer
# die groessten.
_NO_EFFORT = ("claude-haiku-", "claude-sonnet-4-5")
_WITH_FALLBACK = ("claude-opus-5", "claude-fable-")
_client = None
def _output_config(with_effort: bool) -> dict:
settings = {"format": {"type": "json_schema", "schema": SCHEMA}}
if with_effort:
settings["effort"] = "low" # reines Ablesen, keine Denkarbeit
return settings
def available() -> bool:
return bool(config.ANTHROPIC_API_KEY)
@@ -92,27 +105,28 @@ async def _ask(image_jpeg: bytes) -> dict:
],
}
]
common = dict(
model=config.ANTHROPIC_MODEL,
max_tokens=2000,
system=SYSTEM,
messages=messages,
output_config={
"format": {"type": "json_schema", "schema": SCHEMA},
"effort": "low", # reines Ablesen, keine Denkarbeit
},
)
model = config.ANTHROPIC_MODEL
common = dict(model=model, max_tokens=2000, system=SYSTEM, messages=messages)
effort = not model.startswith(_NO_EFFORT)
try:
response = await client.beta.messages.create(
betas=["server-side-fallback-2026-07-01"],
fallbacks="default",
**common,
if model.startswith(_WITH_FALLBACK):
response = await client.beta.messages.create(
betas=["server-side-fallback-2026-07-01"],
fallbacks="default",
output_config=_output_config(effort),
**common,
)
else:
response = await client.messages.create(
output_config=_output_config(effort), **common
)
except anthropic.BadRequestError as exc:
# Ein Modell mit engerem Parametersatz, als die Listen oben kennen.
# Einmal ohne Zusatzparameter nachfassen, statt die Karte zu verlieren.
log.warning("Anfrage abgelehnt (%s) - Wiederholung ohne Zusatzparameter", exc)
response = await client.messages.create(
output_config=_output_config(with_effort=False), **common
)
except anthropic.BadRequestError:
# Der Fallback-Parameter haengt am Modell. Ohne ihn laeuft die Anfrage
# weiter, sie bricht bei einer Ablehnung nur ohne Ersatzmodell ab.
log.warning("Server-side fallback abgelehnt, Anfrage ohne Fallback")
response = await client.messages.create(**common)
if response.stop_reason == "refusal":
return _empty("Vom Modell abgelehnt")