Skip to content

Commit 4b946ae

Browse files
committed
feat: Add CLI options for auth and ssl verify
Adds command-line options to the "guidellm benchmark" command to support custom authentication headers and skipping SSL verification when communicating with the target system. New options: --target-header: Allows specifying a custom header(s) for the target. --target-skip-ssl-verify: A flag to disable SSL certificate verification for the target. These options provide more flexibility for benchmarking targets in various environments, such as those with self-signed certificates or custom authentication mechanisms. The names were chosen to align with the existing --target argument to make it clear these apply to requests made to the target. The implementation now follows the following precedence for setting request headers: 1. Headers from the `--target-header` CLI option (highest priority). 2. Headers defined in a `.env` file or as environment variables. 3. Default headers derived from other parameters like `api_key`, `organization`, or `project` (lowest priority). This commit also adds documentation and tests for these new features, covering both CLI usage and configuration via environment variables or a `.env` file.
1 parent f1f8ca8 commit 4b946ae

File tree

9 files changed

+221
-21
lines changed

9 files changed

+221
-21
lines changed

docs/guides/cli.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,17 @@
1-
# Coming Soon
1+
# CLI Reference
2+
3+
This page provides a reference for the `guidellm` command-line interface. For more advanced configuration, including environment variables and `.env` files, see the [Configuration Guide](./configuration.md).
4+
5+
## `guidellm benchmark run`
6+
7+
This command is the primary entrypoint for running benchmarks.
8+
9+
### Target Configuration
10+
11+
These options configure how `guidellm` connects to the system under test.
12+
13+
| Option | Description |
14+
| --- | --- |
15+
| `--target <URL>` | **Required.** The endpoint of the target system, e.g., `http://localhost:8080`. |
16+
| `--target-header <HEADER>` | A header to send with requests to the target. This option can be specified multiple times to send multiple headers. The header should be in the format `"Header-Name: Header-Value"`. For example: `--target-header "Authorization: Bearer my-secret-token"` |
17+
| `--target-skip-ssl-verify` | A flag to disable SSL certificate verification when connecting to the target. This is useful for development environments with self-signed certificates, but should be used with caution in production. |

docs/guides/configuration.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,42 @@
1-
# Coming Soon
1+
# Configuration
2+
3+
The `guidellm` application can be configured using command-line arguments, environment variables, or a `.env` file. This page details the file-based and environment variable configuration options.
4+
5+
## Configuration Methods
6+
7+
Settings are loaded with the following priority (highest priority first):
8+
1. Command-line arguments.
9+
2. Environment variables.
10+
3. Values in a `.env` file in the directory where the command is run.
11+
4. Default values.
12+
13+
## Environment Variable Format
14+
15+
All settings can be configured using environment variables. The variables must be prefixed with `GUIDELLM__`, and nested settings are separated by a double underscore `__`.
16+
17+
For example, to set the `api_key` for the `openai` backend, you would use the following environment variable:
18+
```bash
19+
export GUIDELLM__OPENAI__API_KEY="your-api-key"
20+
```
21+
22+
### Target Configuration
23+
24+
You can configure the connection to the target system using environment variables. This is an alternative to using the `--target-*` command-line flags.
25+
26+
| Environment Variable | Description | Example |
27+
| --- | --- | --- |
28+
| `GUIDELLM__OPENAI__HEADERS` | A JSON string representing a dictionary of headers to send to the target. These headers will override any default headers (like `Authorization` from `api_key`). | `export GUIDELLM__OPENAI__HEADERS='{"Authorization": "Bearer my-token", "X-Custom-Header": "value"}'` |
29+
| `GUIDELLM__OPENAI__ORGANIZATION` | The OpenAI organization to use for requests. | `export GUIDELLM__OPENAI__ORGANIZATION="org-12345"` |
30+
| `GUIDELLM__OPENAI__PROJECT` | The OpenAI project to use for requests. | `export GUIDELLM__OPENAI__PROJECT="proj-67890"` |
31+
| `GUIDELLM__OPENAI__VERIFY_SSL` | Set to `false` or `0` to disable SSL certificate verification. | `export GUIDELLM__OPENAI__VERIFY_SSL=false` |
32+
33+
### Using a `.env` file
34+
35+
You can also place these variables in a `.env` file in your project's root directory:
36+
37+
```dotenv
38+
# .env file
39+
GUIDELLM__OPENAI__API_KEY="your-api-key"
40+
GUIDELLM__OPENAI__HEADERS='{"Authorization": "Bearer my-token"}'
41+
GUIDELLM__OPENAI__VERIFY_SSL=false
42+
```

src/guidellm/__main__.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
)
1414
from guidellm.benchmark.entrypoints import benchmark_with_scenario
1515
from guidellm.benchmark.scenario import GenerativeTextScenario, get_builtin_scenarios
16-
from guidellm.config import print_config
16+
from guidellm.config import print_config, settings
1717
from guidellm.preprocess.dataset import ShortPromptStrategy, process_dataset
1818
from guidellm.scheduler import StrategyType
1919
from guidellm.utils import DefaultGroupHandler
@@ -85,6 +85,18 @@ def benchmark():
8585
"dict with **kwargs."
8686
),
8787
)
88+
@click.option(
89+
"--target-header",
90+
"target_headers",
91+
multiple=True,
92+
help="A header to send to the target, e.g., --target-header 'Authorization: Bearer <token>'. Can be specified multiple times.",
93+
)
94+
@click.option(
95+
"--target-skip-ssl-verify",
96+
is_flag=True,
97+
default=False,
98+
help="Skip SSL certificate verification when sending requests to the target.",
99+
)
88100
@click.option(
89101
"--model",
90102
default=GenerativeTextScenario.get_default("model"),
@@ -249,6 +261,8 @@ def run(
249261
target,
250262
backend_type,
251263
backend_args,
264+
target_headers,
265+
target_skip_ssl_verify,
252266
model,
253267
processor,
254268
processor_args,
@@ -271,6 +285,21 @@ def run(
271285
):
272286
click_ctx = click.get_current_context()
273287

288+
if target_headers:
289+
headers = {}
290+
for header in target_headers:
291+
if ":" not in header:
292+
raise click.BadParameter(
293+
f"Invalid header format: {header}. Expected 'Key: Value'.",
294+
ctx=click_ctx,
295+
param_hint="--target-header",
296+
)
297+
key, value = header.split(":", 1)
298+
headers[key.strip()] = value.strip()
299+
settings.openai.headers = headers
300+
if target_skip_ssl_verify:
301+
settings.openai.verify_ssl = False
302+
274303
overrides = cli_tools.set_if_not_default(
275304
click_ctx,
276305
target=target,

src/guidellm/backend/openai.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -110,20 +110,36 @@ def __init__(
110110

111111
self._model = model
112112

113+
# Start with default headers based on other params
114+
default_headers: dict[str, str] = {}
113115
api_key = api_key or settings.openai.api_key
114-
self.authorization = (
115-
f"Bearer {api_key}" if api_key else settings.openai.bearer_token
116-
)
116+
bearer_token = settings.openai.bearer_token
117+
if api_key:
118+
default_headers["Authorization"] = f"Bearer {api_key}"
119+
elif bearer_token:
120+
default_headers["Authorization"] = bearer_token
117121

118122
self.organization = organization or settings.openai.organization
123+
if self.organization:
124+
default_headers["OpenAI-Organization"] = self.organization
125+
119126
self.project = project or settings.openai.project
127+
if self.project:
128+
default_headers["OpenAI-Project"] = self.project
129+
130+
# User-provided headers from CLI override defaults
131+
user_headers = settings.openai.headers or {}
132+
default_headers.update(user_headers)
133+
self.headers = default_headers
134+
120135
self.timeout = timeout if timeout is not None else settings.request_timeout
121136
self.http2 = http2 if http2 is not None else settings.request_http2
122137
self.follow_redirects = (
123138
follow_redirects
124139
if follow_redirects is not None
125140
else settings.request_follow_redirects
126141
)
142+
self.verify_ssl = settings.openai.verify_ssl
127143
self.max_output_tokens = (
128144
max_output_tokens
129145
if max_output_tokens is not None
@@ -160,9 +176,7 @@ def info(self) -> dict[str, Any]:
160176
"timeout": self.timeout,
161177
"http2": self.http2,
162178
"follow_redirects": self.follow_redirects,
163-
"authorization": bool(self.authorization),
164-
"organization": self.organization,
165-
"project": self.project,
179+
"headers": self.headers,
166180
"text_completions_path": TEXT_COMPLETIONS_PATH,
167181
"chat_completions_path": CHAT_COMPLETIONS_PATH,
168182
}
@@ -383,6 +397,7 @@ def _get_async_client(self) -> httpx.AsyncClient:
383397
http2=self.http2,
384398
timeout=self.timeout,
385399
follow_redirects=self.follow_redirects,
400+
verify=self.verify_ssl,
386401
)
387402
self._async_client = client
388403
else:
@@ -394,16 +409,7 @@ def _headers(self) -> dict[str, str]:
394409
headers = {
395410
"Content-Type": "application/json",
396411
}
397-
398-
if self.authorization:
399-
headers["Authorization"] = self.authorization
400-
401-
if self.organization:
402-
headers["OpenAI-Organization"] = self.organization
403-
404-
if self.project:
405-
headers["OpenAI-Project"] = self.project
406-
412+
headers.update(self.headers)
407413
return headers
408414

409415
def _params(self, endpoint_type: EndpointType) -> dict[str, str]:

src/guidellm/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,12 @@ class OpenAISettings(BaseModel):
8181

8282
api_key: Optional[str] = None
8383
bearer_token: Optional[str] = None
84+
headers: Optional[dict[str, str]] = None
8485
organization: Optional[str] = None
8586
project: Optional[str] = None
8687
base_url: str = "http://localhost:8000"
8788
max_output_tokens: int = 16384
89+
verify_ssl: bool = True
8890

8991

9092
class Settings(BaseSettings):

tests/unit/backend/test_openai_backend.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def test_openai_http_backend_default_initialization():
1111
backend = OpenAIHTTPBackend()
1212
assert backend.target == settings.openai.base_url
1313
assert backend.model is None
14-
assert backend.authorization == settings.openai.bearer_token
14+
assert backend.headers.get("Authorization") == settings.openai.bearer_token
1515
assert backend.organization == settings.openai.organization
1616
assert backend.project == settings.openai.project
1717
assert backend.timeout == settings.request_timeout
@@ -37,7 +37,7 @@ def test_openai_http_backend_intialization():
3737
)
3838
assert backend.target == "http://test-target"
3939
assert backend.model == "test-model"
40-
assert backend.authorization == "Bearer test-key"
40+
assert backend.headers.get("Authorization") == "Bearer test-key"
4141
assert backend.organization == "test-org"
4242
assert backend.project == "test-proj"
4343
assert backend.timeout == 10
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import pytest
2+
3+
from guidellm.backend import OpenAIHTTPBackend
4+
from guidellm.config import settings
5+
6+
7+
@pytest.mark.smoke
8+
def test_openai_http_backend_default_initialization():
9+
backend = OpenAIHTTPBackend()
10+
assert backend.verify_ssl is True
11+
12+
13+
@pytest.mark.smoke
14+
def test_openai_http_backend_custom_ssl_verification():
15+
settings.openai.verify_ssl = False
16+
backend = OpenAIHTTPBackend()
17+
assert backend.verify_ssl is False
18+
# Reset the setting
19+
settings.openai.verify_ssl = True
20+
21+
22+
@pytest.mark.smoke
23+
def test_openai_http_backend_custom_headers_override():
24+
# Set a default api_key, which would normally create an Authorization header
25+
settings.openai.api_key = "default-api-key"
26+
27+
# Set custom headers that override the default Authorization and add a new header
28+
openshift_token = "Bearer sha256~xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
29+
override_headers = {
30+
"Authorization": openshift_token,
31+
"Custom-Header": "Custom-Value",
32+
}
33+
settings.openai.headers = override_headers
34+
35+
# Initialize the backend
36+
backend = OpenAIHTTPBackend()
37+
38+
# Check that the override headers are used
39+
assert backend.headers["Authorization"] == openshift_token
40+
assert backend.headers["Custom-Header"] == "Custom-Value"
41+
assert len(backend.headers) == 2
42+
43+
# Reset the settings
44+
settings.openai.api_key = None
45+
settings.openai.headers = None

tests/unit/test_config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,13 @@ def test_settings_with_env_variables(mocker):
142142
"GUIDELLM__DATASET__PREFERRED_DATA_COLUMNS": '["custom_column"]',
143143
"GUIDELLM__OPENAI__API_KEY": "env_api_key",
144144
"GUIDELLM__TABLE_BORDER_CHAR": "*",
145+
"GUIDELLM__OPENAI__HEADERS": '{"Authorization": "Bearer env-token"}',
146+
"GUIDELLM__OPENAI__VERIFY_SSL": "false",
145147
},
146148
)
147149
settings = Settings()
148150
assert settings.dataset.preferred_data_columns == ["custom_column"]
149151
assert settings.openai.api_key == "env_api_key"
150152
assert settings.table_border_char == "*"
153+
assert settings.openai.headers == {"Authorization": "Bearer env-token"}
154+
assert settings.openai.verify_ssl is False

tests/unit/test_main.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import pytest
2+
from click.testing import CliRunner
3+
4+
from guidellm.__main__ import cli
5+
6+
7+
@pytest.mark.smoke
8+
def test_benchmark_run_invalid_header_format():
9+
runner = CliRunner()
10+
result = runner.invoke(
11+
cli,
12+
[
13+
"benchmark",
14+
"run",
15+
"--target-header",
16+
"invalid-header",
17+
"--target",
18+
"http://localhost:8000",
19+
"--data",
20+
"prompt_tokens=1,output_tokens=1",
21+
"--rate-type",
22+
"constant",
23+
"--rate",
24+
"1",
25+
"--max-requests",
26+
"1",
27+
],
28+
)
29+
assert result.exit_code != 0
30+
assert "Invalid header format" in result.output
31+
32+
33+
@pytest.mark.smoke
34+
def test_benchmark_run_valid_header_format():
35+
runner = CliRunner()
36+
result = runner.invoke(
37+
cli,
38+
[
39+
"benchmark",
40+
"run",
41+
"--target-header",
42+
"Authorization: Bearer my-token",
43+
"--target",
44+
"http://localhost:8000",
45+
"--data",
46+
"prompt_tokens=1,output_tokens=1",
47+
"--rate-type",
48+
"constant",
49+
"--rate",
50+
"1",
51+
"--max-requests",
52+
"1",
53+
],
54+
)
55+
# This will fail because it can't connect to the server,
56+
# but it will pass the header parsing, which is what we want to test.
57+
assert "Invalid header format" not in result.output

0 commit comments

Comments
 (0)