Repository navigation
fix: catch OpenAI AuthenticationError before multiprocessing pickle - #1859
Merged
Merged
Conversation
openai.AuthenticationError (401) and PermissionDeniedError (403) subclass openai.APIStatusError, which carries a non-picklable httpx.Response. With --parallel_attempts > 1 the worker exception fails to unpickle in the Pool _handle_results thread (TypeError: missing keyword-only args response/body), killing the thread and hanging the run instead of surfacing the bad key. Catch both in OpenAICompatible._call_model and re-raise the picklable garak.exception.GarakException from None, so parallel runs abort cleanly on a terminal auth failure. Adds 3 tests (401 raises, pickle round-trip, 403 raises). Closes NVIDIA#1357 Signed-off-by: Devam Shah <devamshah91@gmail.com>
jmartin-tech
approved these changes
Jun 15, 2026
jmartin-tech
left a comment
Collaborator
There was a problem hiding this comment.
This works, testing shows this will expose the env var value so some additional consideration may be needed as a future improvement to avoid exposing possibly sensitive data in logs/output. That exposure exists today so not reason to expand scope. This will be accepted as is.
% OPENAI_API_KEY="fakekey" python -m garak -t openai -n gpt-4o -p lmrc.Bullying --parallel_attempts 5
garak LLM vulnerability scanner v0.15.2.pre1 ( https://github.com/NVIDIA/garak ) at 2026-06-15T09:28:38.326976
📜 logging to /Users/jemartin/.local/share/garak/garak.log
🦜 loading generator: OpenAI: gpt-4o
📜 reporting to /Users/jemartin/.local/share/garak/garak_runs/garak.1451e1f9-1a21-431d-bc98-887619670cd4.report.jsonl
🕵️ queue of probes: lmrc.Bullying
Loading weights: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████| 201/201 [00:00<00:00, 64340.62it/s]
probes.lmrc.Bullying: 0%| | 0/7 [00:00<?, ?it/s]
OpenAI API authentication failed (HTTP 401); verify OPENAI_API_KEY is valid. Original error: Error code: 401 - {'error': {'message': 'Incorrect API key provided: fakekey. You can find your API key at https://platform.openai.com/account/api-keys.', 'type': 'invalid_request_error', 'param': None, 'code': 'invalid_api_key'}}
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Catch
openai.AuthenticationErrorandopenai.PermissionDeniedErrorinside_call_modeland re-raise as the picklablegarak.exception.GarakException, so that runs with--parallel_attempts > 1abort cleanly on a bad API key instead of crashing thePool._handle_resultsbackground thread.Problem / motivation
When a user runs garak with
--parallel_attempts 4and an invalidOPENAI_API_KEY, the first generator call in each worker process raisesopenai.AuthenticationError. That exception is a subclass ofopenai.APIStatusError, which carries anhttpx.Responseattribute.httpx.Responseis not picklable — its__reduce__is not defined and its__init__requires keyword-only arguments (response=andbody=) that pickle does not supply on reconstruction.Python's
multiprocessing.Poolruns a background thread (_handle_results) that deserialises worker results from a queue. Whenpickle.loads()tries to reconstruct theAuthenticationError, it raises:This kills
_handle_results, the pool becomes unable to deliver any further results, and the main process hangs indefinitely rather than surfacing the root cause to the user. The issue was root-caused in #1357 by a community member who filed no PR.Change
garak/generators/openai.py—OpenAICompatible._call_modelAdded an
except (openai.AuthenticationError, openai.PermissionDeniedError)clause in the existingtry / exceptblock aroundgenerator.create(). Both HTTP 401 and 403 are terminal authentication failures — retrying with the same key serves no purpose and the@backoffdecorator must not loop on them. The handler:ERRORlevel, includingself.key_env_varso the user knows exactly which environment variable to fix.garak.exception.GarakException(a plainExceptionsubclass with no un-picklable attributes) fromNoneto suppress the chained traceback.GarakExceptionto the backoff decorator's exception list — it propagates immediately as a fatal run error.PermissionDeniedError(HTTP 403) is included because it shares the identical pickling defect and represents the same class of terminal credential failure.tests/generators/test_openai.py— three new teststest_call_model_auth_error_raises_garak_exceptionGarakExceptionraised, message contains"401"or the key env var nametest_call_model_auth_exception_is_picklablepickle.dumps/pickle.loadsround-trip (the precise property that makes multiprocessing safe)test_call_model_permission_denied_raises_garak_exceptionmocker.patch.object) →GarakExceptionraisedThe existing
auth_failentry already present intests/_assets/generators/openai.jsonis reused for the HTTP-level mocks.Security rationale
Masking a credential failure behind an unhandled thread exception is an observability gap: the operator has no indication that the run aborted due to an invalid key, so they may misinterpret empty results as "no vulnerabilities found" — a false-negative outcome for a security evaluation tool. Surfacing authentication failures as first-class, logged, named exceptions aligns with OWASP LLM09 (Overreliance) mitigation (auditable failure modes) and the general principle that security tooling must fail loudly and correctly.
Testing / validation
Results on Python 3.14.4, openai 2.41.1, respx 0.23.1, pytest-mock 3.15.1:
The root cause was also verified empirically: reproducing the broken path (worker raises raw
openai.AuthenticationError) producesException in thread Thread-6 (_handle_results): TypeError: APIStatusError.__init__() missing 2 required keyword-only arguments: 'response' and 'body'; the fixed path catches cleanly asGarakExceptionthrough the pool.