Skip to content

Add custom headers from the env var LLM_EXTRA_HEADERS#476

Open
Luyansi3 wants to merge 1 commit into
usestrix:mainfrom
Luyansi3:feat/custom_headers
Open

Add custom headers from the env var LLM_EXTRA_HEADERS#476
Luyansi3 wants to merge 1 commit into
usestrix:mainfrom
Luyansi3:feat/custom_headers

Conversation

@Luyansi3
Copy link
Copy Markdown

@Luyansi3 Luyansi3 commented May 5, 2026

Summary

Add support for passing custom HTTP headers to every LiteLLM request via a new LLM_EXTRA_HEADERS environment variable (JSON object). This enables custom API base / gateway setups that require multiple credentials (e.g. gateway key + model key) or additional enterprise headers.

Fixes #466

What changed

  • New config: LLM_EXTRA_HEADERS (string) added to Strix config (Config.llm_extra_headers).
  • Validation: strix/interface/main.py now validates LLM_EXTRA_HEADERS at startup and shows a clear error panel with an example when invalid.
  • LLM config resolution: resolve_llm_config() now returns (model, api_key, api_base, extra_headers) and parses LLM_EXTRA_HEADERS as JSON (must be an object).
  • LiteLLM calls: extra_headers is forwarded to litellm.completion() in:
    • warm-up (warm_up_llm)
    • dedupe (check_duplicate)
    • memory compressor (_summarize_messages)
    • main LLM wrapper (strix/llm/llm.py) via LLMConfig.extra_headers
  • Docs: docs/advanced/configuration.mdx documents LLM_EXTRA_HEADERS with a JSON example.

Usage

Set LLM_EXTRA_HEADERS to a JSON object of headers:

export LLM_EXTRA_HEADERS='{"x-gateway-api-key":"<gateway-key>","x-model-api-key":"<model-key>"}

Notes

  • LLM_EXTRA_HEADERS must be a valid JSON object (non-dict values are rejected).
  • Values are treated as configuration/secrets and are not printed as part of the validation error.

Testing

  • Manual: start Strix with a valid LLM_EXTRA_HEADERS and confirm requests succeed against a custom api_base.
  • Manual: set an invalid LLM_EXTRA_HEADERS value and confirm Strix exits with a clear error message and example.

Copilot AI review requested due to automatic review settings May 5, 2026 16:25
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 5, 2026

Greptile Summary

This PR adds support for passing custom HTTP headers to every LiteLLM request via a new LLM_EXTRA_HEADERS environment variable (JSON object), enabling gateway/enterprise setups that need extra credentials or headers. The extra_headers value is parsed in resolve_llm_config() and forwarded to all five LiteLLM call sites.

  • Two related bugs in error handling: TypeError (raised when the JSON value is a non-dict) is not caught by except (json.JSONDecodeError, ValueError) in validate_environment() — and the same uncaught TypeError propagates from resolve_llm_config() to every call site. Setting LLM_EXTRA_HEADERS=42 or LLM_EXTRA_HEADERS=true bypasses the designed error panel and produces an unhandled traceback at startup.
  • Shell quoting in the example: The error panel example (export LLM_EXTRA_HEADERS={"x-my-header": "value"}) is missing single quotes, so a user who copies it will get a malformed assignment in most shells.

Confidence Score: 3/5

Not safe to merge as-is — supplying a syntactically valid but non-object JSON value (e.g. a number or boolean) triggers an unhandled exception at startup instead of the designed error panel.

Both validate_environment() in main.py and resolve_llm_config() in config.py raise TypeError for non-dict JSON but catch only json.JSONDecodeError and ValueError. The fix is a one-line change in each place, but the bug affects the primary startup validation path and every LiteLLM call site.

strix/interface/main.py and strix/config/config.py — both contain the mismatched exception handling for the non-dict case.

Important Files Changed

Filename Overview
strix/config/config.py Adds llm_extra_headers config field and extends resolve_llm_config() to parse and return extra_headers; the non-dict TypeError is not caught, propagating to all callers as an unhandled exception.
strix/interface/main.py Adds startup validation for LLM_EXTRA_HEADERS and threads extra_headers into warm_up_llm; the except clause misses TypeError, so non-dict JSON values bypass the error panel and crash with an unhandled traceback. The example command is also missing shell quoting.
strix/llm/config.py Unpacks the new extra_headers return value from resolve_llm_config() and stores it on LLMConfig; straightforward change with no issues.
strix/llm/llm.py Forwards self.config.extra_headers to litellm.completion() when set; clean, consistent with the pattern for api_key and api_base.
strix/llm/dedupe.py Unpacks extra_headers from resolve_llm_config() and forwards it to the dedupe LiteLLM call; no issues.
strix/llm/memory_compressor.py Unpacks extra_headers from resolve_llm_config() and forwards it to the summarization LiteLLM call; no issues.
docs/advanced/configuration.mdx Adds LLM_EXTRA_HEADERS documentation entry with type and JSON example; accurate and consistent with the rest of the doc.
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
strix/interface/main.py:92
The `TypeError` raised when `parsed` is not a dict (line 91) is not caught by `except (json.JSONDecodeError, ValueError)`, because `TypeError` is not a subclass of `ValueError`. When `LLM_EXTRA_HEADERS` is set to a valid JSON non-object (e.g. `42`, `true`, `"string"`), the `TypeError` escapes the try/except entirely, bypasses the nice error panel, and surfaces as an unhandled exception traceback instead of calling `sys.exit(1)` with a user-friendly message.

```suggestion
        except (json.JSONDecodeError, TypeError, ValueError) as e:
```

### Issue 2 of 3
strix/interface/main.py:99-102
The example shell command is missing single quotes around the JSON value. Without them, the shell will interpret the spaces inside `{}` as argument separators, so copy-pasting this command will produce an incorrect assignment (the variable will only receive `{x-my-header:` and the rest will be treated as separate shell words).

```suggestion
            error_text.append(
                "export LLM_EXTRA_HEADERS='{\"x-my-header\": \"value\"}'\n",
                style="dim white",
            )
```

### Issue 3 of 3
strix/config/config.py:233-238
When `parsed` is valid JSON but not a `dict`, the `TypeError` is raised inside the `try` block but is not caught by `except json.JSONDecodeError`. It propagates to every call site (`warm_up_llm`, `check_duplicate`, `_summarize_messages`, `LLMConfig.__init__`) as an unhandled exception. Raising `ValueError` instead (consistent with how `json.JSONDecodeError` is already handled) makes the contract uniform and lets callers handle one exception type.

```suggestion
            if isinstance(parsed, dict):
                extra_headers = {str(k): str(v) for k, v in parsed.items() if v is not None}
            else:
                raise ValueError("LLM_EXTRA_HEADERS must be a JSON object, got a non-dict value")
        except json.JSONDecodeError as e:
            raise ValueError(f"Invalid LLM_EXTRA_HEADERS JSON: {e}") from e
```

Reviews (1): Last reviewed commit: "Add custom headers from the env var LLM_..." | Re-trigger Greptile

Comment thread strix/interface/main.py
parsed = json.loads(raw_headers)
if not isinstance(parsed, dict):
raise TypeError("LLM_EXTRA_HEADERS must be a JSON object, got a non-dict value")
except (json.JSONDecodeError, ValueError) as e:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The TypeError raised when parsed is not a dict (line 91) is not caught by except (json.JSONDecodeError, ValueError), because TypeError is not a subclass of ValueError. When LLM_EXTRA_HEADERS is set to a valid JSON non-object (e.g. 42, true, "string"), the TypeError escapes the try/except entirely, bypasses the nice error panel, and surfaces as an unhandled exception traceback instead of calling sys.exit(1) with a user-friendly message.

Suggested change
except (json.JSONDecodeError, ValueError) as e:
except (json.JSONDecodeError, TypeError, ValueError) as e:
Prompt To Fix With AI
This is a comment left during a code review.
Path: strix/interface/main.py
Line: 92

Comment:
The `TypeError` raised when `parsed` is not a dict (line 91) is not caught by `except (json.JSONDecodeError, ValueError)`, because `TypeError` is not a subclass of `ValueError`. When `LLM_EXTRA_HEADERS` is set to a valid JSON non-object (e.g. `42`, `true`, `"string"`), the `TypeError` escapes the try/except entirely, bypasses the nice error panel, and surfaces as an unhandled exception traceback instead of calling `sys.exit(1)` with a user-friendly message.

```suggestion
        except (json.JSONDecodeError, TypeError, ValueError) as e:
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread strix/interface/main.py
Comment on lines +99 to +102
error_text.append(
'export LLM_EXTRA_HEADERS={"x-my-header": "value"}\n',
style="dim white",
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The example shell command is missing single quotes around the JSON value. Without them, the shell will interpret the spaces inside {} as argument separators, so copy-pasting this command will produce an incorrect assignment (the variable will only receive {x-my-header: and the rest will be treated as separate shell words).

Suggested change
error_text.append(
'export LLM_EXTRA_HEADERS={"x-my-header": "value"}\n',
style="dim white",
)
error_text.append(
"export LLM_EXTRA_HEADERS='{\"x-my-header\": \"value\"}'\n",
style="dim white",
)
Prompt To Fix With AI
This is a comment left during a code review.
Path: strix/interface/main.py
Line: 99-102

Comment:
The example shell command is missing single quotes around the JSON value. Without them, the shell will interpret the spaces inside `{}` as argument separators, so copy-pasting this command will produce an incorrect assignment (the variable will only receive `{x-my-header:` and the rest will be treated as separate shell words).

```suggestion
            error_text.append(
                "export LLM_EXTRA_HEADERS='{\"x-my-header\": \"value\"}'\n",
                style="dim white",
            )
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread strix/config/config.py
Comment on lines +233 to +238
if isinstance(parsed, dict):
extra_headers = {str(k): str(v) for k, v in parsed.items() if v is not None}
else:
raise TypeError("LLM_EXTRA_HEADERS must be a JSON object")
except json.JSONDecodeError as e:
raise ValueError(f"Invalid LLM_EXTRA_HEADERS JSON: {e}") from e
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 When parsed is valid JSON but not a dict, the TypeError is raised inside the try block but is not caught by except json.JSONDecodeError. It propagates to every call site (warm_up_llm, check_duplicate, _summarize_messages, LLMConfig.__init__) as an unhandled exception. Raising ValueError instead (consistent with how json.JSONDecodeError is already handled) makes the contract uniform and lets callers handle one exception type.

Suggested change
if isinstance(parsed, dict):
extra_headers = {str(k): str(v) for k, v in parsed.items() if v is not None}
else:
raise TypeError("LLM_EXTRA_HEADERS must be a JSON object")
except json.JSONDecodeError as e:
raise ValueError(f"Invalid LLM_EXTRA_HEADERS JSON: {e}") from e
if isinstance(parsed, dict):
extra_headers = {str(k): str(v) for k, v in parsed.items() if v is not None}
else:
raise ValueError("LLM_EXTRA_HEADERS must be a JSON object, got a non-dict value")
except json.JSONDecodeError as e:
raise ValueError(f"Invalid LLM_EXTRA_HEADERS JSON: {e}") from e
Prompt To Fix With AI
This is a comment left during a code review.
Path: strix/config/config.py
Line: 233-238

Comment:
When `parsed` is valid JSON but not a `dict`, the `TypeError` is raised inside the `try` block but is not caught by `except json.JSONDecodeError`. It propagates to every call site (`warm_up_llm`, `check_duplicate`, `_summarize_messages`, `LLMConfig.__init__`) as an unhandled exception. Raising `ValueError` instead (consistent with how `json.JSONDecodeError` is already handled) makes the contract uniform and lets callers handle one exception type.

```suggestion
            if isinstance(parsed, dict):
                extra_headers = {str(k): str(v) for k, v in parsed.items() if v is not None}
            else:
                raise ValueError("LLM_EXTRA_HEADERS must be a JSON object, got a non-dict value")
        except json.JSONDecodeError as e:
            raise ValueError(f"Invalid LLM_EXTRA_HEADERS JSON: {e}") from e
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds LLM_EXTRA_HEADERS so Strix can attach user-supplied HTTP headers to LiteLLM requests, which is useful for proxy/gateway setups that require additional credentials or custom routing headers.

Changes:

  • Added LLM_EXTRA_HEADERS to config resolution and LLMConfig.
  • Forwarded extra headers to warm-up, dedupe, memory compression, and main LLM request paths.
  • Added startup validation and documentation for the new environment variable.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
strix/llm/memory_compressor.py Passes resolved extra headers into memory summarization requests.
strix/llm/llm.py Includes extra_headers in main completion request arguments.
strix/llm/dedupe.py Forwards extra headers in duplicate-check LLM calls.
strix/llm/config.py Stores resolved extra_headers on LLMConfig.
strix/interface/main.py Validates LLM_EXTRA_HEADERS at startup and uses it during LLM warm-up.
strix/config/config.py Adds the new config key and extends resolve_llm_config() to parse/return headers.
docs/advanced/configuration.mdx Documents the new LLM_EXTRA_HEADERS setting.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread strix/interface/main.py
try:
parsed = json.loads(raw_headers)
if not isinstance(parsed, dict):
raise TypeError("LLM_EXTRA_HEADERS must be a JSON object, got a non-dict value")
Comment thread strix/interface/main.py
error_text.append(f"Error: {e}\n", style="white")
error_text.append("\nExample:\n", style="white")
error_text.append(
'export LLM_EXTRA_HEADERS={"x-my-header": "value"}\n',
Comment thread strix/config/config.py
Comment on lines 212 to +214
model = Config.get("strix_llm")
if not model:
return None, None, None
return None, None, None, None
Comment thread strix/config/config.py
if isinstance(parsed, dict):
extra_headers = {str(k): str(v) for k, v in parsed.items() if v is not None}
else:
raise TypeError("LLM_EXTRA_HEADERS must be a JSON object")
Comment thread strix/config/config.py
Comment on lines +233 to +239
if isinstance(parsed, dict):
extra_headers = {str(k): str(v) for k, v in parsed.items() if v is not None}
else:
raise TypeError("LLM_EXTRA_HEADERS must be a JSON object")
except json.JSONDecodeError as e:
raise ValueError(f"Invalid LLM_EXTRA_HEADERS JSON: {e}") from e

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Allow injection of custom headers for custom models via Litellm

2 participants