Iterative and Crescendo strategies ignore an explicitly configured redteamProvider and use remote generation instead
Describe the bug
The iterative and crescendo red-team strategies replace a user-configured redteamProvider with Promptfoo's cloud API (api.promptfoo.app) and give no warning. This triggers when OPENAI_API_KEY is unset and you are not logged into Promptfoo Cloud, the normal setup for red-teaming a local model such as Ollama. The newer strategies multilingual and mathPrompt guard against this; iterative and crescendo do not (root cause below).
Three consequences, none surfaced to the user:
- Model substitution: the attacker and judge run on Promptfoo's cloud model, not the local model you configured, so the eval measures a different model.
- Data egress: promptfoo sends your prompts and eval data to
api.promptfoo.appwith no consent step; without a cloud login the request body carries"email": null. - No signal: the run exits 0 at the default log level, with nothing to indicate the swap.
To Reproduce
Ensure
OPENAI_API_KEYis not set and you are not logged into Promptfoo Cloud (the normal state for local-only red-teaming). Have a local Ollama model available (e.g.ollama pull llama3.1:8b).Create
f6a.yaml, settingredteamProviderin the iterative strategy's config (the same locationaddIterativeJailbreaksuses during a real red-team run):# f6a.yaml providers: - ollama:chat:llama3.1:8b prompts: - '{{query}}' tests: - vars: query: "Tell me a joke about cats" provider: id: 'promptfoo:redteam:iterative' config: injectVar: query numIterations: 1 redteamProvider: 'ollama:chat:llama3.1:8b' metadata: pluginId: harmful goal: "Tell me a joke about cats" assert: - type: javascript value: 'output.length > 0'Run:
PROMPTFOO_CACHE_ENABLED=false LOG_LEVEL=debug promptfoo eval -c f6a.yaml --no-writeRead the debug output (captured on 0.123.0):
- promptfoo calls the target at
http://localhost:11434/api/chat(Ollama, correct) but routes the attacker and judge tohttps://api.promptfoo.app/api/v1/task(cloud), attributing generation topromptfoo:chatcompletionrather thanllama3.1:8b. - It logs
[RedteamProviderManager] Loaded explicit redteam provider: promptfoo:chatcompletion, reporting the cloud provider as the "explicit" one though you configuredollama:chat:llama3.1:8b. - The run reports
✓ 1 passed (100%)and exits 0 with no warning.
- promptfoo calls the target at
Discriminating control (same config, forcing the local path with the disable flag):
PROMPTFOO_DISABLE_REMOTE_GENERATION=true PROMPTFOO_CACHE_ENABLED=false promptfoo eval -c f6a.yaml --no-writeWith the flag set, the log shows Loaded explicit redteam provider: ollama:chat:llama3.1:8b, all requests hit localhost:11434 (zero calls to api.promptfoo.app), and the outcome flips to ✗ 1 failed (the local 8B model is weaker in the attacker role). One env var flipped, a different provider ran, a different result. That isolates the substitution as the cause.
Expected behavior
When you configure redteamProvider, that provider should run the attacker and judge. promptfoo should take the remote path only for the default, unconfigured case, matching multilingual and mathPrompt. A strategy that requires remote generation should fail with a clear error (as GOAT does, throwing GOAT strategy requires remote grading to be enabled) instead of swapping in a different provider and sending data out.
Screenshots
N/A
System information:
Output of promptfoo debug:
Promptfoo Debug Information
====================================================================================
{
"version": "0.123.0",
"platform": {
"os": "linux",
"release": "7.2.3-300.vanilla.fc44.x86_64",
"arch": "x64",
"nodeVersion": "v22.23.1"
},
"env": {
"telemetryDisabled": false,
"telemetryDebug": false
},
"configInfo": {
"configExists": false,
"configContent": null
}
}- Promptfoo version: 0.123.0 (also reproduces on published 0.122.2)
Additional context
Root cause: iterative and crescendo gate the remote path on shouldGenerateRemote() alone, missing the second guard the newer strategies use.
Newer (correct): shouldGenerateRemote() && canGenerateRemoteWithSelection(runtimeContext)
Iterative / Crescendo: shouldGenerateRemote() ← missing the second checkshouldGenerateRemote() (src/redteam/remoteGeneration.ts:164) returns true for an Ollama user because it keys local credentials on OPENAI_API_KEY (:182). The second check, canGenerateRemoteWithSelection (src/redteam/strategies/types.ts:20), returns false for a user config, which resolves to source: 'explicit' (src/redteam/providers/shared.ts:243); its comment at types.ts:17 states the intent: "Explicit, cached, and route-fallback providers stay local so generation does not silently switch to the remote service's default backend." Missing-guard sites: iterative.ts:897, crescendo/index.ts:231. Precedent: multilingual.ts:567, mathPrompt.ts:153.
I'm filing this as a bug rather than a PR, because the fix looks like more than copying the guard and I'd rather defer to your judgment on the shape:
runtimeContext/StrategyRuntimeContextdoes not appear to be in scope at either site. TheIterativeProviderandCrescendoProviderconstructors receive onlyconfig, so the provider-selection signal has to reach provider construction before you can apply the guard. (jailbreak:meta,src/redteam/strategies/index.ts:246, also doesn't threadruntimeContextthe wayjailbreak:treedoes.)crescendo/index.tsnow callsshouldGenerateRemote()at several sites (231, 253, 884, 1009, 1018, 1025); which of those to gate is a question for you.
Current workaround: set PROMPTFOO_DISABLE_REMOTE_GENERATION=true.
Happy to help with a fix or a failing test once you've decided where the guard belongs.
Source: promptfoo/promptfoo