---
sidebar_label: HTTP API
description: Configure HTTP/HTTPS endpoints for custom LLM integrations with dynamic request transforms, variable substitution, and multi-provider API compatibility
---
# HTTP/HTTPS API
Setting the provider ID to a URL sends an HTTP request to the endpoint. This provides a general-purpose way to use any HTTP endpoint for inference.
The provider configuration allows you to construct the HTTP request and extract the inference result from the response.
```yaml
providers:
- id: https
config:
url: 'https://example.com/generate'
method: 'POST'
headers:
'Content-Type': 'application/json'
body:
myPrompt: '{{prompt}}'
transformResponse: 'json.output' # Extract the "output" field from the response
```
The placeholder variable `{{prompt}}` will be replaced with the final prompt for the test case. You can also reference test variables as you construct the request:
```yaml
providers:
- id: https
config:
url: 'https://example.com/generateTranslation'
body:
prompt: '{{prompt}}'
model: '{{model}}'
translate: '{{language}}'
tests:
- vars:
model: 'gpt-5-mini'
language: 'French'
```
When available, Promptfoo also injects runtime variables such as `{{evaluationId}}`, which is useful for correlating downstream logs with a specific eval run:
```yaml
body:
prompt: '{{prompt}}'
evaluation_id: '{{evaluationId}}'
```
`body` can be a string or JSON object. If the body is a string, the `Content-Type` header defaults to `text/plain` unless specified otherwise. If the body is an object, then content type is automatically set to `application/json`.
### JSON Example
```yaml
providers:
- id: https
config:
url: 'https://example.com/generateTranslation'
body:
model: '{{model}}'
translate: '{{language}}'
```
### Form-data Example
```yaml
providers:
- id: https
config:
headers:
'Content-Type': 'application/x-www-form-urlencoded'
body: 'model={{model}}&translate={{language}}'
```
## Sending multipart/form-data
Use `multipart.parts` when the target API expects form fields or file uploads.
Promptfoo builds a fresh `FormData` body for every request and automatically sets the
multipart boundary. Do not set `Content-Type: multipart/form-data` yourself unless
you are using [raw HTTP request mode](#sending-a-raw-http-request).
### Text fields and generated documents
This example sends the prompt as a `documentQuery` text field and sends a simple
generated PDF as the `files` upload field:
```yaml
providers:
- id: http
config:
url: 'http://localhost:8080/api/genai/analyze-file'
method: POST
headers:
X-API-Key: '{{api_key}}'
multipart:
parts:
- kind: file
name: files
filename: promptfoo-document.pdf
source:
type: generated
format: pdf
text: 'Promptfoo generated document for multipart testing.'
- kind: field
name: documentQuery
value: '{{prompt}}'
transformResponse: json.summary
```
The `generated` source creates a deterministic document suitable for transport
tests. Supported formats are `pdf`, `png`, `jpeg`, and `jpg` (alias for `jpeg`).
### Uploading local files
Use a `path` source to upload a file from the machine running promptfoo. Relative
paths resolve from the promptfoo config directory.
```yaml
providers:
- id: http
config:
url: 'http://localhost:8080/api/genai/analyze-file'
method: POST
multipart:
parts:
- kind: file
name: files
filename: sample-report45.pdf
contentType: application/pdf
source:
type: path
path: file://fixtures/sample-report45.pdf
- kind: field
name: documentQuery
value: '{{prompt}}'
transformResponse: json.summary
```
Structured multipart requests bypass the HTTP response cache by default.
`multipart` is mutually exclusive with `request` and `body`, and it cannot be
used with `GET` or `HEAD`.
## Sending a raw HTTP request
You can also send a raw HTTP request by specifying the `request` property in the provider configuration. This allows you to have full control over the request, including headers and body.
Here's an example of how to use the raw HTTP request feature:
```yaml
providers:
- id: https
config:
useHttps: true
request: |
POST /v1/completions HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer {{api_key}}
{
"model": "llama3.1-405b-base",
"prompt": "{{prompt}}",
"max_tokens": 100
}
transformResponse: 'json.content' # extract the "content" field from the response
```
In this example:
1. The `request` property contains a raw HTTP request, including the method, path, headers, and body.
2. The `useHttps` property is set to `true`, so the request will be sent over HTTPS.
3. You can use template variables like `{{api_key}}` and `{{prompt}}` within the raw request. These will be replaced with actual values when the request is sent.
4. The `transformResponse` property is used to extract the desired information from the JSON response.
You can also load the raw request from an external file using the `file://` prefix:
```yaml
providers:
- id: https
config:
request: file://path/to/request.txt
transformResponse: 'json.text'
```
This path is relative to the directory containing the Promptfoo config file.
Then create a file at `path/to/request.txt`:
```http
POST /api/generate HTTP/1.1
Host: example.com
Content-Type: application/json
{"prompt": "Tell me a joke"}
```
### Nested objects
Nested objects are supported and should be passed to the `dump` function.
```yaml
providers:
- id: https
config:
url: 'https://example.com/generateTranslation'
body:
// highlight-start
messages: '{{messages | dump}}'
// highlight-end
model: '{{model}}'
translate: '{{language}}'
tests:
- vars:
// highlight-start
messages:
- role: 'user'
content: 'foobar'
- role: 'assistant'
content: 'baz'
// highlight-end
model: 'gpt-5-mini'
language: 'French'
```
Note that any valid JSON string within `body` will be converted to a JSON object.
## Query parameters
Query parameters can be specified in the provider config using the `queryParams` field. These will be appended to the URL as GET parameters.
```yaml
providers:
- id: https
config:
url: 'https://example.com/search'
// highlight-start
method: 'GET'
queryParams:
q: '{{prompt}}'
foo: 'bar'
// highlight-end
```
## Dynamic URLs
Both the provider `id` and the `url` field support Nunjucks templates. Variables in your test `vars` will be rendered before sending the request.
```yaml
providers:
- id: https://api.example.com/users/{{userId}}/profile
config:
method: 'GET'
```
## Using as a library
If you are using promptfoo as a [node library](/docs/usage/node-package/), you can provide the equivalent provider config:
```javascript
{
// ...
providers: [{
id: 'https',
config: {
url: 'https://example.com/generate',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: {
foo: '{{bar}}',
},
transformResponse: (json) => json.output,
}
}],
}
```
## Request Transform
Request transform modifies your prompt after it is rendered but before it is sent to a provider API. This allows you to:
- Format prompts into specific message structures
- Add metadata or context
- Handle nuanced message formats for multi-turn conversations
### Basic Usage
```yaml
providers:
- id: https
config:
url: 'https://api.example.com/chat'
transformRequest: '{"message": "{{prompt}}"}'
body:
user_message: '{{prompt}}'
```
### Transform Types
#### String Template
Use Nunjucks templates to transform the prompt:
```yaml
transformRequest: '{"text": "{{prompt}}"}'
```
#### JavaScript Function
Define a function that transforms the prompt:
```javascript
transformRequest: (prompt, vars, context) =>
JSON.stringify({ text: prompt, timestamp: Date.now() });
```
#### File-based Transform
Load a transform from an external file:
```yaml
transformRequest: 'file://transforms/request.js'
```
Example transform file (transforms/request.js):
```javascript
module.exports = (prompt, vars, context) => {
return {
text: prompt,
metadata: {
timestamp: Date.now(),
version: '1.0',
},
};
};
```
You can also specify a specific function to use:
```yaml
transformRequest: 'file://transforms/request.js:transformRequest'
```
## Response Transform
The `transformResponse` option allows you to extract and transform the API response. If no `transformResponse` is specified, the provider will attempt to parse the response as JSON. If JSON parsing fails, it will return the raw text response.
You can override this behavior by specifying a `transformResponse` in the provider config. The `transformResponse` can be one of the following:
1. A string containing a JavaScript expression
2. A function
3. A file path (prefixed with `file://`) to a JavaScript module
### Parsing a JSON response
By default, the entire response is returned as the output. If your API responds with a JSON object and you want to pick out a specific value, use the `transformResponse` property to set a JavaScript snippet that manipulates the provided `json` object.
For example, this `transformResponse` configuration:
```yaml
providers:
- id: https
config:
url: 'https://example.com/openai-compatible/chat/completions'
# ...
transformResponse: 'json.choices[0].message.content'
```
Extracts the message content from this response:
```json
{
"id": "chatcmpl-abc123",
"object": "chat.completion",
"created": 1677858242,
"model": "gpt-5-mini",
"usage": {
"prompt_tokens": 13,
"completion_tokens": 7,
"total_tokens": 20
},
"choices": [
{
"message": {
"role": "assistant",
// highlight-start
"content": "\n\nThis is a test!"
// highlight-end
},
"logprobs": null,
"finish_reason": "stop",
"index": 0
}
]
}
```
### Parsing a text response
If your API responds with a text response, you can use the `transformResponse` property to set a JavaScript snippet that manipulates the provided `text` object.
For example, this `transformResponse` configuration:
```yaml
providers:
- id: https
config:
url: 'https://example.com/api'
# ...
transformResponse: 'text.slice(11)'
```
Extracts the message content "hello world" from this response:
```text
Assistant: hello world
```
### Response Parser Types
#### String parser
You can use a string containing a JavaScript expression to extract data from the response:
```yaml
providers:
- id: https
config:
url: 'https://example.com/api'
transformResponse: 'json.choices[0].message.content'
```
This expression will be evaluated with three variables available:
- `json`: The parsed JSON response (if the response is valid JSON)
- `text`: The raw text response
- `context`: `context.response` is of type `FetchWithCacheResult` which includes:
- `data`: The response data (parsed as JSON if possible)
- `cached`: Boolean indicating if response was from cache
- `status`: HTTP status code
- `statusText`: HTTP status text
- `headers`: Response headers (if present)
#### Function parser
When using promptfoo as a Node.js library, you can provide a function as the response. You may return a string or an object of type `ProviderResponse`.
parser:
```javascript
{
providers: [{
id: 'https',
config: {
url: 'https://example.com/generate_response',
transformResponse: (json, text) => {
// Custom parsing logic that returns string
return json.choices[0].message.content;
},
}
},
{
id: 'https',
config: {
url: 'https://example.com/generate_with_tokens',
transformResponse: (json, text) => {
// Custom parsing logic that returns object
return {
output: json.output,
tokenUsage: {
prompt: json.usage.input_tokens,
completion: json.usage.output_tokens,
total: json.usage.input_tokens + json.usage.output_tokens,
}
}
},
}
}],
}
```
Type definition
```typescript
interface ProviderResponse {
cached?: boolean;
cost?: number;
error?: string;
logProbs?: number[];
metadata?: {
redteamFinalPrompt?: string;
[key: string]: any;
};
raw?: string | any;
output?: string | any;
tokenUsage?: TokenUsage;
isRefusal?: boolean;
conversationEnded?: boolean;
conversationEndReason?: string;
sessionId?: string;
guardrails?: GuardrailResponse;
audio?: {
id?: string;
expiresAt?: number;
data?: string; // base64 encoded audio data
transcript?: string;
format?: string;
};
}
export type TokenUsage = z.infer;
export const TokenUsageSchema = BaseTokenUsageSchema.extend({
assertions: BaseTokenUsageSchema.optional(),
});
export const BaseTokenUsageSchema = z.object({
// Core token counts
prompt: z.number().optional(),
completion: z.number().optional(),
cached: z.number().optional(),
total: z.number().optional(),
// Request metadata
numRequests: z.number().optional(),
// Detailed completion information
completionDetails: CompletionTokenDetailsSchema.optional(),
});
```
#### File-based parser
You can use a JavaScript file as a response parser by specifying the file path with the `file://` prefix. The file path is resolved relative to the directory containing the promptfoo configuration file.
```yaml
providers:
- id: https
config:
url: 'https://example.com/api'
transformResponse: 'file://path/to/parser.js'
```
The parser file should export a function that takes three arguments (`json`, `text`, `context`) and return the parsed output. Note that text and context are optional.
```javascript
module.exports = (json, text) => {
return json.choices[0].message.content;
};
```
You can use the `context` parameter to access response metadata and implement custom logic. For example, implementing guardrails checking:
```javascript
module.exports = (json, text, context) => {
return {
output: json.choices[0].message.content,
guardrails: { flagged: context.response.headers['x-content-filtered'] === 'true' },
};
};
```
This allows you to access additional response metadata and implement custom logic based on response status codes, headers, or other properties.
You can also use a default export:
```javascript
export default (json, text) => {
return json.choices[0].message.content;
};
```
You can also specify a function name to be imported from a file:
```yaml
providers:
- id: https
config:
url: 'https://example.com/api'
transformResponse: 'file://path/to/parser.js:parseResponse'
```
This will import the function `parseResponse` from the file `path/to/parser.js`.
### Guardrails Support
If your HTTP target has guardrails set up, you need to return an object with both `output` and `guardrails` fields from your transform. The `guardrails` field should be a top-level field in your returned object and must conform to the [GuardrailResponse](/docs/configuration/reference#guardrails) interface. For example:
```yaml
providers:
- id: https
config:
url: 'https://example.com/api'
transformResponse: |
{
output: json.choices[0].message.content,
guardrails: { flagged: context.response.headers['x-content-filtered'] === 'true' }
}
```
### Ending Multi-turn Conversations
For stateful red team strategies, you can signal that the target intentionally closed the active thread by returning:
- `conversationEnded: true`
- Optional `conversationEndReason` for debugging (for example, `thread_closed`)
```yaml
providers:
- id: https
config:
url: 'https://example.com/api'
transformResponse: |
{
output: json.message || '',
sessionId: json.sessionId,
conversationEnded: json.threadClosed === true,
conversationEndReason: json.threadClosed ? 'thread_closed' : undefined
}
```
When this flag is set, multi-turn red team attackers stop gracefully instead of continuing into timeout/error turns.
### Interaction with Test Transforms
The `transformResponse` output becomes the input for test-level transforms. Understanding this pipeline is important for complex evaluations:
```yaml
providers:
- id: https
config:
url: 'https://example.com/api'
# Step 1: Provider transform normalizes API response
transformResponse: 'json.data' # Extract data field
tests:
- vars:
query: 'What is the weather?'
options:
# Step 2a: Test transform for assertions (receives provider transform output)
transform: 'output.answer'
assert:
- type: contains
value: 'sunny'
# Step 2b: Context transform for RAG assertions (also receives provider transform output)
- type: context-faithfulness
contextTransform: 'output.sources.join(" ")'
```
## Tool Calling
The HTTP provider supports tool calling through the `tools`, `tool_choice`, and `transformToolsFormat` config options. Define your tools and tool choice in OpenAI format, then set `transformToolsFormat` to the target provider's format (`openai`, `anthropic`, `bedrock`, or `google`). Promptfoo converts `tools` and `tool_choice` before injecting them into your request body via `{{tools}}` and `{{tool_choice}}`.
Setting `transformToolsFormat` is especially important when the HTTP provider is used as a guardrails provider, so that managed tool calls are formatted correctly for the target API.
### Basic Configuration
```yaml
providers:
- id: https://api.example.com/v1/chat/completions
config:
method: POST
headers:
Content-Type: application/json
Authorization: 'Bearer {{env.API_KEY}}'
transformToolsFormat: openai
tools:
- type: function
function:
name: get_weather
description: Get weather for a location
parameters:
type: object
properties:
location:
type: string
required:
- location
tool_choice: auto
body:
model: gpt-4o-mini
messages:
- role: user
content: '{{prompt}}'
tools: '{{tools}}'
tool_choice: '{{tool_choice}}'
transformResponse: 'json.choices[0].message.tool_calls'
```
### transformToolsFormat
The `transformToolsFormat` option converts **both** `tools` and `tool_choice` from [OpenAI format](/docs/configuration/tools) to provider-specific formats. Define your tools once in OpenAI format, and they'll be automatically transformed to the target provider's native format.
| Provider | Format |
| ---------------- | ----------- |
| Anthropic | `anthropic` |
| AWS Bedrock | `bedrock` |
| Azure OpenAI | `openai` |
| Cerebras | `openai` |
| DeepSeek | `openai` |
| Fireworks AI | `openai` |
| Google AI Studio | `google` |
| Google Vertex AI | `google` |
| Groq | `openai` |
| Ollama | `openai` |
| OpenAI | `openai` |
| OpenRouter | `openai` |
| Perplexity | `openai` |
| Together AI | `openai` |
| xAI (Grok) | `openai` |
Use `openai` or omit for OpenAI-compatible APIs where no conversion is needed.
**Why tool_choice needs transformation:** Each provider represents tool choice differently:
| OpenAI (Promptfoo default) | Anthropic | Bedrock | Google |
| -------------------------- | ------------------ | -------------- | --------------------------------------------- |
| `"auto"` | `{ type: "auto" }` | `{ auto: {} }` | `{ functionCallingConfig: { mode: "AUTO" } }` |
| `"required"` | `{ type: "any" }` | `{ any: {} }` | `{ functionCallingConfig: { mode: "ANY" } }` |
| `"none"` | — | — | `{ functionCallingConfig: { mode: "NONE" } }` |
### Template Variables
Use these variables in your request body:
- `{{tools}}` - The transformed tools array, automatically serialized as JSON
- `{{tool_choice}}` - The transformed tool choice, automatically serialized as JSON
For complete documentation on tool formats and configuration, see [Tool Calling Configuration](/docs/configuration/tools).
## Token Estimation
By default, the HTTP provider does not provide token usage statistics since it's designed for general HTTP APIs that may not return token information. However, you can enable optional token estimation to get approximate token counts for cost tracking and analysis. Token estimation is automatically enabled when running redteam scans so you can track approximate costs without additional configuration.
Token estimation uses a simple word-based counting method with configurable multipliers. This provides a rough approximation that's useful for basic cost estimation and usage tracking.
:::note Accuracy
Word-based estimation provides approximate token counts. For precise token counting, implement custom logic in your `transformResponse` function using a proper tokenizer library.
:::
### When to Use Token Estimation
Token estimation is useful when:
- Your API doesn't return token usage information
- You need basic cost estimates for budget tracking
- You want to monitor usage patterns across different prompts
- You're migrating from an API that provides token counts
Don't use token estimation when:
- Your API already provides accurate token counts (use `transformResponse` instead)
- You need precise token counts for billing
- You're working with non-English text where word counting is less accurate
### Basic Token Estimation
Enable basic token estimation with default settings:
```yaml
providers:
- id: https
config:
url: 'https://example.com/api'
body:
prompt: '{{prompt}}'
tokenEstimation:
enabled: true
```
This will use word-based estimation with a multiplier of 1.3 for both prompt and completion tokens.
### Custom Multipliers
Configure a custom multiplier for more accurate estimation based on your specific use case:
```yaml
providers:
- id: https
config:
url: 'https://example.com/api'
body:
prompt: '{{prompt}}'
tokenEstimation:
enabled: true
multiplier: 1.5 # Adjust based on your content complexity
```
**Multiplier Guidelines:**
- Start with default `1.3` and adjust based on actual usage
- Technical/code content may need higher multipliers (1.5-2.0)
- Simple conversational text may work with lower multipliers (1.1-1.3)
- Monitor actual vs. estimated usage to calibrate
### Integration with Transform Response
Token estimation works alongside response transforms. If your `transformResponse` returns token usage information, the estimation will be skipped:
```yaml
providers:
- id: https
config:
url: 'https://example.com/api'
tokenEstimation:
enabled: true # Will be ignored if transformResponse provides tokenUsage
transformResponse: |
{
output: json.choices[0].message.content,
tokenUsage: {
prompt: json.usage.prompt_tokens,
completion: json.usage.completion_tokens,
total: json.usage.total_tokens
}
}
```
### Custom Token Counting
For accurate token counting, implement it in your `transformResponse` function:
```yaml
providers:
- id: https
config:
url: 'https://example.com/api'
transformResponse: |
(json, text, context) => {
// Use a proper tokenizer library for accuracy
const promptTokens = customTokenizer.encode(context.vars.prompt).length;
const completionTokens = customTokenizer.encode(json.response).length;
return {
output: json.response,
tokenUsage: {
prompt: promptTokens,
completion: completionTokens,
total: promptTokens + completionTokens,
numRequests: 1
}
};
}
```
You can also load custom logic from a file:
```yaml
providers:
- id: https
config:
url: 'https://example.com/api'
transformResponse: 'file://token-counter.js'
```
Example `token-counter.js`:
```javascript
// Using a tokenizer library like 'tiktoken' or 'gpt-tokenizer'
const { encode } = require('gpt-tokenizer');
module.exports = (json, text, context) => {
const promptText = context.vars.prompt || '';
const responseText = json.response || text;
return {
output: responseText,
tokenUsage: {
prompt: encode(promptText).length,
completion: encode(responseText).length,
total: encode(promptText).length + encode(responseText).length,
numRequests: 1,
},
};
};
```
### Configuration Options
| Option | Type | Default | Description |
| ---------- | ------- | ---------------------------- | -------------------------------------------------------- |
| enabled | boolean | false (true in redteam mode) | Enable or disable token estimation |
| multiplier | number | 1.3 | Multiplier applied to word count (adjust for complexity) |
### Example: Cost Tracking
Here's a complete example for cost tracking with token estimation:
```yaml
providers:
- id: https
config:
url: 'https://api.example.com/v1/generate'
method: POST
headers:
Authorization: 'Bearer {{env.API_KEY}}'
Content-Type: 'application/json'
body:
model: 'custom-model'
prompt: '{{prompt}}'
max_tokens: 100
tokenEstimation:
enabled: true
multiplier: 1.4 # Adjusted based on testing
transformResponse: |
{
output: json.generated_text,
cost: (json.usage?.total_tokens || 0) * 0.0001 // $0.0001 per token
}
```
## TLS/HTTPS Configuration
The HTTP provider supports custom TLS certificate configuration for secure HTTPS connections. This enables:
- Custom CA certificates for verifying server certificates
- Client certificates for mutual TLS authentication
- PFX/PKCS12 certificate bundles
- Fine-grained control over TLS security settings
### Basic TLS Configuration
Configure custom CA certificates to verify server certificates:
```yaml
providers:
- id: https
config:
url: 'https://api.example.com/secure'
tls:
caPath: '/path/to/ca-cert.pem' # Custom CA certificate
rejectUnauthorized: true # Verify server certificate (default: true)
```
### Mutual TLS (mTLS)
For APIs requiring client certificate authentication:
```yaml
providers:
- id: https
config:
url: 'https://secure-api.example.com/v1'
tls:
# Client certificate and private key
certPath: '/path/to/client-cert.pem'
keyPath: '/path/to/client-key.pem'
# Optional: Custom CA for server verification
caPath: '/path/to/ca-cert.pem'
```
### Using PFX/PKCS12 Certificates
For PFX or PKCS12 certificate bundles, you can either provide a file path or inline base64-encoded content:
```yaml
providers:
- id: https
config:
url: 'https://secure-api.example.com/v1'
tls:
# Option 1: Using a file path
pfxPath: '/path/to/certificate.pfx'
passphrase: '{{env.PFX_PASSPHRASE}}' # Optional: passphrase for PFX
```
```yaml
providers:
- id: https
config:
url: 'https://secure-api.example.com/v1'
tls:
# Option 2: Using inline base64-encoded content
pfx: 'MIIJKQIBAzCCCO8GCSqGSIb3DQEHAaCCCOAEggjcMIII2DCCBYcGCSqGSIb3DQEHBqCCBXgwggV0AgEAMIIFbQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQI...' # Base64-encoded PFX content
passphrase: '{{env.PFX_PASSPHRASE}}' # Optional: passphrase for PFX
```
### Using JKS (Java KeyStore) Certificates
For Java applications using JKS certificates, the provider can automatically extract the certificate and key for TLS:
```yaml
providers:
- id: https
config:
url: 'https://secure-api.example.com/v1'
tls:
# Option 1: Using a file path
jksPath: '/path/to/keystore.jks'
passphrase: '{{env.JKS_PASSWORD}}' # Required for JKS
keyAlias: 'mykey' # Optional: specific alias to use
```
```yaml
providers:
- id: https
config:
url: 'https://secure-api.example.com/v1'
tls:
# Option 2: Using inline base64-encoded JKS content
jksContent: 'MIIJKQIBAzCCCO8GCSqGSIb3DQEHA...' # Base64-encoded JKS content
passphrase: '{{env.JKS_PASSWORD}}'
keyAlias: 'client-cert' # Optional: defaults to first available key
```
The JKS file is processed using the `jks-js` library, which automatically:
- Extracts the certificate and private key from the keystore
- Converts them to PEM format for use with TLS
- Selects the appropriate key based on the alias (or uses the first available)
:::info
JKS support requires the `jks-js` package. Install it with:
```bash
npm install jks-js
```
:::
### Advanced TLS Options
Fine-tune TLS connection parameters:
```yaml
providers:
- id: https
config:
url: 'https://api.example.com/v1'
tls:
# Certificate configuration
certPath: '/path/to/client-cert.pem'
keyPath: '/path/to/client-key.pem'
caPath: '/path/to/ca-cert.pem'
# Security options
rejectUnauthorized: true # Verify server certificate
servername: 'api.example.com' # Override SNI hostname
# Cipher and protocol configuration
ciphers: 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256'
secureProtocol: 'TLSv1_3_method' # Force TLS 1.3
minVersion: 'TLSv1.2' # Minimum TLS version
maxVersion: 'TLSv1.3' # Maximum TLS version
```
### Inline Certificates
You can provide certificates directly in the configuration instead of file paths:
```yaml
providers:
- id: https
config:
url: 'https://secure-api.example.com/v1'
tls:
# Provide PEM certificates as strings
cert: |
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKl...
-----END CERTIFICATE-----
key: |
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0...
-----END PRIVATE KEY-----
ca: |
-----BEGIN CERTIFICATE-----
MIIDQTCCAimgAwIBAgITBmyf...
-----END CERTIFICATE-----
```
For PFX certificates, provide them as base64-encoded strings:
```yaml
providers:
- id: https
config:
url: 'https://secure-api.example.com/v1'
tls:
# PFX certificate as base64-encoded string
pfx: 'MIIJKQIBAzCCCO8GCSqGSIb3DQEHAaCCCOAEggjcMIII2DCCBYcGCSqGSIb3DQEHBqCCBXgwggV0AgEAMIIFbQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQI...'
passphrase: 'your-pfx-passphrase'
```
### Multiple CA Certificates
Support for multiple CA certificates in the trust chain:
```yaml
providers:
- id: https
config:
url: 'https://api.example.com/v1'
tls:
ca:
- |
-----BEGIN CERTIFICATE-----
[Root CA certificate content]
-----END CERTIFICATE-----
- |
-----BEGIN CERTIFICATE-----
[Intermediate CA certificate content]
-----END CERTIFICATE-----
```
### Self-Signed Certificates
For development/testing with self-signed certificates:
```yaml
providers:
- id: https
config:
url: 'https://localhost:8443/api'
tls:
rejectUnauthorized: false # Accept self-signed certificates (NOT for production!)
```
:::warning
Setting `rejectUnauthorized: false` disables certificate verification and should **never** be used in production environments as it makes connections vulnerable to man-in-the-middle attacks.
:::
### Environment Variables
Use environment variables for sensitive certificate data:
```yaml
providers:
- id: https
config:
url: 'https://api.example.com/v1'
tls:
certPath: '{{env.CLIENT_CERT_PATH}}'
keyPath: '{{env.CLIENT_KEY_PATH}}'
passphrase: '{{env.CERT_PASSPHRASE}}'
```
### TLS Configuration Options
| Option | Type | Default | Description |
| ------------------ | ------------------ | ------- | ---------------------------------------------------------------------------------- |
| ca | string \| string[] | - | CA certificate(s) for verifying server certificates |
| caPath | string | - | Path to CA certificate file |
| cert | string \| string[] | - | Client certificate(s) for mutual TLS |
| certPath | string | - | Path to client certificate file |
| key | string \| string[] | - | Private key(s) for client certificate |
| keyPath | string | - | Path to private key file |
| pfx | string \| Buffer | - | PFX/PKCS12 certificate bundle (base64-encoded string or Buffer for inline content) |
| pfxPath | string | - | Path to PFX/PKCS12 file |
| jksPath | string | - | Path to JKS keystore file |
| jksContent | string | - | Base64-encoded JKS keystore content |
| keyAlias | string | - | Alias of the key to use from JKS (defaults to first available) |
| passphrase | string | - | Passphrase for encrypted private key, PFX, or JKS |
| rejectUnauthorized | boolean | true | If true, verify server certificate against CA |
| servername | string | - | Server name for SNI (Server Name Indication) TLS extension |
| ciphers | string | - | Cipher suite specification (OpenSSL format) |
| secureProtocol | string | - | SSL method to use (e.g., 'TLSv1_2_method', 'TLSv1_3_method') |
| minVersion | string | - | Minimum TLS version to allow (e.g., 'TLSv1.2', 'TLSv1.3') |
| maxVersion | string | - | Maximum TLS version to allow (e.g., 'TLSv1.2', 'TLSv1.3') |
:::info
- When using client certificates, you must provide both certificate and key (unless using PFX or JKS)
- PFX and JKS bundles contain both certificate and key, so only the bundle and passphrase are needed
- The TLS configuration is applied to all HTTPS requests made by this provider
:::
## Authentication
The HTTP provider supports multiple authentication methods. For specialized cases, use custom hooks or custom providers.
### Bearer Token
For APIs that accept a static bearer token:
```yaml
providers:
- id: https
config:
url: 'https://api.example.com/v1/chat'
body:
prompt: '{{prompt}}'
auth:
type: bearer
token: '{{env.API_TOKEN}}'
```
The provider adds an `Authorization: Bearer ` header to each request.
### API Key
For APIs that use API key authentication, you can place the key in either a header or query parameter:
```yaml
providers:
- id: https
config:
url: 'https://api.example.com/v1/chat'
body:
prompt: '{{prompt}}'
auth:
type: api_key
keyName: 'X-API-Key'
value: '{{env.API_KEY}}'
placement: header # or 'query'
```
When `placement` is `header`, the key is added as a request header. When `placement` is `query`, it's appended as a URL query parameter.
### Basic Authentication
For APIs that use HTTP Basic authentication:
```yaml
providers:
- id: https
config:
url: 'https://api.example.com/v1/chat'
body:
prompt: '{{prompt}}'
auth:
type: basic
username: '{{env.API_USERNAME}}'
password: '{{env.API_PASSWORD}}'
```
The provider Base64-encodes credentials and adds an `Authorization: Basic ` header.
### OAuth 2.0
OAuth 2.0 authentication supports **Client Credentials** and **Password** (Resource Owner Password Credentials) grant types.
When a request is made, the provider:
1. Checks if a valid access token exists in cache
2. If no token exists or is expired, requests a new one from `tokenUrl`
3. Caches the access token
4. Adds the token to API requests as an `Authorization: Bearer ` header
Tokens are refreshed proactively with a 60-second buffer before expiry.
#### Client Credentials Grant
Use this grant type for server-to-server authentication:
```yaml
providers:
- id: https
config:
url: 'https://api.example.com/v1/chat'
body:
prompt: '{{prompt}}'
auth:
type: oauth
grantType: client_credentials
tokenUrl: 'https://auth.example.com/oauth/token'
clientId: '{{env.OAUTH_CLIENT_ID}}'
clientSecret: '{{env.OAUTH_CLIENT_SECRET}}'
scopes:
- read
- write
```
#### Password Grant
Use this grant type when authenticating with user credentials:
```yaml
providers:
- id: https
config:
url: 'https://api.example.com/v1/chat'
body:
prompt: '{{prompt}}'
auth:
type: oauth
grantType: password
tokenUrl: 'https://auth.example.com/oauth/token'
clientId: '{{env.OAUTH_CLIENT_ID}}'
clientSecret: '{{env.OAUTH_CLIENT_SECRET}}'
username: '{{env.OAUTH_USERNAME}}'
password: '{{env.OAUTH_PASSWORD}}'
scopes:
- read
```
#### Token Endpoint Requirements
The token endpoint must return a JSON response with an `access_token` field. If `expires_in` (lifetime in seconds) is included, the provider uses it to schedule refresh. Otherwise, a 1-hour default is used.
### File-Based Authentication
Use file-based authentication when your token needs custom logic that doesn't fit the built-in auth flows.
The auth file can be written in JavaScript, TypeScript, or Python:
- JavaScript and TypeScript files should export a default function by default
- Python files should define `get_auth` by default
- Named exports are supported with `file://path/to/file.ts:functionName`
The auth function receives the standard HTTP provider `callApi` context and must return:
```ts
{
token: string;
expiration?: number | null;
}
```
- `token` is required
- `expiration` is optional and should be an absolute Unix timestamp in milliseconds
- If `expiration` is omitted or `null`, the token is cached for the lifetime of the provider instance
- If `expiration` is provided, the function is called again when the token is within the same 60-second refresh buffer used by OAuth
The auth function receives the same `callApi` context object that providers receive at runtime, including `vars`, `prompt`, `test`, `originalProvider`, `evaluationId`, `testCaseId`, `traceparent`, `tracestate`, and `repeatIndex`.
Unlike `bearer` and `oauth`, file auth does not automatically attach an `Authorization` header. Instead, the returned values are injected into template variables before the request is rendered so you can place them anywhere in the request:
- headers
- query params
- JSON bodies
- raw requests
- session endpoint config
- request transforms
```yaml
providers:
- id: https
config:
url: 'https://api.example.com/v1/chat'
method: POST
headers:
Authorization: 'Bearer {{token}}'
body:
prompt: '{{prompt}}'
auth:
type: file
path: './auth/get-token.ts'
```
Available template variables:
- `{{token}}`
- `{{expiration}}`
If `token` or `expiration` already exist in `vars`, the file auth result overwrites them and emits a warning.
This is intentionally different from OAuth:
- `oauth` automatically adds `Authorization: Bearer `
- `oauth` does not inject `{{token}}` into template vars
- `file` injects `{{token}}` and `{{expiration}}` into template vars
- `file` does not automatically add an `Authorization` header
Example auth files:
```ts
export default async function getAuth(context) {
return {
token: context.vars.apiKey,
expiration: Date.now() + 55 * 60 * 1000,
};
}
```
```ts
export async function buildAuth(context) {
return {
token: context.vars.sessionToken,
};
}
```
Use the named export with:
```yaml
auth:
type: file
path: file://./auth/get-token.ts:buildAuth
```
```python
def get_auth(context):
return {
"token": context["vars"]["api_key"],
"expiration": None,
}
```
### Digital Signature Authentication
For APIs requiring cryptographic request signing, the HTTP provider supports digital signatures with PEM, JKS (Java KeyStore), and PFX certificate formats. The private key is **never sent to Promptfoo** and remains stored locally.
#### Basic Usage (PEM)
```yaml
providers:
- id: https
config:
url: 'https://api.example.com/v1'
headers:
'x-signature': '{{signature}}'
'x-timestamp': '{{signatureTimestamp}}'
signatureAuth:
type: pem
privateKeyPath: '/path/to/private.key'
```
When signature authentication is enabled, these template variables become available for use in headers or body:
- `{{signature}}`: The generated signature (base64-encoded)
- `{{signatureTimestamp}}`: Unix timestamp when the signature was generated
#### Certificate Formats
**PEM Certificates:**
```yaml
signatureAuth:
type: pem
privateKeyPath: '/path/to/private.key' # Path to PEM file
# OR inline key:
# privateKey: '-----BEGIN PRIVATE KEY-----\n...'
```
**JKS (Java KeyStore):**
```yaml
signatureAuth:
type: jks
keystorePath: '/path/to/keystore.jks'
keystorePassword: '{{env.JKS_PASSWORD}}' # Or use PROMPTFOO_JKS_PASSWORD env var
keyAlias: 'your-key-alias' # Optional: uses first available if not specified
```
**PFX (PKCS#12):**
```yaml
signatureAuth:
type: pfx
pfxPath: '/path/to/certificate.pfx'
pfxPassword: '{{env.PFX_PASSWORD}}' # Or use PROMPTFOO_PFX_PASSWORD env var
# OR use separate certificate and key files:
# certPath: '/path/to/certificate.crt'
# keyPath: '/path/to/private.key'
```
#### Full Configuration Example
```yaml
providers:
- id: https
config:
url: 'https://api.example.com/v1'
headers:
'x-signature': '{{signature}}'
'x-timestamp': '{{signatureTimestamp}}'
signatureAuth:
type: pem
privateKeyPath: '/path/to/private.key'
signatureValidityMs: 300000 # 5 minutes (default)
signatureAlgorithm: 'SHA256' # Default
signatureDataTemplate: '{{signatureTimestamp}}' # Default; customize as needed
signatureRefreshBufferMs: 30000 # Optional custom refresh buffer
```
:::info Dependencies
- **JKS support** requires the `jks-js` package: `npm install jks-js`
- **PFX support** requires the `pem` package: `npm install pem`
:::
### Authentication Options Reference
#### Bearer Token Options
| Option | Type | Required | Description |
| ------ | ------ | -------- | ------------------ |
| type | string | Yes | Must be `'bearer'` |
| token | string | Yes | The bearer token |
#### API Key Options
| Option | Type | Required | Description |
| --------- | ------ | -------- | ----------------------------------------------- |
| type | string | Yes | Must be `'api_key'` |
| keyName | string | Yes | Name of the header or query parameter |
| value | string | Yes | The API key value |
| placement | string | Yes | Where to place the key: `'header'` or `'query'` |
#### Basic Auth Options
| Option | Type | Required | Description |
| -------- | ------ | -------- | ----------------- |
| type | string | Yes | Must be `'basic'` |
| username | string | Yes | Username |
| password | string | Yes | Password |
#### OAuth 2.0 Options
| Option | Type | Required | Description |
| ------------ | -------- | --------------------------------------- | -------------------------------------- |
| type | string | Yes | Must be `'oauth'` |
| grantType | string | Yes | `'client_credentials'` or `'password'` |
| tokenUrl | string | Yes | OAuth token endpoint URL |
| clientId | string | Yes (client_credentials), No (password) | OAuth client ID |
| clientSecret | string | Yes (client_credentials), No (password) | OAuth client secret |
| username | string | Yes (password grant) | Username for password grant |
| password | string | Yes (password grant) | Password for password grant |
| scopes | string[] | No | OAuth scopes to request |
#### File Auth Options
| Option | Type | Required | Description |
| ------ | ------ | -------- | ----------------------------------------------------- |
| type | string | Yes | Must be `'file'` |
| path | string | Yes | Path to a JavaScript, TypeScript, or Python auth file |
#### Digital Signature Options
| Option | Type | Required | Default | Description |
| ------------------------ | ------ | -------- | -------------------------- | ------------------------------------------------------ |
| type | string | No | `'pem'` | Certificate type: `'pem'`, `'jks'`, or `'pfx'` |
| privateKeyPath | string | No\* | - | Path to PEM private key file (PEM only) |
| privateKey | string | No\* | - | Inline PEM private key string (PEM only) |
| keystorePath | string | No\* | - | Path to JKS keystore file (JKS only) |
| keystoreContent | string | No\* | - | Base64-encoded JKS keystore content (JKS only) |
| keystorePassword | string | No | - | JKS password (or use `PROMPTFOO_JKS_PASSWORD` env var) |
| keyAlias | string | No | First available | JKS key alias (JKS only) |
| pfxPath | string | No\* | - | Path to PFX certificate file (PFX only) |
| pfxPassword | string | No | - | PFX password (or use `PROMPTFOO_PFX_PASSWORD` env var) |
| certPath | string | No\* | - | Path to certificate file (PFX alternative) |
| keyPath | string | No\* | - | Path to private key file (PFX alternative) |
| certContent | string | No\* | - | Base64-encoded certificate content (PFX alternative) |
| keyContent | string | No\* | - | Base64-encoded private key content (PFX alternative) |
| signatureValidityMs | number | No | 300000 | Signature validity period in milliseconds |
| signatureAlgorithm | string | No | `'SHA256'` | Signature algorithm (any Node.js crypto supported) |
| signatureDataTemplate | string | No | `'{{signatureTimestamp}}'` | Template for data to sign (`\n` = newline) |
| signatureRefreshBufferMs | number | No | 10% of validityMs | Buffer time before expiry to refresh |
\* Requirements by certificate type:
- **PEM**: Either `privateKeyPath` or `privateKey` required
- **JKS**: Either `keystorePath` or `keystoreContent` required
- **PFX**: Either `pfxPath`, or both `certPath` and `keyPath`, or both `certContent` and `keyContent` required
## Session management
### Server-side session management
When using an HTTP provider with multi-turn redteam attacks like GOAT and Crescendo, you may need to maintain session IDs between rounds. The HTTP provider will automatically extract the session ID from the response headers and store it in the `vars` object.
A session parser is a javascript expression that should be used to extract the session ID from the response headers and returns it. All of the same formats of response parsers are supported.
The input to the session parser is an object `data` with this interface:
```typescript
{
headers?: Record | null;
body?: Record | null;
}
```
Simple header parser:
```yaml
sessionParser: 'data.headers["set-cookie"]'
```
Example extracting the session from the body:
Example Response
```json
{
"responses": [{ "sessionId": "abd-abc", "message": "Bad LLM" }]
}
```
Session Parser value:
```yaml
sessionParser: 'data.body.responses[0]?.sessionId
```
The parser can take a string, file or function like the response parser.
Then you need to set the session ID in the `vars` object for the next round:
```yaml
providers:
- id: https
config:
url: 'https://example.com/api'
headers:
'Cookie': '{{sessionId}}'
```
You can use the `{{sessionId}}` var anywhere in a header or body. Example:
```yaml
providers:
- id: https
config:
url: 'https://example.com/api'
body:
'message': '{{prompt}}'
'sessionId': '{{sessionId}}'
```
Accessing the headers or body:
```yaml
sessionParser: 'data.body.sessionId'
```
```yaml
sessionParser: 'data.headers.["x-session-Id"]'
```
### Client-side session management
If you want the Promptfoo client to send a unique session or conversation ID with each test case, you can add a `transformVars` option to your Promptfoo or redteam config. This is useful for multi-turn evals or multi-turn redteam attacks where the provider maintains a conversation state.
For example:
```yaml
defaultTest:
options:
transformVars: '{ ...vars, sessionId: context.uuid }'
```
Now you can use the `sessionId` variable in your HTTP target config:
```yaml
providers:
- id: https
config:
url: 'https://example.com/api'
headers:
'x-promptfoo-session': '{{sessionId}}'
body:
user_message: '{{prompt}}'
```
## Request Retries
The HTTP provider automatically retries failed requests in the following scenarios:
- Rate limiting (HTTP 429)
- Network failures
- Transient server errors (502, 503, 504, 524)
By default, it will attempt up to 4 retries with exponential backoff. You can configure the maximum number of retries using the `maxRetries` option:
```yaml
providers:
- id: http
config:
url: https://api.example.com/v1/chat
maxRetries: 2 # Override default of 4 retries
```
### Transient Error Handling
Certain server errors are automatically retried when they indicate temporary infrastructure issues:
| Status Code | Description | Retry Condition |
| ----------- | ------------------- | ------------------------------------------- |
| 502 | Bad Gateway | Status text contains "bad gateway" |
| 503 | Service Unavailable | Status text contains "service unavailable" |
| 504 | Gateway Timeout | Status text contains "gateway timeout" |
| 524 | A Timeout Occurred | Status text contains "timeout" (Cloudflare) |
These are retried up to 3 times with exponential backoff (1s, 2s, 4s). The status text check ensures permanent failures (like authentication errors using 5xx codes) are not retried.
### Retrying All Server Errors
By default, only the transient errors above are retried. To enable retries for all 5xx responses:
```bash
PROMPTFOO_RETRY_5XX=true promptfoo eval
```
## Streaming Responses
HTTP streaming allows servers to send responses incrementally as data becomes available, rather than waiting to send a complete response all at once. This is commonly used for LLM APIs to provide real-time token generation, where text appears progressively as the model generates it. Streaming can include both final output text and intermediate reasoning or thinking tokens, depending on the model's capabilities.
Streaming responses typically use one of these formats:
- **Server-Sent Events (SSE)**: Text-based protocol where each line starts with `data: ` followed by JSON. Common in OpenAI and similar APIs.
- **Chunked JSON**: Multiple JSON objects sent sequentially, often separated by newlines or delimiters.
- **HTTP chunked transfer encoding**: Standard HTTP mechanism for streaming arbitrary data.
Promptfoo offers full support for HTTP targets that stream responses in these formats. WebSocket requests are also supported via the [WebSocket Provider](./websocket.md). However, synchronous REST/HTTP requests are often preferable for the following reasons:
- Streaming formats vary widely and often require custom parsing logic in `transformResponse`.
- Evals wait for the full response before scoring, so progressive tokens may not be surfaced.
- Overall test duration is typically similar to non-streaming requests, so streaming does not provide a performance benefit.
If you need to evaluate a streaming endpoint, you will need to configure the `transformResponse` function to parse and reconstruct the final text. For SSE-style responses, you can accumulate chunks from each `data:` line. The logic for extracting each line and determining when the response is complete may vary based on the event types and semantics used by your specific application/provider.
**Example streaming response format:**
A typical Server-Sent Events (SSE) streaming response from OpenAI or similar APIs looks like this:
```
data: {"type":"response.created","response":{"id":"resp_abc123"}}
data: {"type":"response.output_text.delta","delta":"The"}
data: {"type":"response.output_text.delta","delta":" quick"}
data: {"type":"response.output_text.delta","delta":" brown"}
data: {"type":"response.output_text.delta","delta":" fox"}
data: {"type":"response.completed","response_id":"resp_abc123"}
```
Each line starts with `data: ` followed by a JSON object. The parser extracts text from `response.output_text.delta` events and concatenates the `delta` values to reconstruct the full response.
```yaml
providers:
- id: https
config:
url: 'https://api.example.com/v1/responses'
body:
model: 'custom-model'
stream: true
transformResponse: |
(json, text) => {
if (json && (json.output_text || json.response)) {
return json.output_text || json.response;
}
let out = '';
for (const line of String(text || '').split('\n')) {
const trimmed = line.trim();
if (!trimmed.startsWith('data: ')) continue;
try {
const evt = JSON.parse(trimmed.slice(6));
if (evt.type === 'response.output_text.delta' && typeof evt.delta === 'string') {
out += evt.delta;
}
} catch {}
}
return out.trim();
}
```
This parser would extract `"The quick brown fox"` from the example response above.
## Reference
Supported config options:
| Option | Type | Description |
| ----------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| url | string | The URL to send the HTTP request to. Supports Nunjucks templates. If not provided, the `id` of the provider will be used as the URL. |
| request | string | A raw HTTP request to send. This will override the `url`, `method`, `headers`, `body`, and `queryParams` options. |
| method | string | HTTP method (GET, POST, etc). Defaults to POST if body is provided, GET otherwise. |
| headers | Record\ | Key-value pairs of HTTP headers to include in the request. |
| body | object \| string | The request body. For POST requests, objects are automatically stringified as JSON. |
| multipart | object | Multipart form configuration with ordered `parts`. Supports text fields, local file uploads, and generated PDF/PNG/JPEG documents. |
| queryParams | Record\ | Key-value pairs of query parameters to append to the URL. |
| transformRequest | string \| Function | A function, string template, or file path to transform the prompt before sending it to the API. |
| transformResponse | string \| Function | Transforms the API response using a JavaScript expression (e.g., 'json.result'), function, or file path (e.g., 'file://parser.js'). Replaces the deprecated `responseParser` field. |
| tokenEstimation | object | Configuration for optional token usage estimation. See Token Estimation section above for details. |
| maxRetries | number | Maximum number of retry attempts for failed requests. Defaults to 4. |
| validateStatus | string \| Function | A function or string expression that returns true if the status code should be treated as successful. By default, accepts all status codes. |
| auth | object | Authentication configuration (bearer, api_key, basic, oauth, or file). See [Authentication](#authentication) section. |
| signatureAuth | object | Digital signature authentication configuration. See [Digital Signature Authentication](#digital-signature-authentication) section. |
| tls | object | Configuration for TLS/HTTPS connections including client certificates, CA certificates, and cipher settings. See TLS Configuration Options above. |
In addition to a full URL, the provider `id` field accepts `http` or `https` as values.
## Error Handling
The HTTP provider throws errors for:
- Network errors or request failures
- Invalid response parsing
- Session parsing errors
- Invalid request configurations
- Status codes that fail the configured validation (if `validateStatus` is set)
By default, all response status codes are accepted. This accommodates APIs that return valid responses with non-2xx codes (common with guardrails and content filtering). You can customize this using the `validateStatus` option:
```yaml
providers:
- id: https
config:
url: 'https://example.com/api'
# Function-based validation
validateStatus: (status) => status < 500 # Accept any status below 500
# Or string-based expression
validateStatus: 'status >= 200 && status <= 299' # Accept only 2xx responses
# Or load from file
validateStatus: 'file://validators/status.js' # Load default export
validateStatus: 'file://validators/status.js:validateStatus' # Load specific function
```
Example validator file (`validators/status.js`):
```javascript
export default (status) => status < 500;
// Or named export
export function validateStatus(status) {
return status < 500;
}
```
The provider automatically retries certain errors (like rate limits) based on `maxRetries`, while other errors are thrown immediately.