refactor: replace set_gemini_safety_settings() with LLMProvider.with_config() (#474)
* refactor: replace set_gemini_safety_settings() with LLMProvider.with_config() Removes the fragile ContextVar-setter pattern where callers had to remember to call set_gemini_safety_settings() at every operation entry point. Instead, LLMProvider.with_config(resolved_config) returns a ConfiguredLLMProvider wrapper that: - injects per-bank settings (Gemini safety settings) on every call via token-based ContextVar set/reset — properly scoped, no leakage - proxies all attribute access to the underlying provider via __getattr__ - requires zero changes to LLMInterface or any provider implementations Call sites (retain, reflect, consolidation) now pass llm_config.with_config(resolved_config) to sub-components instead of setting a global context var and hoping nothing else runs in between. This pattern also composes naturally with a future per-bank provider factory: callers always receive something with a .call() method. * fix: pass messages/tools as kwargs in ConfiguredLLMProvider to preserve class-level patch compatibility
This commit is contained in:
parent
15f4b8769b
commit
861295dd7c
5 changed files with 151 additions and 86 deletions
|
|
@ -127,10 +127,9 @@ async def run_consolidation_job(
|
||||||
# Resolve bank-specific config with hierarchical overrides
|
# Resolve bank-specific config with hierarchical overrides
|
||||||
config = await memory_engine._config_resolver.resolve_full_config(bank_id, request_context)
|
config = await memory_engine._config_resolver.resolve_full_config(bank_id, request_context)
|
||||||
|
|
||||||
# Apply bank-specific Gemini safety settings for this request context
|
# Build a configured LLM wrapper that applies per-bank settings (e.g. safety settings)
|
||||||
from ..providers.gemini_llm import set_gemini_safety_settings
|
# to every call without leaking across operations.
|
||||||
|
llm_config = memory_engine._consolidation_llm_config.with_config(config)
|
||||||
set_gemini_safety_settings(config.llm_gemini_safety_settings)
|
|
||||||
|
|
||||||
perf = ConsolidationPerfLog(bank_id)
|
perf = ConsolidationPerfLog(bank_id)
|
||||||
max_memories_per_batch = config.consolidation_batch_size
|
max_memories_per_batch = config.consolidation_batch_size
|
||||||
|
|
@ -281,6 +280,7 @@ async def run_consolidation_job(
|
||||||
pass_results = await _process_memory_batch(
|
pass_results = await _process_memory_batch(
|
||||||
conn=conn,
|
conn=conn,
|
||||||
memory_engine=memory_engine,
|
memory_engine=memory_engine,
|
||||||
|
llm_config=llm_config,
|
||||||
bank_id=bank_id,
|
bank_id=bank_id,
|
||||||
memories=llm_batch,
|
memories=llm_batch,
|
||||||
request_context=request_context,
|
request_context=request_context,
|
||||||
|
|
@ -318,6 +318,7 @@ async def run_consolidation_job(
|
||||||
results = await _process_memory_batch(
|
results = await _process_memory_batch(
|
||||||
conn=conn,
|
conn=conn,
|
||||||
memory_engine=memory_engine,
|
memory_engine=memory_engine,
|
||||||
|
llm_config=llm_config,
|
||||||
bank_id=bank_id,
|
bank_id=bank_id,
|
||||||
memories=llm_batch,
|
memories=llm_batch,
|
||||||
request_context=request_context,
|
request_context=request_context,
|
||||||
|
|
@ -513,6 +514,7 @@ async def _trigger_mental_model_refreshes(
|
||||||
async def _process_memory_batch(
|
async def _process_memory_batch(
|
||||||
conn: "Connection",
|
conn: "Connection",
|
||||||
memory_engine: "MemoryEngine",
|
memory_engine: "MemoryEngine",
|
||||||
|
llm_config: Any,
|
||||||
bank_id: str,
|
bank_id: str,
|
||||||
memories: list[dict[str, Any]],
|
memories: list[dict[str, Any]],
|
||||||
request_context: "RequestContext",
|
request_context: "RequestContext",
|
||||||
|
|
@ -581,7 +583,7 @@ async def _process_memory_batch(
|
||||||
# 3. Single LLM call
|
# 3. Single LLM call
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
llm_result = await _consolidate_batch_with_llm(
|
llm_result = await _consolidate_batch_with_llm(
|
||||||
memory_engine=memory_engine,
|
llm_config=llm_config,
|
||||||
memories=memories,
|
memories=memories,
|
||||||
union_observations=union_observations,
|
union_observations=union_observations,
|
||||||
union_source_facts=union_source_facts,
|
union_source_facts=union_source_facts,
|
||||||
|
|
@ -945,7 +947,7 @@ def _build_observations_for_llm(
|
||||||
|
|
||||||
|
|
||||||
async def _consolidate_batch_with_llm(
|
async def _consolidate_batch_with_llm(
|
||||||
memory_engine: "MemoryEngine",
|
llm_config: Any,
|
||||||
memories: list[dict[str, Any]],
|
memories: list[dict[str, Any]],
|
||||||
union_observations: "list[MemoryFact]",
|
union_observations: "list[MemoryFact]",
|
||||||
union_source_facts: "dict[str, MemoryFact]",
|
union_source_facts: "dict[str, MemoryFact]",
|
||||||
|
|
@ -981,7 +983,7 @@ async def _consolidate_batch_with_llm(
|
||||||
last_exc: Exception | None = None
|
last_exc: Exception | None = None
|
||||||
for attempt in range(1, max_attempts + 1):
|
for attempt in range(1, max_attempts + 1):
|
||||||
try:
|
try:
|
||||||
response: _ConsolidationBatchResponse = await memory_engine._consolidation_llm_config.call(
|
response: _ConsolidationBatchResponse = await llm_config.call(
|
||||||
messages=[{"role": "user", "content": prompt}],
|
messages=[{"role": "user", "content": prompt}],
|
||||||
response_format=_ConsolidationBatchResponse,
|
response_format=_ConsolidationBatchResponse,
|
||||||
scope="consolidation",
|
scope="consolidation",
|
||||||
|
|
|
||||||
|
|
@ -622,6 +622,23 @@ class LLMProvider:
|
||||||
# SDK will automatically check for authentication when first used
|
# SDK will automatically check for authentication when first used
|
||||||
# No need to verify here - let it fail gracefully on first call with helpful error
|
# No need to verify here - let it fail gracefully on first call with helpful error
|
||||||
|
|
||||||
|
def with_config(self, config: Any) -> "ConfiguredLLMProvider":
|
||||||
|
"""
|
||||||
|
Return a configured wrapper for a specific bank operation.
|
||||||
|
|
||||||
|
The wrapper applies per-bank overrides (e.g. Gemini safety settings)
|
||||||
|
to every ``call()`` / ``call_with_tools()`` invocation without
|
||||||
|
changing the underlying provider or its long-lived client connection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Resolved ``HindsightConfig`` for the current bank/request.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A ``ConfiguredLLMProvider`` that delegates to this provider with
|
||||||
|
the supplied config applied.
|
||||||
|
"""
|
||||||
|
return ConfiguredLLMProvider(self, config.llm_gemini_safety_settings)
|
||||||
|
|
||||||
async def cleanup(self) -> None:
|
async def cleanup(self) -> None:
|
||||||
"""Clean up resources."""
|
"""Clean up resources."""
|
||||||
pass
|
pass
|
||||||
|
|
@ -683,5 +700,58 @@ class LLMProvider:
|
||||||
return cls(provider=provider, api_key=api_key, base_url=base_url, model=model, reasoning_effort="high")
|
return cls(provider=provider, api_key=api_key, base_url=base_url, model=model, reasoning_effort="high")
|
||||||
|
|
||||||
|
|
||||||
|
class ConfiguredLLMProvider:
|
||||||
|
"""
|
||||||
|
Thin wrapper around LLMProvider that applies bank-specific config to every call.
|
||||||
|
|
||||||
|
Obtained via ``LLMProvider.with_config(resolved_config)``. The wrapper
|
||||||
|
sets any provider-specific overrides (currently Gemini safety settings)
|
||||||
|
immediately before each call using a ContextVar token, then resets it
|
||||||
|
afterwards — so nesting is safe and the configuration cannot leak across
|
||||||
|
operations.
|
||||||
|
|
||||||
|
All attribute access falls through to the underlying provider so callers
|
||||||
|
that read ``llm.provider``, ``llm.model``, etc. continue to work without
|
||||||
|
any changes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, provider: "LLMProvider", gemini_safety_settings: list | None) -> None:
|
||||||
|
# Use object.__setattr__ to avoid triggering __getattr__
|
||||||
|
object.__setattr__(self, "_provider", provider)
|
||||||
|
object.__setattr__(self, "_gemini_safety_settings", gemini_safety_settings)
|
||||||
|
|
||||||
|
# ── attribute passthrough ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def __getattr__(self, name: str) -> Any:
|
||||||
|
return getattr(object.__getattribute__(self, "_provider"), name)
|
||||||
|
|
||||||
|
# ── overridden call methods ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def call(self, messages: list[dict[str, Any]], **kwargs: Any) -> Any:
|
||||||
|
from .providers.gemini_llm import _safety_settings_ctx
|
||||||
|
|
||||||
|
token = _safety_settings_ctx.set(object.__getattribute__(self, "_gemini_safety_settings"))
|
||||||
|
try:
|
||||||
|
return await object.__getattribute__(self, "_provider").call(messages=messages, **kwargs)
|
||||||
|
finally:
|
||||||
|
_safety_settings_ctx.reset(token)
|
||||||
|
|
||||||
|
async def call_with_tools(
|
||||||
|
self,
|
||||||
|
messages: list[dict[str, Any]],
|
||||||
|
tools: list[dict[str, Any]],
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> "LLMToolCallResult":
|
||||||
|
from .providers.gemini_llm import _safety_settings_ctx
|
||||||
|
|
||||||
|
token = _safety_settings_ctx.set(object.__getattribute__(self, "_gemini_safety_settings"))
|
||||||
|
try:
|
||||||
|
return await object.__getattribute__(self, "_provider").call_with_tools(
|
||||||
|
messages=messages, tools=tools, **kwargs
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
_safety_settings_ctx.reset(token)
|
||||||
|
|
||||||
|
|
||||||
# Backwards compatibility alias
|
# Backwards compatibility alias
|
||||||
LLMConfig = LLMProvider
|
LLMConfig = LLMProvider
|
||||||
|
|
|
||||||
|
|
@ -1831,17 +1831,12 @@ class MemoryEngine(MemoryEngineInterface):
|
||||||
# Resolve bank-specific config for this operation
|
# Resolve bank-specific config for this operation
|
||||||
resolved_config = await self._config_resolver.resolve_full_config(bank_id, request_context)
|
resolved_config = await self._config_resolver.resolve_full_config(bank_id, request_context)
|
||||||
|
|
||||||
# Apply bank-specific Gemini safety settings for this request context
|
|
||||||
from .providers.gemini_llm import set_gemini_safety_settings
|
|
||||||
|
|
||||||
set_gemini_safety_settings(resolved_config.llm_gemini_safety_settings)
|
|
||||||
|
|
||||||
# Create parent span for retain operation
|
# Create parent span for retain operation
|
||||||
with create_operation_span("retain", bank_id):
|
with create_operation_span("retain", bank_id):
|
||||||
return await orchestrator.retain_batch(
|
return await orchestrator.retain_batch(
|
||||||
pool=pool,
|
pool=pool,
|
||||||
embeddings_model=self.embeddings,
|
embeddings_model=self.embeddings,
|
||||||
llm_config=self._retain_llm_config,
|
llm_config=self._retain_llm_config.with_config(resolved_config),
|
||||||
entity_resolver=self.entity_resolver,
|
entity_resolver=self.entity_resolver,
|
||||||
format_date_fn=self._format_readable_date,
|
format_date_fn=self._format_readable_date,
|
||||||
bank_id=bank_id,
|
bank_id=bank_id,
|
||||||
|
|
@ -4468,11 +4463,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||||
# The agent can call lookup() to list available models if needed.
|
# The agent can call lookup() to list available models if needed.
|
||||||
# This is critical for banks with many mental models to avoid huge prompts.
|
# This is critical for banks with many mental models to avoid huge prompts.
|
||||||
|
|
||||||
# Apply bank-specific Gemini safety settings for this request context
|
|
||||||
resolved_reflect_config = await self._config_resolver.resolve_full_config(bank_id, request_context)
|
resolved_reflect_config = await self._config_resolver.resolve_full_config(bank_id, request_context)
|
||||||
from .providers.gemini_llm import set_gemini_safety_settings
|
|
||||||
|
|
||||||
set_gemini_safety_settings(resolved_reflect_config.llm_gemini_safety_settings)
|
|
||||||
|
|
||||||
# Compute max iterations based on budget
|
# Compute max iterations based on budget
|
||||||
config = get_config()
|
config = get_config()
|
||||||
|
|
@ -4576,7 +4567,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
agent_result = await run_reflect_agent(
|
agent_result = await run_reflect_agent(
|
||||||
llm_config=self._reflect_llm_config,
|
llm_config=self._reflect_llm_config.with_config(resolved_reflect_config),
|
||||||
bank_id=bank_id,
|
bank_id=bank_id,
|
||||||
query=query,
|
query=query,
|
||||||
bank_profile=profile,
|
bank_profile=profile,
|
||||||
|
|
|
||||||
|
|
@ -25,25 +25,12 @@ from hindsight_api.metrics import get_metrics_collector
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Context variable for per-request Gemini safety settings override (supports per-bank configuration)
|
# Per-request Gemini safety settings override.
|
||||||
|
# Set exclusively by ConfiguredLLMProvider.call() / call_with_tools() via token-based
|
||||||
|
# set/reset, so it is properly scoped to each individual LLM call and never leaks.
|
||||||
_safety_settings_ctx: ContextVar[list | None] = ContextVar("gemini_safety_settings", default=None)
|
_safety_settings_ctx: ContextVar[list | None] = ContextVar("gemini_safety_settings", default=None)
|
||||||
|
|
||||||
|
|
||||||
def set_gemini_safety_settings(settings: list | None) -> None:
|
|
||||||
"""
|
|
||||||
Set Gemini safety settings for the current async context.
|
|
||||||
|
|
||||||
This allows per-bank safety settings to be applied without changing
|
|
||||||
the LLM provider interface. Call this before making LLM calls within
|
|
||||||
an operation that has resolved bank-specific configuration.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
settings: List of safety setting dicts with 'category' and 'threshold' keys,
|
|
||||||
or None to use the instance default (from env var).
|
|
||||||
"""
|
|
||||||
_safety_settings_ctx.set(settings)
|
|
||||||
|
|
||||||
|
|
||||||
# Vertex AI imports (optional)
|
# Vertex AI imports (optional)
|
||||||
try:
|
try:
|
||||||
import google.auth
|
import google.auth
|
||||||
|
|
|
||||||
|
|
@ -216,81 +216,96 @@ async def test_call_with_tools_applies_safety_settings():
|
||||||
assert "HARM_CATEGORY_HARASSMENT" in categories
|
assert "HARM_CATEGORY_HARASSMENT" in categories
|
||||||
|
|
||||||
|
|
||||||
# ─── Context variable override ────────────────────────────────────────────────
|
# ─── with_config() override ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def _make_llm_provider(safety_settings=None):
|
||||||
|
"""Return an LLMProvider (wrapping GeminiLLM) with a mocked genai.Client."""
|
||||||
|
with patch("google.genai.Client") as mock_client_cls:
|
||||||
|
mock_client_cls.return_value = MagicMock()
|
||||||
|
from hindsight_api.engine.llm_wrapper import LLMProvider
|
||||||
|
|
||||||
|
provider = LLMProvider(
|
||||||
|
provider="gemini",
|
||||||
|
api_key="fake-api-key",
|
||||||
|
base_url="",
|
||||||
|
model="gemini-2.5-flash",
|
||||||
|
gemini_safety_settings=safety_settings,
|
||||||
|
)
|
||||||
|
# Replace the underlying Gemini client with a fresh mock
|
||||||
|
provider._provider_impl._client = MagicMock()
|
||||||
|
return provider
|
||||||
|
|
||||||
|
|
||||||
|
def _fake_response():
|
||||||
|
r = MagicMock()
|
||||||
|
r.text = "hello"
|
||||||
|
r.candidates = [MagicMock(finish_reason="STOP")]
|
||||||
|
r.usage_metadata = MagicMock(prompt_token_count=5, candidates_token_count=2)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def _make_config(safety_settings):
|
||||||
|
"""Return a minimal config-like object with llm_gemini_safety_settings."""
|
||||||
|
cfg = MagicMock()
|
||||||
|
cfg.llm_gemini_safety_settings = safety_settings
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_context_var_overrides_instance_settings():
|
async def test_with_config_overrides_instance_settings():
|
||||||
"""The context var safety settings take precedence over instance defaults."""
|
"""with_config() settings take precedence over the provider instance defaults."""
|
||||||
from hindsight_api.engine.providers.gemini_llm import set_gemini_safety_settings
|
|
||||||
|
|
||||||
# Instance has settings, but we'll override via context var with different settings
|
|
||||||
instance_settings = [{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_ONLY_HIGH"}]
|
instance_settings = [{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_ONLY_HIGH"}]
|
||||||
ctx_settings = [{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"}]
|
override_settings = [{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"}]
|
||||||
|
|
||||||
provider = _make_gemini_provider(safety_settings=instance_settings)
|
provider = _make_llm_provider(safety_settings=instance_settings)
|
||||||
|
provider._provider_impl._client.aio.models.generate_content = AsyncMock(return_value=_fake_response())
|
||||||
|
|
||||||
fake_response = MagicMock()
|
configured = provider.with_config(_make_config(override_settings))
|
||||||
fake_response.text = "hello"
|
await configured.call(messages=[{"role": "user", "content": "hi"}], scope="test")
|
||||||
fake_response.candidates = [MagicMock(finish_reason="STOP")]
|
|
||||||
fake_response.usage_metadata = MagicMock(prompt_token_count=5, candidates_token_count=2)
|
|
||||||
|
|
||||||
provider._client.aio.models.generate_content = AsyncMock(return_value=fake_response)
|
|
||||||
|
|
||||||
# Set context var override
|
|
||||||
set_gemini_safety_settings(ctx_settings)
|
|
||||||
try:
|
|
||||||
await provider.call(
|
|
||||||
messages=[{"role": "user", "content": "hi"}],
|
|
||||||
scope="test",
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
set_gemini_safety_settings(None) # Reset context
|
|
||||||
|
|
||||||
call_args = provider._client.aio.models.generate_content.call_args
|
|
||||||
config_arg = call_args.kwargs.get("config")
|
|
||||||
|
|
||||||
|
config_arg = provider._provider_impl._client.aio.models.generate_content.call_args.kwargs.get("config")
|
||||||
assert config_arg is not None
|
assert config_arg is not None
|
||||||
assert config_arg.safety_settings is not None
|
|
||||||
|
|
||||||
# Should use ctx_settings (HATE_SPEECH/BLOCK_NONE), not instance_settings (HARASSMENT/BLOCK_ONLY_HIGH)
|
|
||||||
categories = [s.category.value if hasattr(s.category, "value") else str(s.category) for s in config_arg.safety_settings]
|
categories = [s.category.value if hasattr(s.category, "value") else str(s.category) for s in config_arg.safety_settings]
|
||||||
|
# Should use override_settings (HATE_SPEECH), not instance_settings (HARASSMENT)
|
||||||
assert "HARM_CATEGORY_HATE_SPEECH" in categories
|
assert "HARM_CATEGORY_HATE_SPEECH" in categories
|
||||||
assert "HARM_CATEGORY_HARASSMENT" not in categories
|
assert "HARM_CATEGORY_HARASSMENT" not in categories
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_context_var_none_falls_back_to_instance():
|
async def test_with_config_none_falls_back_to_instance():
|
||||||
"""When context var is None (not set), instance settings are used."""
|
"""When with_config() supplies None, the instance default is used."""
|
||||||
from hindsight_api.engine.providers.gemini_llm import set_gemini_safety_settings
|
|
||||||
|
|
||||||
instance_settings = [{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"}]
|
instance_settings = [{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"}]
|
||||||
provider = _make_gemini_provider(safety_settings=instance_settings)
|
|
||||||
|
|
||||||
fake_response = MagicMock()
|
provider = _make_llm_provider(safety_settings=instance_settings)
|
||||||
fake_response.text = "hello"
|
provider._provider_impl._client.aio.models.generate_content = AsyncMock(return_value=_fake_response())
|
||||||
fake_response.candidates = [MagicMock(finish_reason="STOP")]
|
|
||||||
fake_response.usage_metadata = MagicMock(prompt_token_count=5, candidates_token_count=2)
|
|
||||||
|
|
||||||
provider._client.aio.models.generate_content = AsyncMock(return_value=fake_response)
|
configured = provider.with_config(_make_config(None))
|
||||||
|
await configured.call(messages=[{"role": "user", "content": "hi"}], scope="test")
|
||||||
# Explicitly set context var to None (fallback)
|
|
||||||
set_gemini_safety_settings(None)
|
|
||||||
|
|
||||||
await provider.call(
|
|
||||||
messages=[{"role": "user", "content": "hi"}],
|
|
||||||
scope="test",
|
|
||||||
)
|
|
||||||
|
|
||||||
call_args = provider._client.aio.models.generate_content.call_args
|
|
||||||
config_arg = call_args.kwargs.get("config")
|
|
||||||
|
|
||||||
|
config_arg = provider._provider_impl._client.aio.models.generate_content.call_args.kwargs.get("config")
|
||||||
assert config_arg is not None
|
assert config_arg is not None
|
||||||
assert config_arg.safety_settings is not None
|
|
||||||
categories = [s.category.value if hasattr(s.category, "value") else str(s.category) for s in config_arg.safety_settings]
|
categories = [s.category.value if hasattr(s.category, "value") else str(s.category) for s in config_arg.safety_settings]
|
||||||
assert "HARM_CATEGORY_HARASSMENT" in categories
|
assert "HARM_CATEGORY_HARASSMENT" in categories
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_with_config_resets_after_call():
|
||||||
|
"""The ContextVar is properly reset after a with_config() call (no leakage)."""
|
||||||
|
from hindsight_api.engine.providers.gemini_llm import _safety_settings_ctx
|
||||||
|
|
||||||
|
settings = [{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"}]
|
||||||
|
provider = _make_llm_provider(safety_settings=None)
|
||||||
|
provider._provider_impl._client.aio.models.generate_content = AsyncMock(return_value=_fake_response())
|
||||||
|
|
||||||
|
before = _safety_settings_ctx.get()
|
||||||
|
configured = provider.with_config(_make_config(settings))
|
||||||
|
await configured.call(messages=[{"role": "user", "content": "hi"}], scope="test")
|
||||||
|
after = _safety_settings_ctx.get()
|
||||||
|
|
||||||
|
assert after == before # ContextVar restored to its original value
|
||||||
|
|
||||||
|
|
||||||
# ─── LLMProvider reads safety settings from config ────────────────────────────
|
# ─── LLMProvider reads safety settings from config ────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue