-
Notifications
You must be signed in to change notification settings - Fork 727
fix(server): forward model auth header and preserve precedence #1883
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -138,13 +138,20 @@ async def fetch_models( | |||||||||||||||||||||||
| api_key_env = provider.get("api_key_env", "OPENAI_API_KEY") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| headers: Dict[str, str] = {} | ||||||||||||||||||||||||
| forwarded = request_headers.get("Authorization", "") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| forwarded = next( | ||||||||||||||||||||||||
| (value for name, value in request_headers.items() if name.lower() == "authorization"), | ||||||||||||||||||||||||
| "", | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| raw_key = os.environ.get(api_key_env, "") | ||||||||||||||||||||||||
| if not raw_key: | ||||||||||||||||||||||||
| raw_key = forwarded.removeprefix("Bearer ").strip() if forwarded else "" | ||||||||||||||||||||||||
| if raw_key: | ||||||||||||||||||||||||
| headers[auth_header_name] = f"Bearer {raw_key}" if use_bearer else raw_key | ||||||||||||||||||||||||
| elif forwarded: | ||||||||||||||||||||||||
| if auth_header_name == "Authorization" and use_bearer: | ||||||||||||||||||||||||
| headers[auth_header_name] = forwarded | ||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||
| raw_key = forwarded.removeprefix("Bearer ").strip() | ||||||||||||||||||||||||
| if raw_key: | ||||||||||||||||||||||||
| headers[auth_header_name] = f"Bearer {raw_key}" if use_bearer else raw_key | ||||||||||||||||||||||||
|
Comment on lines
+148
to
+154
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Prompt To Fix With AIThis is a comment left during a code review.
Path: nemoguardrails/server/schemas/utils.py
Line: 148-154
Comment:
The shortcut branch for the OpenAI-compatible case only triggers when both `auth_header_name == "Authorization"` **and** `use_bearer` are true, so the forwarded value is passed through verbatim. If a client sends a non-Bearer scheme (e.g. `Authorization: ApiKey xyz`), that value lands in `headers["Authorization"]` unmodified and the upstream provider will likely reject the request with a 401. Stripping and re-wrapping with `Bearer` (as the `else` branch already does) would make the forwarding consistent with the env-key path.
```suggestion
elif forwarded:
raw_key = forwarded.removeprefix("Bearer ").strip()
if raw_key:
headers[auth_header_name] = f"Bearer {raw_key}" if use_bearer else raw_key
```
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| headers.update(provider.get("extra_headers", {})) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -450,13 +450,41 @@ async def test_fetch_unknown_engine_no_base_url(): | |||||
| async def test_fetch_auth_forwarded(): | ||||||
| """Incoming Authorization header is forwarded for OpenAI-compatible providers.""" | ||||||
| mock = _mock_httpx({"data": []}) | ||||||
| with patch.dict(os.environ, {"MAIN_MODEL_BASE_URL": "http://localhost:8000"}): | ||||||
| with patch.dict(os.environ, {"MAIN_MODEL_BASE_URL": "http://localhost:8000", "OPENAI_API_KEY": ""}): | ||||||
| with patch("httpx.AsyncClient", return_value=mock): | ||||||
| await fetch_models("openai", {"Authorization": "Bearer user-token"}) | ||||||
| call_headers = mock.get.call_args.kwargs["headers"] | ||||||
| assert call_headers["Authorization"] == "Bearer user-token" | ||||||
|
|
||||||
|
|
||||||
| @pytest.mark.asyncio | ||||||
| async def test_fetch_env_key_precedes_forwarded_auth(): | ||||||
| """Configured OpenAI key is used before a caller-supplied placeholder token.""" | ||||||
| mock = _mock_httpx({"data": []}) | ||||||
| with patch.dict( | ||||||
| os.environ, | ||||||
| { | ||||||
| "MAIN_MODEL_BASE_URL": "http://localhost:8000", | ||||||
| "OPENAI_API_KEY": "sk-configured-key", | ||||||
| }, | ||||||
| ): | ||||||
| with patch("httpx.AsyncClient", return_value=mock): | ||||||
| await fetch_models("openai", {"Authorization": "Bearer not-used"}) | ||||||
| call_headers = mock.get.call_args.kwargs["headers"] | ||||||
| assert call_headers["Authorization"] == "Bearer sk-configured-key" | ||||||
|
|
||||||
|
|
||||||
| @pytest.mark.asyncio | ||||||
| async def test_fetch_non_openai_provider_uses_forwarded_auth_without_env_key(): | ||||||
| """Forwarded auth falls back to provider-specific auth headers when env key is unset.""" | ||||||
| mock = _mock_httpx({"data": []}) | ||||||
| with patch.dict(os.environ, {"ANTHROPIC_API_KEY": ""}): | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Prompt To Fix With AIThis is a comment left during a code review.
Path: tests/server/test_schema_utils.py
Line: 481
Comment:
The Anthropic provider constructs its URL using `MAIN_MODEL_BASE_URL` when that variable is set. If a test environment (or a previous test) leaves `MAIN_MODEL_BASE_URL` set, this test would hit a different URL than `https://api.anthropic.com/v1/models`. Since `httpx.AsyncClient` is fully mocked, the URL doesn't affect the assertion, but explicitly clearing the variable documents the intent and guards against subtle breakage if the mock scope ever narrows.
```suggestion
with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "", "MAIN_MODEL_BASE_URL": ""}):
```
How can I resolve this? If you propose a fix, please make it concise. |
||||||
| with patch("httpx.AsyncClient", return_value=mock): | ||||||
| await fetch_models("anthropic", {"Authorization": "Bearer user-token"}) | ||||||
| call_headers = mock.get.call_args.kwargs["headers"] | ||||||
| assert call_headers["x-api-key"] == "user-token" | ||||||
|
|
||||||
|
|
||||||
| @pytest.mark.asyncio | ||||||
| async def test_fetch_non_dict_items_skipped(): | ||||||
| """Non-dict items in the response data are skipped.""" | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle case-insensitive bearer prefix normalization.
At Line 152,
removeprefix("Bearer ")is case-sensitive. A valid header likeauthorization: bearer user-tokenwon’t be stripped and can become an invalid provider key (e.g.,x-api-key: bearer user-token).Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents