ai newsSo., 25. Okt.
EilmeldungWas KI-Modelle ueber uns verraten, nicht ueber sich selbst

Inspect AI Evals mit eigener OpenAI-kompatibler API ausfuehren

Inspect AI, das Open-Source-Evaluierungsframework des UK AI Security Institute, unterstuetzt ueber einen generischen openai-api Provider beliebige APIs mit OpenAI-Schnittstelle. Beim Einsatz sind Modellnamen, zwei Retry-Ebenen und versteckte Fehlermeldungen zu beachten.

dev.toSonntag 09:46 · 25.10.DE6 Min. Lesezeit
Bild: dev.to

Inspect AI ist das Open-Source-Evaluierungsframework des UK AI Security Institute. Man schreibt eine Task (Dataset, Solver, Scorer), führt inspect eval aus und erhält ein Log, das man durchsehen kann. Es bietet First-Party-Provider für die großen Labs sowie einen generischen openai-api-Provider für alles, was das OpenAI Chat Completions Format spricht: vLLM, einen LiteLLM-Proxy, Ollamas /v1-Endpunkt oder ein gehostetes Gateway.

Die Einrichtung besteht aus zwei Umgebungsvariablen, aber einige Voreinstellungen verändern Ihre Ergebnisse und Ihre Anzahl an Anfragen auf leicht zu übersehende Weise. Ich habe inspect_ai 0.3.281 mit openai 3.35.1 auf Python 3.13.5 gegen einen kleinen lokalen Mock-Server getestet. Der Mock protokollierte Pfad, Header und JSON-Body jeder Anfrage und konnte auf Verlangen 400, 401, 429 oder 503 zurückgeben. Alles Folgende ist, was über die Leitung ging. Die Beispiele verwenden APIClaw, ein OpenAI-kompatibles Gateway, das ich entwickle, weshalb Sie meine Einschätzungen mit diesem Wissen lesen sollten. Seine Basis-URL ist https://apiclaw.biz/v1. Ersetzen Sie YOUR_MODEL_ID durch eine exakte ID aus der Modellliste Ihres Providers, statt eine zu raten.

pip install inspect-ai openai export APICLAW_API_KEY = "your-key" export APICLAW_BASE_URL = "https://apiclaw.biz/v1" inspect eval arith.py --model openai-api/apiclaw/YOUR_MODEL_ID Enter fullscreen mode Exit fullscreen mode Der Modell-String besteht aus drei Teilen: openai-api, ein selbst gewählter Service-Name und die Modell-ID. Inspect wandelt den Service-Namen in Großbuchstaben um und liest <SERVICE>_API_KEY und <SERVICE>_BASE_URL. Bindestriche werden zu Unterstrichen, daher liest openai-api/my-gw/... MY_GW_API_KEY und MY_GW_BASE_URL. Wählen Sie einen kurzen Namen und bleiben Sie dabei, denn er erscheint auch in Ihren Logs und in der Fortschrittsanzeige.

Fehlt eine der beiden Variablen, erhalten Sie eine klare Meldung, bevor irgendeine Anfrage rausgeht: No APICLAW_API_KEY defined in the environment. (oder..._BASE_URL). Lassen Sie den Service komplett weg ( openai-api/YOUR_MODEL_ID ), erhalten Sie einen ValueError: openai-api model names must include a service prefix, verpackt in einen langen TaskGroup-Traceback, der leicht als Netzwerkproblem missdeutet wird.

Alles nach dem Service-Namen wird unverändert als Modell-ID durchgereicht, Schrägstriche eingeschlossen. openai-api/apiclaw/deepseek/deepseek-chat sendete "model": "deepseek/deepseek-chat", was genau das ist, was Gateways mit herstellerpräfixierten IDs erwarten.

Inspect übergibt die Basis-URL unverändert an das OpenAI Python SDK, und das SDK hängt nur /chat/completions an. Mit APICLAW_BASE_URL=http://host/v1 sah der Mock POST /v1/chat/completions. Ohne /v1 sah er POST /chat/completions, was die meisten Server mit einem 404 beantworten. Die Anfrage enthielt Authorization: Bearer <your key> und den User-Agent des SDK, AsyncOpenAI/Python.

# arith.py from inspect_ai import Task, task from inspect_ai.dataset import Sample from inspect_ai.scorer import match from inspect_ai.solver import generate @task def arith (): return Task ( dataset = [ Sample ( input = f " What is 2+2? (variant { i } ) ", target = " 4 " ) for i in range ( 20 )], solver = generate (), scorer = match (), ) Enter fullscreen mode Exit fullscreen mode Ohne Generierungsoptionen enthielt der Body nur model und messages. Keine temperature, kein max_tokens, kein seed, Sie bekommen also die Voreinstellungen, die der Server anwendet. Wenn Sie reproduzierbare Läufe wollen, sagen Sie das auf der Kommandozeile: --temperature 0 --max-tokens 50 --seed 7 kamen als "temperature": 0.0, "max_tokens": 50, "seed": 7 an.

Streaming ist ausgeschaltet, außer man fordert es an. -M stream=true fügte "stream": true und "stream_options": {"include_usage": true} hinzu, und die Token-Zahlen erschienen trotzdem in der Zusammenfassung, weil der Mock am Ende einen Usage-Chunk sendete. Wenn Ihr Server beim Streaming keine Usage-Daten sendet, müssen Sie mit fehlenden Token-Summen rechnen.

Wenn das Modell sein Reasoning in <think>...</think> einschließt, trennt Inspect es heraus. Der Mock antwortete <think>let me reason</think>ANSWER: 4, und die gespeicherte Nachricht enthielt einen Reasoning-Block plus einen Textblock, wobei output.completion gleich ANSWER: 4 war. Der match()-Scorer sah nur die Antwort.

Inspect sendet nicht eine Anfrage nach der anderen. Adaptive Concurrency ist standardmäßig aktiviert; der Config-Docstring nennt als Grenzen ein Minimum von 10, einen Startwert von 20 und ein Maximum von 100 Verbindungen. Bei 60 Samples und einem Mock, der pro Antwort 300 ms brauchte, sah ich bis zu 30 Anfragen gleichzeitig in der Luft.

Das ist wichtig, wenn Ihr Provider Anfragen pro Minute oder pro Tag begrenzt, so wie es viele Flatrate-Tarife tun. Setzen Sie stattdessen eine feste Zahl:

inspect eval arith.py --model openai-api/apiclaw/YOUR_MODEL_ID --max-connections 4 Enter fullscreen mode Exit fullscreen mode Mit --max-connections 4 lag der Peak exakt bei 4. Zählen Sie außerdem Ihre Anfragen ehrlich: Jedes Sample ist mindestens ein Aufruf, und model-graded Scorer kommen hinzu (siehe unten).

Das war die größte Überraschung. Inspect erstellt einen AsyncOpenAI-Client und lässt die eigene max_retries-Einstellung des SDK auf dem Standardwert 2, sodass jeder Inspect-Versuch aus bis zu drei HTTP-Anfragen besteht. Inspect wiederholt dann fehlgeschlagene Versuche selbst, und sein max_retries steht standardmäßig auf unbegrenzt.

Mit dem Mock, der für ein Sample 429 und Retry-After: 1 zurückgab:

Um eine Retry-Ebene zu erhalten, die Sie kontrollieren, schalten Sie die des SDK mit einem model arg aus und geben Inspect ein Limit:

inspect eval arith.py --model openai-api/apiclaw/YOUR_MODEL_ID \ -M max_retries = 0 --max-retries 3 --timeout 120 Enter fullscreen mode Exit fullscreen mode -M-Argumente gehen an den Konstruktor des OpenAI-Clients, daher gilt max_retries=0 dort. Damit und mit --max-retries 3 erzeugte dasselbe 429 vier Anfragen statt zwölf. --timeout begrenzt die gesamte Anfrage inklusive Wiederholungen; mit --timeout 20 brach der Lauf nach 4 Anfragen ab.

Ich ließ den Mock für eines von fünf Samples ein 400 zurückgeben. Standardmäßig endete die Eval mit dem Status error und gar keinen Scores, obwohl vier Samples erfolgreich waren.

Die übliche Lösung ist --fail-on-error 0.25 (bis zu 25 % fehlgeschlagene Samples tolerieren) oder --no-fail-on-error. Beides ergab den Status success und accuracy 1.000. Diese Zahl wird über die vier funktionierenden Samples berechnet; das fehlgeschlagene wird ausgelassen statt als falsch gezählt. Wenn ein Server Ihre schwierigsten Prompts ablehnt, etwa weil sie das Context Window überschreiten, steigt Ihre Accuracy. Prüfen Sie die Anzahl der fehlerhaften Samples im Log, bevor Sie Modelle vergleichen:

from inspect_ai.log import read_eval_log log = read_eval_log ( " logs/your-run.eval " ) errored = sum ( 1 for s in log. samples if s. error ) print ( log. status, f " { errored } / { len ( log. samples ) } samples errored " ) Enter fullscreen mode Exit fullscreen mode Der Grader ist das Modell, das Sie testen. model_graded_qa() und die anderen model-graded Scorer verwenden die Grader-Modellrolle, und wenn Sie sie nicht setzen, verwenden sie das zu evaluierende Modell. In meinem Lauf sendete jedes Sample zwei Anfragen an dasselbe Modell: erst die Antwort, dann einen Grading-Prompt, der mit "You are assessing a submitted answer..." beginnt. Ein Modell sich selbst bewerten zu lassen ist selten das, was Sie wollen, und es verdoppelt Ihre Anzahl an Anfragen. Lenken Sie den Grader woanders hin:

inspect eval graded.py --model openai-api/apiclaw/YOUR_MODEL_ID \ --model-role grader = openai-api/apiclaw/YOUR_GRADER_MODEL_ID Enter fullscreen mode Exit fullscreen mode Mit dieser Einstellung sah der Mock, dass die Antwort-Anfrage an eine Modell-ID ging und die Grading-Anfrage an die andere. Beide verwenden dieselben APICLAW_*-Variablen, weil sie sich einen Service-Namen teilen.

from inspect_ai import eval from arith import arith logs = eval ( arith (), model = " openai-api/apiclaw/YOUR_MODEL_ID ", model_base_url = " https://apiclaw.biz/v1 ", model_args = { " max_retries ": 0 }, max_retries = 3, max_connections = 4, ) print ( logs [ 0 ]. status ) Enter fullscreen mode Exit fullscreen mode model_base_url ersetzt die _BASE_URL-Variable; der Schlüssel kommt weiterhin aus APICLAW_API_KEY. Für einmalige Aufrufe außerhalb einer Eval funktionierten get_model("openai-api/apiclaw/YOUR_MODEL_ID", base_url=..., api_key=...) und await model.generate("...") auf dieselbe Weise.

Inspect schreibt für jeden Lauf eine.eval-Datei, auch für fehlgeschlagene, und speichert die Messages und Parameter jeder Anfrage, damit Sie sie in inspect view einsehen können. Ich durchsuchte alle 18 Logs meiner Tests nach dem API-Key und nach dem String Bearer und fand beides nicht. Prompts und Antworten werden vollständig gespeichert, behandeln Sie den logs/-Ordner also wie jede andere Datei mit Testdaten.

Offenlegung: Ich entwickle APIClaw, das in diesen Beispielen verwendete OpenAI-kompatible Gateway (monatliche Flatrate-Tarife; Krypto auf der Website, andere Zahlungsmethoden über den Support). Alles oben Genannte gilt für jeden kompatiblen Endpunkt.

Automatisch übersetzt. Das Original auf Englisch ist unten aufklappbar.

Original (Englisch) anzeigen

Inspect AI is the open-source evaluation framework from the UK AI Security Institute. You write a task (dataset, solver, scorer), run inspect eval, and get a log you can browse. It has first-party providers for the big labs, plus a generic openai-api provider for anything that speaks the OpenAI Chat Completions format: vLLM, a LiteLLM proxy, Ollama's /v1 endpoint or a hosted gateway.

The setup is two environment variables, but a few defaults change your results and your request count in ways that are easy to miss. I tested inspect_ai 0.3.281 with openai 3.35.1 on Python 3.13.5 against a small local mock server. The mock logged the path, headers and JSON body of every request and could return 400, 401, 429 or 503 on demand. Everything below is what went over the wire. The examples use APIClaw, an OpenAI-compatible gateway I build, so weigh my opinions with that in mind. Its base URL is https://apiclaw.biz/v1. Replace YOUR_MODEL_ID with an exact ID from your provider's model list rather than guessing one.

pip install inspect-ai openai export APICLAW_API_KEY = "your-key" export APICLAW_BASE_URL = "https://apiclaw.biz/v1" inspect eval arith.py --model openai-api/apiclaw/YOUR_MODEL_ID Enter fullscreen mode Exit fullscreen mode The model string has three parts: openai-api, a service name you choose, and the model ID. Inspect upper-cases the service name and reads <SERVICE>_API_KEY and <SERVICE>_BASE_URL. Hyphens become underscores, so openai-api/my-gw/... reads MY_GW_API_KEY and MY_GW_BASE_URL. Pick a short name and stick with it, because it also appears in your logs and progress display.

If either variable is missing you get a clear message before any request goes out: No APICLAW_API_KEY defined in the environment. (or..._BASE_URL ). If you leave the service out entirely ( openai-api/YOUR_MODEL_ID ), you get a ValueError: openai-api model names must include a service prefix wrapped in a long TaskGroup traceback, which is easy to misread as a network problem.

Everything after the service name is passed through as the model ID, slashes included. openai-api/apiclaw/deepseek/deepseek-chat sent "model": "deepseek/deepseek-chat", which is what you want for gateways that use vendor-prefixed IDs.

Inspect hands the base URL to the OpenAI Python SDK unchanged, and the SDK only appends /chat/completions. With APICLAW_BASE_URL=http://host/v1 the mock saw POST /v1/chat/completions. Without /v1 it saw POST /chat/completions, which most servers answer with a 404. The request carried Authorization: Bearer <your key> and the SDK's AsyncOpenAI/Python user agent.

# arith.py from inspect_ai import Task, task from inspect_ai.dataset import Sample from inspect_ai.scorer import match from inspect_ai.solver import generate @task def arith (): return Task ( dataset = [ Sample ( input = f " What is 2+2? (variant { i } ) ", target = " 4 " ) for i in range ( 20 )], solver = generate (), scorer = match (), ) Enter fullscreen mode Exit fullscreen mode With no generation options, the body was just model and messages. No temperature, no max_tokens, no seed, so you get whatever defaults the server applies. If you want reproducible runs, say so on the command line: --temperature 0 --max-tokens 50 --seed 7 arrived as "temperature": 0.0, "max_tokens": 50, "seed": 7.

Streaming is off unless you ask for it. -M stream=true added "stream": true and "stream_options": {"include_usage": true}, and token counts still showed up in the summary because the mock sent a usage chunk at the end. If your server does not send usage when streaming, expect missing token totals.

If the model wraps its reasoning in <think>...</think>, Inspect splits it out. The mock answered <think>let me reason</think>ANSWER: 4, and the stored message had a reasoning block plus a text block, with output.completion equal to ANSWER: 4. The match() scorer saw only the answer.

Inspect does not send one request at a time. Adaptive concurrency is on by default; the config docstring gives its bounds as a minimum of 10, a start of 20 and a maximum of 100 connections. With 60 samples and a mock that took 300 ms per reply, I saw up to 30 requests in flight at once.

That matters if your provider limits requests per minute or per day, which is how many flat-rate plans work. Set a fixed number instead:

inspect eval arith.py --model openai-api/apiclaw/YOUR_MODEL_ID --max-connections 4 Enter fullscreen mode Exit fullscreen mode With --max-connections 4 the peak was exactly 4. Also count your requests honestly: every sample is at least one call, and model-graded scorers add more (see below).

This was the biggest surprise. Inspect creates an AsyncOpenAI client and leaves the SDK's own max_retries at its default of 2, so every Inspect attempt is up to three HTTP requests. Inspect then retries failed attempts itself, and its max_retries defaults to unlimited.

With the mock returning 429 and Retry-After: 1 for one sample:

To get one retry layer you control, turn the SDK's off with a model arg and give Inspect a limit:

inspect eval arith.py --model openai-api/apiclaw/YOUR_MODEL_ID \ -M max_retries = 0 --max-retries 3 --timeout 120 Enter fullscreen mode Exit fullscreen mode -M arguments go to the OpenAI client constructor, so max_retries=0 applies there. With that and --max-retries 3 the same 429 produced 4 requests, not 12. --timeout caps the whole request including retries; with --timeout 20 the run stopped after 4 requests.

I made the mock return a 400 for one sample out of five. By default the eval ended with status error and no scores at all, even though four samples had succeeded.

The usual fix is --fail-on-error 0.25 (tolerate up to 25% failed samples) or --no-fail-on-error. Both gave status success and accuracy 1.000. That number is computed over the four samples that worked; the failed one is left out rather than counted as wrong. If a server rejects your hardest prompts, for example because they exceed the context window, your accuracy goes up. Check the errored sample count in the log before you compare models:

from inspect_ai.log import read_eval_log log = read_eval_log ( " logs/your-run.eval " ) errored = sum ( 1 for s in log. samples if s. error ) print ( log. status, f " { errored } / { len ( log. samples ) } samples errored " ) Enter fullscreen mode Exit fullscreen mode The grader is the model you are testing model_graded_qa() and the other model-graded scorers use the grader model role, and if you do not set it they use the model under evaluation. In my run each sample sent two requests to the same model: the answer, then a grading prompt starting "You are assessing a submitted answer...". Letting a model grade itself is rarely what you want, and it doubles your request count. Point the grader somewhere else:

inspect eval graded.py --model openai-api/apiclaw/YOUR_MODEL_ID \ --model-role grader = openai-api/apiclaw/YOUR_GRADER_MODEL_ID Enter fullscreen mode Exit fullscreen mode With that set, the mock saw the answer request go to one model ID and the grading request to the other. Both use the same APICLAW_* variables because they share a service name.

from inspect_ai import eval from arith import arith logs = eval ( arith (), model = " openai-api/apiclaw/YOUR_MODEL_ID ", model_base_url = " https://apiclaw.biz/v1 ", model_args = { " max_retries ": 0 }, max_retries = 3, max_connections = 4, ) print ( logs [ 0 ]. status ) Enter fullscreen mode Exit fullscreen mode model_base_url replaces the _BASE_URL variable; the key still comes from APICLAW_API_KEY. For one-off calls outside an eval, get_model("openai-api/apiclaw/YOUR_MODEL_ID", base_url=..., api_key=...) and await model.generate("...") worked the same way.

Inspect writes a.eval file for every run, including failed ones, and it stores each request's messages and parameters so you can inspect them in inspect view. I searched all 18 logs from my tests for the API key and for the string Bearer and found neither. Prompts and responses are stored in full, so treat the logs/ folder like any other file with test data in it.

Disclosure: I build APIClaw, the OpenAI-compatible gateway used in these examples (flat monthly plans; crypto on the site, other payment methods through support). Everything above applies to any compatible endpoint.

Originaltitel: Running Inspect AI Evals on a Custom OpenAI-Compatible API: openai-api/ Model Names, Two Retry Layers and Hidden ErrorsQuelle: dev.to (dev.to). Original öffnen