Skip to content

Commit fe2ddc9

Browse files
authored
Merge pull request trustyai-explainability#32 from trustyai-explainability/develop
Sync with trustyai develop
2 parents 7ad8471 + a07460b commit fe2ddc9

11 files changed

Lines changed: 1214 additions & 16 deletions

File tree

Dockerfile.server

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ ENV HF_HOME=/app/.cache/huggingface \
6969
FASTEMBED_CACHE_PATH=/app/.cache/fastembed \
7070
PATH="/app/.venv/bin:$PATH" \
7171
PYTHONUNBUFFERED=1 \
72-
PYTHONDONTWRITEBYTECODE=1
72+
PYTHONDONTWRITEBYTECODE=1 \
73+
DO_NOT_TRACK=1
7374

7475
EXPOSE 8000
7576
ENTRYPOINT ["./scripts/entrypoint.sh"]

Makefile

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
PLATFORMS ?= linux/$(TARGETARCH)
2+
TARGETARCH ?= amd64
3+
4+
DOCKER_BUILDX_CMD ?= docker buildx
5+
IMAGE_BUILD_CMD ?= $(DOCKER_BUILDX_CMD) build
6+
IMAGE_BUILD_EXTRA_OPTS ?=
7+
8+
IMAGE_REGISTRY ?= quay.io/trustyai
9+
IMAGE_NAME := nemo-guardrails-server
10+
IMAGE_REPO ?= $(IMAGE_REGISTRY)/$(IMAGE_NAME)
11+
IMAGE_TAG ?= $(IMAGE_REPO):$(GIT_TAG)
12+
13+
GIT_COMMIT_SHA ?= $(shell git rev-parse HEAD)
14+
GIT_TAG ?= $(or $(shell git describe --abbrev=0 2>/dev/null),$(shell git rev-parse --short HEAD))
15+
BUILD_REF ?= $(or $(shell git describe --abbrev=0 2>/dev/null),$(GIT_TAG)) # release tag or fallback to git commit hash
16+
17+
# The name of the kind cluster to use for the "kind-load" target.
18+
KIND_CLUSTER ?= kind
19+
120
.PHONY: all test tests test_watch test_coverage test_profile docs docs-strict docs-serve docs-update-cards docs-check-cards docs-watch-cards pre_commit help
221

322
# Default target executed when no specific target is provided to make.
@@ -46,6 +65,35 @@ pre_commit:
4665
pre-commit install
4766
pre-commit run --all-files
4867

68+
# BUILD
69+
.PHONY: image-build
70+
image-build: # Build the image using Docker buildx.
71+
$(IMAGE_BUILD_CMD) -t $(IMAGE_TAG) \
72+
--file Dockerfile.server \
73+
--platform=$(PLATFORMS) \
74+
--build-arg COMMIT_SHA=${GIT_COMMIT_SHA} \
75+
--build-arg BUILD_REF=${BUILD_REF} \
76+
$(PUSH) \
77+
$(LOAD) \
78+
$(IMAGE_BUILD_EXTRA_OPTS) ./
79+
80+
# Build the container image for the server
81+
.PHONY: image-local-build
82+
image-local-build: # Build the image using Docker buildx
83+
set -e; \
84+
builder=$$($(DOCKER_BUILDX_CMD) create --use); \
85+
trap '$(DOCKER_BUILDX_CMD) rm -f "$$builder"' EXIT; \
86+
$(MAKE) image-build PUSH="$(PUSH)" LOAD="$(LOAD)"
87+
88+
.PHONY: image-local-push
89+
image-local-push: # Push the image to the local Docker registry
90+
image-local-push: PUSH=--push # Build the image for local development and push it to $IMAGE_REPO.
91+
image-local-push: image-local-build
92+
93+
.PHONY: image-kind
94+
image-kind: LOAD=--load
95+
image-kind: image-build # Build the image and load it to kind cluster $KIND_CLUSTER ("kind" by default)
96+
kind load docker-image $(IMAGE_TAG) --name $(KIND_CLUSTER)
4997

5098
# HELP
5199

@@ -64,3 +112,7 @@ help:
64112
@echo 'docs-watch-cards - watch for file changes and auto-update cards'
65113
@echo 'docs-check-redirects - validate that all redirect targets exist'
66114
@echo 'pre_commit - run pre-commit hooks'
115+
@echo 'image-build - build the image using Docker buildx'
116+
@echo 'image-local-build - build the image using Docker buildx for local development'
117+
@echo 'image-local-push - build the image and push it to the local Docker registry'
118+
@echo 'image-kind - build the image and load it to kind cluster $KIND_CLUSTER ("kind" by default)'

nemoguardrails/actions/llm/utils.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
from nemoguardrails.colang.v2_x.lang.colang_ast import Flow
2626
from nemoguardrails.colang.v2_x.runtime.flows import InternalEvent, InternalEvents
2727
from nemoguardrails.context import (
28+
api_request_headers_var,
29+
get_llm_needs_runtime_auth,
2830
llm_call_info_var,
2931
llm_response_metadata_var,
3032
reasoning_trace_var,
@@ -54,6 +56,45 @@
5456
]
5557

5658

59+
_INFRA_PREFIXES = ("x-forwarded", "x-real-", "x-request-id", "x-remote-")
60+
61+
62+
def get_extra_headers_from_request(forward_auth: bool = True) -> Optional[Dict[str, str]]:
63+
"""Forward X-* headers from the incoming request to the LLM call.
64+
65+
Excludes proxy/infra headers and the inbound Authorization header (which
66+
typically carries K8s/proxy auth and must never be forwarded to the LLM).
67+
When forward_auth is True, forwards X-Authorization as Authorization to
68+
the LLM provider.
69+
"""
70+
request_headers = api_request_headers_var.get()
71+
if not request_headers:
72+
return None
73+
74+
extra_headers = {}
75+
76+
for k, v in request_headers.items():
77+
lower = k.lower()
78+
if lower in ("authorization", "x-authorization"):
79+
continue
80+
if lower.startswith("x-") and not lower.startswith(_INFRA_PREFIXES):
81+
extra_headers[k] = v
82+
83+
if forward_auth:
84+
auth = request_headers.get("x-authorization")
85+
if auth:
86+
extra_headers["Authorization"] = auth
87+
88+
return extra_headers or None
89+
90+
91+
def get_extra_headers_for_llm(llm: "BaseLanguageModel") -> Dict[str, str]:
92+
"""Return extra_headers dict for an LLM based on its runtime auth needs."""
93+
needs_runtime_auth = get_llm_needs_runtime_auth(llm)
94+
extra_headers = get_extra_headers_from_request(forward_auth=needs_runtime_auth)
95+
return extra_headers or {}
96+
97+
5798
def _infer_provider_from_module(llm: BaseLanguageModel) -> Optional[str]:
5899
"""Infer provider name from the LLM's module path.
59100
@@ -224,6 +265,15 @@ async def llm_call(
224265
llm_params_with_stop = llm_params
225266

226267
filtered_params = _filter_params_for_openai_reasoning_models(llm, llm_params_with_stop)
268+
269+
# Forward X-* headers from the incoming request. Only forward Authorization
270+
# to models that need runtime auth (i.e. had no valid static API key at init).
271+
needs_runtime_auth = get_llm_needs_runtime_auth(llm)
272+
extra_headers = get_extra_headers_from_request(forward_auth=needs_runtime_auth)
273+
if extra_headers:
274+
filtered_params = filtered_params.copy() if filtered_params else {}
275+
filtered_params["extra_headers"] = extra_headers
276+
227277
generation_llm: Union[BaseLanguageModel, Runnable] = llm.bind(**filtered_params) if filtered_params else llm
228278

229279
if streaming_handler:

nemoguardrails/context.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,24 @@
6262
llm_response_metadata_var: contextvars.ContextVar[Optional[dict]] = contextvars.ContextVar(
6363
"llm_response_metadata", default=None
6464
)
65+
66+
# The request headers from the incoming API request (set by the server layer).
67+
# Used to forward Authorization and X-* headers to outgoing LLM calls.
68+
api_request_headers_var: contextvars.ContextVar[Optional[Dict[str, str]]] = contextvars.ContextVar(
69+
"api_request_headers", default=None
70+
)
71+
72+
# Registry tracking which LLM instances need runtime auth (i.e. had no valid
73+
# static API key at init time). Keyed by id(llm) — safe because LLM objects
74+
# are server-lifetime singletons created once in _init_llms().
75+
_llm_needs_runtime_auth: Dict[int, bool] = {}
76+
77+
78+
def set_llm_needs_runtime_auth(llm: Any, needs_auth: bool) -> None:
79+
"""Register whether an LLM instance needs runtime auth from request headers."""
80+
_llm_needs_runtime_auth[id(llm)] = needs_auth
81+
82+
83+
def get_llm_needs_runtime_auth(llm: Any) -> bool:
84+
"""Check whether an LLM instance needs runtime auth from request headers."""
85+
return _llm_needs_runtime_auth.get(id(llm), False)

nemoguardrails/library/hallucination/actions.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from nemoguardrails import RailsConfig
2323
from nemoguardrails.actions import action
2424
from nemoguardrails.actions.llm.utils import (
25+
get_extra_headers_for_llm,
2526
get_multiline_response,
2627
llm_call,
2728
strip_quotes,
@@ -79,7 +80,11 @@ async def self_check_hallucination(
7980

8081
# Generate multiple responses with temperature 1.
8182
# Bind the config parameters to the LLM for this call
82-
llm_with_config = llm.bind(temperature=1.0, n=num_responses)
83+
bind_kwargs = {"temperature": 1.0, "n": num_responses}
84+
extra_headers = get_extra_headers_for_llm(llm)
85+
if extra_headers:
86+
bind_kwargs["extra_headers"] = extra_headers
87+
llm_with_config = llm.bind(**bind_kwargs)
8388
extra_llm_response = await llm_with_config.agenerate(
8489
[formatted_prompt],
8590
callbacks=logging_callback_manager_for_chain.handlers,

nemoguardrails/logging/callbacks.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
15+
import copy
1516
import logging
17+
import re
1618
import uuid
1719
from time import time
1820
from typing import Any, Dict, List, Optional, cast
@@ -36,6 +38,20 @@
3638

3739
log = logging.getLogger(__name__)
3840

41+
_BEARER_RE = re.compile(r"(Bearer\s+)\S+", re.IGNORECASE)
42+
43+
44+
def _redact_invocation_params(params: Dict[str, Any]) -> Dict[str, Any]:
45+
"""Return a copy of invocation params with auth tokens redacted."""
46+
if "extra_headers" not in params:
47+
return params
48+
params = copy.copy(params)
49+
headers = params["extra_headers"]
50+
params["extra_headers"] = {
51+
k: _BEARER_RE.sub(r"\1[REDACTED]", v) if "authorization" in k.lower() else v for k, v in headers.items()
52+
}
53+
return params
54+
3955

4056
class LoggingCallbackHandler(AsyncCallbackHandler):
4157
"""Async callback handler that can be used to handle callbacks from langchain."""
@@ -64,7 +80,7 @@ async def on_llm_start(
6480
if explain_info:
6581
explain_info.llm_calls.append(llm_call_info)
6682

67-
log.info("Invocation Params :: %s", kwargs.get("invocation_params", {}))
83+
log.info("Invocation Params :: %s", _redact_invocation_params(kwargs.get("invocation_params", {})))
6884
log.info(
6985
"Prompt :: %s",
7086
prompts[0],
@@ -123,7 +139,7 @@ async def on_chat_model_start(
123139
]
124140
)
125141

126-
log.info("Invocation Params :: %s", kwargs.get("invocation_params", {}))
142+
log.info("Invocation Params :: %s", _redact_invocation_params(kwargs.get("invocation_params", {})))
127143
log.info(
128144
"Prompt Messages :: %s",
129145
prompt,

nemoguardrails/rails/llm/llmrails.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
generation_options_var,
6767
llm_stats_var,
6868
raw_llm_request,
69+
set_llm_needs_runtime_auth,
6970
streaming_handler_var,
7071
)
7172
from nemoguardrails.embeddings.index import EmbeddingsIndex
@@ -369,19 +370,31 @@ def _prepare_model_kwargs(self, model_config):
369370
model_config: The model configuration object
370371
371372
Returns:
372-
dict: The prepared kwargs for model initialization
373+
dict: kwargs dict for model initialization
373374
"""
374375
# Make a copy to avoid modifying the original model config
375376
kwargs = dict(model_config.parameters) if model_config.parameters else {}
376377

377-
# If the optional API Key Environment Variable is set, add it to kwargs
378378
if model_config.api_key_env_var:
379379
api_key = os.environ.get(model_config.api_key_env_var)
380380
if api_key:
381381
kwargs["api_key"] = api_key
382+
kwargs["openai_api_key"] = api_key
383+
elif "api_key" not in kwargs:
384+
# Placeholder to satisfy LangChain constructors. Real auth arrives
385+
# via forwarded request headers at call time. If no auth header is
386+
# provided, the LLM call will fail with a 401 — this is the intended
387+
# fail-safe: requests without valid auth are rejected by the provider.
388+
kwargs["api_key"] = "runtime-provided"
389+
kwargs["openai_api_key"] = "runtime-provided"
382390

383391
return kwargs
384392

393+
@staticmethod
394+
def _needs_runtime_auth(kwargs):
395+
"""Check if the model needs auth provided at request time."""
396+
return kwargs.get("api_key") == "runtime-provided"
397+
385398
def _init_llms(self):
386399
"""
387400
Initializes the right LLM engines based on the configuration.
@@ -421,6 +434,7 @@ def _init_llms(self):
421434
mode="chat",
422435
kwargs=kwargs,
423436
)
437+
set_llm_needs_runtime_auth(self.llm, self._needs_runtime_auth(kwargs))
424438
self.runtime.register_action_param("llm", self.llm)
425439

426440
else:
@@ -453,6 +467,7 @@ def _init_llms(self):
453467
mode=mode,
454468
kwargs=kwargs,
455469
)
470+
set_llm_needs_runtime_auth(llm_model, self._needs_runtime_auth(kwargs))
456471

457472
# Configure the model based on its type
458473
if llm_config.type == "main":

0 commit comments

Comments
 (0)