fix(startup): suppress proxy startup log noise (#619)
* docs: add enterprise.md
* docs: add link to enterprisemd in README
* fix(copilot): restore generic endpoint for non-subscription OAuth (#610) (#612)
* fix(copilot): restore generic endpoint for non-subscription OAuth (#610)
0.23.0 re-pointed the shared Copilot OAuth branch from the generic
api.githubcopilot.com host to the account-specific endpoints.api host
returned by /copilot_internal/user, and made resolve_copilot_api_url
ignore the GITHUB_COPILOT_API_URL override whenever a token resolved.
That change was meant to add --subscription, but it also altered the
pre-existing non-subscription OAuth flow that worked on 0.22.4. The
account host does not serve newer models (e.g. gpt-5.4) on the responses
API, so wrapped requests began failing with unsupported-model errors
while plain Copilot and 0.22.4 kept working.
Restore 0.22.4 routing for non-subscription OAuth (generic host, still
overridable) and keep account resolution only for --subscription.
resolve_copilot_api_url now honors GITHUB_COPILOT_API_URL first, so the
override escape hatch works for every path. BYOK is unaffected.
Add a regression suite that mocks a successful user-info response, the
real-world path the prior test never exercised (it relied on the network
call failing in CI and falling back to the generic host).
* fix(copilot): route subscription + OAuth through the generic host (#610)
The 0.23.0 endpoint resolution derived the Copilot API host from
/copilot_internal/user (endpoints.api), which returns a segmented host
(e.g. api.individual.githubcopilot.com) that does not serve newer models
on the responses API and is not the host the official Copilot client
routes with (that comes from the token-exchange endpoint). --subscription
used the identical resolution, so it carried the same latent regression
as the non-subscription OAuth path.
Make Copilot host resolution override -> generic for BOTH --subscription
and the implicit OAuth path, and stop using user-info to route. Accounts
that require a dedicated host (enterprise / data residency) pin it via
GITHUB_COPILOT_API_URL. resolve_copilot_api_url no longer makes a network
call; _fetch_copilot_user_info is retained for token validation.
Update the subscription smoke tests that encoded the old account-host
assumption, and add wrap-level + unit coverage that --subscription routes
to the generic host even when user-info advertises an account host, and
that the GITHUB_COPILOT_API_URL override flows through both paths.
* docs(copilot): document generic-host routing + enterprise override (#610)
Spell out the routing contract introduced by the #610 fix so enterprise
users have a supported path. Headroom routes wrapped Copilot hosted
traffic (--subscription and OAuth) to the generic api.githubcopilot.com,
and accounts on a dedicated host (Enterprise Cloud data residency, egress
proxy) pin it via GITHUB_COPILOT_API_URL.
- copilot --help: note the generic host + GITHUB_COPILOT_API_URL override.
- TESTING-copilot-subscription.md: add "API host & Enterprise / data
residency" section; correct the stale api.*.githubcopilot.com claim; and
invite enterprise tenants who want token-exchange-based auto-detection to
open an issue.
- integration-guide.md: short hosted-host + override note in the Copilot
section.
* fix(wrap): report unbindable proxy ports (#602)
* fix(proxy): fail-open on corrupt golden bytes instead of RuntimeError (#603)
* fix(proxy): fail-open on corrupt golden bytes instead of RuntimeError
Permanent session corruption: once golden bytes become unreadable
(UnicodeDecodeError / JSONDecodeError), every subsequent request for
the session raised RuntimeError, returning 500 until proxy restart.
Fix: log at ERROR level and recover — skip the corrupt memory tool,
or regenerate a fresh CCR definition — rather than propagating
RuntimeError and permanently breaking the session.
Also change proxy_inbound_request_aborted from logger.info to
logger.error with exc_info=True so tracebacks appear in logs.
Closes: proxy silent-500 sessions in the wild (observed 2026-06-04)
* fix(tests): re-enable headroom log propagation in corrupt-bytes tests
configure_proxy_logging() sets headroom_logger.propagate = False to prevent
duplicate writes when the proxy redirects stderr to a log file. In CI the
proxy initialises its logging stack before the test suite, leaving propagation
disabled. pytest's caplog handler attaches to the root logger, so records that
stop at the headroom logger are never captured.
Added _enable_headroom_log_propagation autouse fixture that temporarily
re-enables propagation for the duration of each test, making caplog capture
work regardless of the surrounding logging configuration.
* fix(tests): remove unused imports from corrupt-bytes regression tests
Remove json, SessionCcrTracker, and SessionToolTracker imports that were
imported but never referenced in the test body. Fixes ruff F401 and I001
lint errors reported by CI.
---------
Co-authored-by: Patrick Ancillotti <patrick.ancillotti@people.inc>
* fix(startup): suppress log noise from litellm, trafilatura, HF hub, and tiktoken warning
* fix(startup): suppress httpx INFO logs from sentence_transformers HEAD checks
* docs(changelog): add entry for startup log noise suppression fixes
* refactor(startup): extract hf_hub_download_local_first into onnx_runtime
The three _hub_download/_hub_dl helpers in embedders.py, onnx_router.py, and
kompress_compressor.py are identical -- try local cache first, fall back to
network download. Extract into a single hf_hub_download_local_first() function
in onnx_runtime.py (the natural home for shared ORT/HF utilities) and update
all three callers to use it.
* fix(lint): sort imports and remove unused _FALLBACK_WARNING_SHOWN import
* fix(lint): cast hf_hub_download return to str for mypy no-any-return
---------
Co-authored-by: Patrick Ancillotti <patrick.ancillotti@people.inc>
2026-06-05 18:44:40 -04:00
|
|
|
"""Tests for startup log noise suppression.
|
|
|
|
|
|
|
|
|
|
Covers the fixes in:
|
|
|
|
|
- headroom/memory/adapters/embedders.py (HF env vars, httpx logger)
|
|
|
|
|
- headroom/providers/anthropic.py (warn=False suppresses tiktoken warning)
|
|
|
|
|
- headroom/providers/litellm.py (suppress_debug_info, set_verbose)
|
|
|
|
|
- headroom/transforms/html_extractor.py (trafilatura logger CRITICAL)
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-06-11 22:10:20 +02:00
|
|
|
import builtins
|
|
|
|
|
import importlib
|
fix(startup): suppress proxy startup log noise (#619)
* docs: add enterprise.md
* docs: add link to enterprisemd in README
* fix(copilot): restore generic endpoint for non-subscription OAuth (#610) (#612)
* fix(copilot): restore generic endpoint for non-subscription OAuth (#610)
0.23.0 re-pointed the shared Copilot OAuth branch from the generic
api.githubcopilot.com host to the account-specific endpoints.api host
returned by /copilot_internal/user, and made resolve_copilot_api_url
ignore the GITHUB_COPILOT_API_URL override whenever a token resolved.
That change was meant to add --subscription, but it also altered the
pre-existing non-subscription OAuth flow that worked on 0.22.4. The
account host does not serve newer models (e.g. gpt-5.4) on the responses
API, so wrapped requests began failing with unsupported-model errors
while plain Copilot and 0.22.4 kept working.
Restore 0.22.4 routing for non-subscription OAuth (generic host, still
overridable) and keep account resolution only for --subscription.
resolve_copilot_api_url now honors GITHUB_COPILOT_API_URL first, so the
override escape hatch works for every path. BYOK is unaffected.
Add a regression suite that mocks a successful user-info response, the
real-world path the prior test never exercised (it relied on the network
call failing in CI and falling back to the generic host).
* fix(copilot): route subscription + OAuth through the generic host (#610)
The 0.23.0 endpoint resolution derived the Copilot API host from
/copilot_internal/user (endpoints.api), which returns a segmented host
(e.g. api.individual.githubcopilot.com) that does not serve newer models
on the responses API and is not the host the official Copilot client
routes with (that comes from the token-exchange endpoint). --subscription
used the identical resolution, so it carried the same latent regression
as the non-subscription OAuth path.
Make Copilot host resolution override -> generic for BOTH --subscription
and the implicit OAuth path, and stop using user-info to route. Accounts
that require a dedicated host (enterprise / data residency) pin it via
GITHUB_COPILOT_API_URL. resolve_copilot_api_url no longer makes a network
call; _fetch_copilot_user_info is retained for token validation.
Update the subscription smoke tests that encoded the old account-host
assumption, and add wrap-level + unit coverage that --subscription routes
to the generic host even when user-info advertises an account host, and
that the GITHUB_COPILOT_API_URL override flows through both paths.
* docs(copilot): document generic-host routing + enterprise override (#610)
Spell out the routing contract introduced by the #610 fix so enterprise
users have a supported path. Headroom routes wrapped Copilot hosted
traffic (--subscription and OAuth) to the generic api.githubcopilot.com,
and accounts on a dedicated host (Enterprise Cloud data residency, egress
proxy) pin it via GITHUB_COPILOT_API_URL.
- copilot --help: note the generic host + GITHUB_COPILOT_API_URL override.
- TESTING-copilot-subscription.md: add "API host & Enterprise / data
residency" section; correct the stale api.*.githubcopilot.com claim; and
invite enterprise tenants who want token-exchange-based auto-detection to
open an issue.
- integration-guide.md: short hosted-host + override note in the Copilot
section.
* fix(wrap): report unbindable proxy ports (#602)
* fix(proxy): fail-open on corrupt golden bytes instead of RuntimeError (#603)
* fix(proxy): fail-open on corrupt golden bytes instead of RuntimeError
Permanent session corruption: once golden bytes become unreadable
(UnicodeDecodeError / JSONDecodeError), every subsequent request for
the session raised RuntimeError, returning 500 until proxy restart.
Fix: log at ERROR level and recover — skip the corrupt memory tool,
or regenerate a fresh CCR definition — rather than propagating
RuntimeError and permanently breaking the session.
Also change proxy_inbound_request_aborted from logger.info to
logger.error with exc_info=True so tracebacks appear in logs.
Closes: proxy silent-500 sessions in the wild (observed 2026-06-04)
* fix(tests): re-enable headroom log propagation in corrupt-bytes tests
configure_proxy_logging() sets headroom_logger.propagate = False to prevent
duplicate writes when the proxy redirects stderr to a log file. In CI the
proxy initialises its logging stack before the test suite, leaving propagation
disabled. pytest's caplog handler attaches to the root logger, so records that
stop at the headroom logger are never captured.
Added _enable_headroom_log_propagation autouse fixture that temporarily
re-enables propagation for the duration of each test, making caplog capture
work regardless of the surrounding logging configuration.
* fix(tests): remove unused imports from corrupt-bytes regression tests
Remove json, SessionCcrTracker, and SessionToolTracker imports that were
imported but never referenced in the test body. Fixes ruff F401 and I001
lint errors reported by CI.
---------
Co-authored-by: Patrick Ancillotti <patrick.ancillotti@people.inc>
* fix(startup): suppress log noise from litellm, trafilatura, HF hub, and tiktoken warning
* fix(startup): suppress httpx INFO logs from sentence_transformers HEAD checks
* docs(changelog): add entry for startup log noise suppression fixes
* refactor(startup): extract hf_hub_download_local_first into onnx_runtime
The three _hub_download/_hub_dl helpers in embedders.py, onnx_router.py, and
kompress_compressor.py are identical -- try local cache first, fall back to
network download. Extract into a single hf_hub_download_local_first() function
in onnx_runtime.py (the natural home for shared ORT/HF utilities) and update
all three callers to use it.
* fix(lint): sort imports and remove unused _FALLBACK_WARNING_SHOWN import
* fix(lint): cast hf_hub_download return to str for mypy no-any-return
---------
Co-authored-by: Patrick Ancillotti <patrick.ancillotti@people.inc>
2026-06-05 18:44:40 -04:00
|
|
|
import logging
|
2026-06-11 22:10:20 +02:00
|
|
|
import sys
|
fix(startup): suppress proxy startup log noise (#619)
* docs: add enterprise.md
* docs: add link to enterprisemd in README
* fix(copilot): restore generic endpoint for non-subscription OAuth (#610) (#612)
* fix(copilot): restore generic endpoint for non-subscription OAuth (#610)
0.23.0 re-pointed the shared Copilot OAuth branch from the generic
api.githubcopilot.com host to the account-specific endpoints.api host
returned by /copilot_internal/user, and made resolve_copilot_api_url
ignore the GITHUB_COPILOT_API_URL override whenever a token resolved.
That change was meant to add --subscription, but it also altered the
pre-existing non-subscription OAuth flow that worked on 0.22.4. The
account host does not serve newer models (e.g. gpt-5.4) on the responses
API, so wrapped requests began failing with unsupported-model errors
while plain Copilot and 0.22.4 kept working.
Restore 0.22.4 routing for non-subscription OAuth (generic host, still
overridable) and keep account resolution only for --subscription.
resolve_copilot_api_url now honors GITHUB_COPILOT_API_URL first, so the
override escape hatch works for every path. BYOK is unaffected.
Add a regression suite that mocks a successful user-info response, the
real-world path the prior test never exercised (it relied on the network
call failing in CI and falling back to the generic host).
* fix(copilot): route subscription + OAuth through the generic host (#610)
The 0.23.0 endpoint resolution derived the Copilot API host from
/copilot_internal/user (endpoints.api), which returns a segmented host
(e.g. api.individual.githubcopilot.com) that does not serve newer models
on the responses API and is not the host the official Copilot client
routes with (that comes from the token-exchange endpoint). --subscription
used the identical resolution, so it carried the same latent regression
as the non-subscription OAuth path.
Make Copilot host resolution override -> generic for BOTH --subscription
and the implicit OAuth path, and stop using user-info to route. Accounts
that require a dedicated host (enterprise / data residency) pin it via
GITHUB_COPILOT_API_URL. resolve_copilot_api_url no longer makes a network
call; _fetch_copilot_user_info is retained for token validation.
Update the subscription smoke tests that encoded the old account-host
assumption, and add wrap-level + unit coverage that --subscription routes
to the generic host even when user-info advertises an account host, and
that the GITHUB_COPILOT_API_URL override flows through both paths.
* docs(copilot): document generic-host routing + enterprise override (#610)
Spell out the routing contract introduced by the #610 fix so enterprise
users have a supported path. Headroom routes wrapped Copilot hosted
traffic (--subscription and OAuth) to the generic api.githubcopilot.com,
and accounts on a dedicated host (Enterprise Cloud data residency, egress
proxy) pin it via GITHUB_COPILOT_API_URL.
- copilot --help: note the generic host + GITHUB_COPILOT_API_URL override.
- TESTING-copilot-subscription.md: add "API host & Enterprise / data
residency" section; correct the stale api.*.githubcopilot.com claim; and
invite enterprise tenants who want token-exchange-based auto-detection to
open an issue.
- integration-guide.md: short hosted-host + override note in the Copilot
section.
* fix(wrap): report unbindable proxy ports (#602)
* fix(proxy): fail-open on corrupt golden bytes instead of RuntimeError (#603)
* fix(proxy): fail-open on corrupt golden bytes instead of RuntimeError
Permanent session corruption: once golden bytes become unreadable
(UnicodeDecodeError / JSONDecodeError), every subsequent request for
the session raised RuntimeError, returning 500 until proxy restart.
Fix: log at ERROR level and recover — skip the corrupt memory tool,
or regenerate a fresh CCR definition — rather than propagating
RuntimeError and permanently breaking the session.
Also change proxy_inbound_request_aborted from logger.info to
logger.error with exc_info=True so tracebacks appear in logs.
Closes: proxy silent-500 sessions in the wild (observed 2026-06-04)
* fix(tests): re-enable headroom log propagation in corrupt-bytes tests
configure_proxy_logging() sets headroom_logger.propagate = False to prevent
duplicate writes when the proxy redirects stderr to a log file. In CI the
proxy initialises its logging stack before the test suite, leaving propagation
disabled. pytest's caplog handler attaches to the root logger, so records that
stop at the headroom logger are never captured.
Added _enable_headroom_log_propagation autouse fixture that temporarily
re-enables propagation for the duration of each test, making caplog capture
work regardless of the surrounding logging configuration.
* fix(tests): remove unused imports from corrupt-bytes regression tests
Remove json, SessionCcrTracker, and SessionToolTracker imports that were
imported but never referenced in the test body. Fixes ruff F401 and I001
lint errors reported by CI.
---------
Co-authored-by: Patrick Ancillotti <patrick.ancillotti@people.inc>
* fix(startup): suppress log noise from litellm, trafilatura, HF hub, and tiktoken warning
* fix(startup): suppress httpx INFO logs from sentence_transformers HEAD checks
* docs(changelog): add entry for startup log noise suppression fixes
* refactor(startup): extract hf_hub_download_local_first into onnx_runtime
The three _hub_download/_hub_dl helpers in embedders.py, onnx_router.py, and
kompress_compressor.py are identical -- try local cache first, fall back to
network download. Extract into a single hf_hub_download_local_first() function
in onnx_runtime.py (the natural home for shared ORT/HF utilities) and update
all three callers to use it.
* fix(lint): sort imports and remove unused _FALLBACK_WARNING_SHOWN import
* fix(lint): cast hf_hub_download return to str for mypy no-any-return
---------
Co-authored-by: Patrick Ancillotti <patrick.ancillotti@people.inc>
2026-06-05 18:44:40 -04:00
|
|
|
import warnings
|
2026-06-11 22:10:20 +02:00
|
|
|
from types import ModuleType
|
fix(startup): suppress proxy startup log noise (#619)
* docs: add enterprise.md
* docs: add link to enterprisemd in README
* fix(copilot): restore generic endpoint for non-subscription OAuth (#610) (#612)
* fix(copilot): restore generic endpoint for non-subscription OAuth (#610)
0.23.0 re-pointed the shared Copilot OAuth branch from the generic
api.githubcopilot.com host to the account-specific endpoints.api host
returned by /copilot_internal/user, and made resolve_copilot_api_url
ignore the GITHUB_COPILOT_API_URL override whenever a token resolved.
That change was meant to add --subscription, but it also altered the
pre-existing non-subscription OAuth flow that worked on 0.22.4. The
account host does not serve newer models (e.g. gpt-5.4) on the responses
API, so wrapped requests began failing with unsupported-model errors
while plain Copilot and 0.22.4 kept working.
Restore 0.22.4 routing for non-subscription OAuth (generic host, still
overridable) and keep account resolution only for --subscription.
resolve_copilot_api_url now honors GITHUB_COPILOT_API_URL first, so the
override escape hatch works for every path. BYOK is unaffected.
Add a regression suite that mocks a successful user-info response, the
real-world path the prior test never exercised (it relied on the network
call failing in CI and falling back to the generic host).
* fix(copilot): route subscription + OAuth through the generic host (#610)
The 0.23.0 endpoint resolution derived the Copilot API host from
/copilot_internal/user (endpoints.api), which returns a segmented host
(e.g. api.individual.githubcopilot.com) that does not serve newer models
on the responses API and is not the host the official Copilot client
routes with (that comes from the token-exchange endpoint). --subscription
used the identical resolution, so it carried the same latent regression
as the non-subscription OAuth path.
Make Copilot host resolution override -> generic for BOTH --subscription
and the implicit OAuth path, and stop using user-info to route. Accounts
that require a dedicated host (enterprise / data residency) pin it via
GITHUB_COPILOT_API_URL. resolve_copilot_api_url no longer makes a network
call; _fetch_copilot_user_info is retained for token validation.
Update the subscription smoke tests that encoded the old account-host
assumption, and add wrap-level + unit coverage that --subscription routes
to the generic host even when user-info advertises an account host, and
that the GITHUB_COPILOT_API_URL override flows through both paths.
* docs(copilot): document generic-host routing + enterprise override (#610)
Spell out the routing contract introduced by the #610 fix so enterprise
users have a supported path. Headroom routes wrapped Copilot hosted
traffic (--subscription and OAuth) to the generic api.githubcopilot.com,
and accounts on a dedicated host (Enterprise Cloud data residency, egress
proxy) pin it via GITHUB_COPILOT_API_URL.
- copilot --help: note the generic host + GITHUB_COPILOT_API_URL override.
- TESTING-copilot-subscription.md: add "API host & Enterprise / data
residency" section; correct the stale api.*.githubcopilot.com claim; and
invite enterprise tenants who want token-exchange-based auto-detection to
open an issue.
- integration-guide.md: short hosted-host + override note in the Copilot
section.
* fix(wrap): report unbindable proxy ports (#602)
* fix(proxy): fail-open on corrupt golden bytes instead of RuntimeError (#603)
* fix(proxy): fail-open on corrupt golden bytes instead of RuntimeError
Permanent session corruption: once golden bytes become unreadable
(UnicodeDecodeError / JSONDecodeError), every subsequent request for
the session raised RuntimeError, returning 500 until proxy restart.
Fix: log at ERROR level and recover — skip the corrupt memory tool,
or regenerate a fresh CCR definition — rather than propagating
RuntimeError and permanently breaking the session.
Also change proxy_inbound_request_aborted from logger.info to
logger.error with exc_info=True so tracebacks appear in logs.
Closes: proxy silent-500 sessions in the wild (observed 2026-06-04)
* fix(tests): re-enable headroom log propagation in corrupt-bytes tests
configure_proxy_logging() sets headroom_logger.propagate = False to prevent
duplicate writes when the proxy redirects stderr to a log file. In CI the
proxy initialises its logging stack before the test suite, leaving propagation
disabled. pytest's caplog handler attaches to the root logger, so records that
stop at the headroom logger are never captured.
Added _enable_headroom_log_propagation autouse fixture that temporarily
re-enables propagation for the duration of each test, making caplog capture
work regardless of the surrounding logging configuration.
* fix(tests): remove unused imports from corrupt-bytes regression tests
Remove json, SessionCcrTracker, and SessionToolTracker imports that were
imported but never referenced in the test body. Fixes ruff F401 and I001
lint errors reported by CI.
---------
Co-authored-by: Patrick Ancillotti <patrick.ancillotti@people.inc>
* fix(startup): suppress log noise from litellm, trafilatura, HF hub, and tiktoken warning
* fix(startup): suppress httpx INFO logs from sentence_transformers HEAD checks
* docs(changelog): add entry for startup log noise suppression fixes
* refactor(startup): extract hf_hub_download_local_first into onnx_runtime
The three _hub_download/_hub_dl helpers in embedders.py, onnx_router.py, and
kompress_compressor.py are identical -- try local cache first, fall back to
network download. Extract into a single hf_hub_download_local_first() function
in onnx_runtime.py (the natural home for shared ORT/HF utilities) and update
all three callers to use it.
* fix(lint): sort imports and remove unused _FALLBACK_WARNING_SHOWN import
* fix(lint): cast hf_hub_download return to str for mypy no-any-return
---------
Co-authored-by: Patrick Ancillotti <patrick.ancillotti@people.inc>
2026-06-05 18:44:40 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestAnthropicWarnParameter:
|
|
|
|
|
"""AnthropicProvider.warn=False suppresses the no-client tiktoken warning."""
|
|
|
|
|
|
|
|
|
|
def test_warn_true_emits_warning_without_client(self):
|
|
|
|
|
"""Default warn=True should emit UserWarning when no client is given."""
|
|
|
|
|
import headroom.providers.anthropic as _mod
|
|
|
|
|
from headroom.providers.anthropic import AnthropicProvider
|
|
|
|
|
|
|
|
|
|
# Only runs if the module-level dedup flag hasn't fired yet in this process;
|
|
|
|
|
# we reset it to guarantee the warning fires.
|
|
|
|
|
|
|
|
|
|
original = _mod._FALLBACK_WARNING_SHOWN
|
|
|
|
|
_mod._FALLBACK_WARNING_SHOWN = False
|
|
|
|
|
try:
|
|
|
|
|
with warnings.catch_warnings(record=True) as w:
|
|
|
|
|
warnings.simplefilter("always")
|
|
|
|
|
provider = AnthropicProvider(warn=True)
|
|
|
|
|
# Trigger token-counter creation which is where warning fires
|
|
|
|
|
try:
|
|
|
|
|
provider.get_token_counter("claude-3-5-sonnet-20241022")
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
user_warnings = [x for x in w if issubclass(x.category, UserWarning)]
|
|
|
|
|
assert any("tiktoken" in str(warning.message) for warning in user_warnings)
|
|
|
|
|
finally:
|
|
|
|
|
_mod._FALLBACK_WARNING_SHOWN = original
|
|
|
|
|
|
|
|
|
|
def test_warn_false_suppresses_warning(self):
|
|
|
|
|
"""warn=False must produce zero UserWarnings about tiktoken fallback."""
|
|
|
|
|
import headroom.providers.anthropic as _mod
|
|
|
|
|
from headroom.providers.anthropic import AnthropicProvider
|
|
|
|
|
|
|
|
|
|
original = _mod._FALLBACK_WARNING_SHOWN
|
|
|
|
|
_mod._FALLBACK_WARNING_SHOWN = False
|
|
|
|
|
try:
|
|
|
|
|
with warnings.catch_warnings(record=True) as w:
|
|
|
|
|
warnings.simplefilter("always")
|
|
|
|
|
provider = AnthropicProvider(warn=False)
|
|
|
|
|
try:
|
|
|
|
|
provider.get_token_counter("claude-3-5-sonnet-20241022")
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
tiktoken_warnings = [
|
|
|
|
|
x for x in w if issubclass(x.category, UserWarning) and "tiktoken" in str(x.message)
|
|
|
|
|
]
|
|
|
|
|
assert tiktoken_warnings == [], (
|
|
|
|
|
f"Expected no tiktoken warnings with warn=False, got: {tiktoken_warnings}"
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
_mod._FALLBACK_WARNING_SHOWN = original
|
|
|
|
|
|
|
|
|
|
def test_registry_uses_warn_false(self):
|
|
|
|
|
"""The internal proxy provider registry must pass warn=False to AnthropicProvider."""
|
|
|
|
|
import inspect
|
|
|
|
|
|
|
|
|
|
from headroom.providers import registry as _registry_mod
|
|
|
|
|
|
|
|
|
|
source = inspect.getsource(_registry_mod)
|
|
|
|
|
assert "AnthropicProvider(warn=False)" in source, (
|
|
|
|
|
"registry.py must instantiate AnthropicProvider with warn=False"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestEmbedderLogLevels:
|
|
|
|
|
"""headroom.memory.adapters.embedders must set specific logger levels at import time."""
|
|
|
|
|
|
|
|
|
|
def test_huggingface_hub_logger_is_error_or_higher(self):
|
|
|
|
|
"""huggingface_hub logger must be silenced to ERROR or above."""
|
|
|
|
|
import headroom.memory.adapters.embedders # noqa: F401
|
|
|
|
|
|
|
|
|
|
level = logging.getLogger("huggingface_hub").level
|
|
|
|
|
assert level >= logging.ERROR, (
|
|
|
|
|
f"Expected huggingface_hub logger level >= ERROR ({logging.ERROR}), got {level}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_httpx_logger_is_warning_or_higher(self):
|
|
|
|
|
"""httpx logger must be set to WARNING or above to suppress HEAD request INFO lines."""
|
|
|
|
|
import headroom.memory.adapters.embedders # noqa: F401
|
|
|
|
|
|
|
|
|
|
level = logging.getLogger("httpx").level
|
|
|
|
|
assert level >= logging.WARNING, (
|
|
|
|
|
f"Expected httpx logger level >= WARNING ({logging.WARNING}), got {level}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_hf_hub_env_vars_are_set(self):
|
|
|
|
|
"""HF Hub env vars to disable progress bars and implicit tokens must be set."""
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
import headroom.memory.adapters.embedders # noqa: F401
|
|
|
|
|
|
|
|
|
|
assert os.environ.get("HF_HUB_DISABLE_PROGRESS_BARS") == "1"
|
|
|
|
|
assert os.environ.get("HF_HUB_DISABLE_IMPLICIT_TOKEN") == "1"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestLiteLLMLogSuppression:
|
|
|
|
|
"""litellm startup banner suppression must be applied at import time."""
|
|
|
|
|
|
2026-06-11 22:10:20 +02:00
|
|
|
def test_litellm_suppress_env_is_set_before_import(self, monkeypatch):
|
|
|
|
|
"""The env flag must exist before litellm itself is imported."""
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
monkeypatch.delenv("LITELLM_SUPPRESS_DEBUG_INFO", raising=False)
|
|
|
|
|
sys.modules.pop("headroom.providers.litellm", None)
|
|
|
|
|
sys.modules.pop("litellm", None)
|
|
|
|
|
|
|
|
|
|
original_import = builtins.__import__
|
|
|
|
|
fake_litellm = ModuleType("litellm")
|
|
|
|
|
fake_litellm.suppress_debug_info = False
|
|
|
|
|
fake_litellm.set_verbose = True
|
|
|
|
|
fake_litellm.get_model_info = lambda _model: {}
|
|
|
|
|
fake_litellm.model_cost = {}
|
|
|
|
|
fake_litellm.token_counter = lambda **_kwargs: 0
|
|
|
|
|
observed_env: list[str | None] = []
|
|
|
|
|
|
|
|
|
|
def import_spy(name, globals=None, locals=None, fromlist=(), level=0):
|
|
|
|
|
if name == "litellm":
|
|
|
|
|
observed_env.append(os.environ.get("LITELLM_SUPPRESS_DEBUG_INFO"))
|
|
|
|
|
sys.modules["litellm"] = fake_litellm
|
|
|
|
|
return fake_litellm
|
|
|
|
|
return original_import(name, globals, locals, fromlist, level)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(builtins, "__import__", import_spy)
|
|
|
|
|
try:
|
|
|
|
|
importlib.import_module("headroom.providers.litellm")
|
|
|
|
|
finally:
|
|
|
|
|
sys.modules.pop("headroom.providers.litellm", None)
|
|
|
|
|
sys.modules.pop("litellm", None)
|
|
|
|
|
|
|
|
|
|
assert observed_env
|
|
|
|
|
assert all(value == "True" for value in observed_env)
|
|
|
|
|
|
fix(startup): suppress proxy startup log noise (#619)
* docs: add enterprise.md
* docs: add link to enterprisemd in README
* fix(copilot): restore generic endpoint for non-subscription OAuth (#610) (#612)
* fix(copilot): restore generic endpoint for non-subscription OAuth (#610)
0.23.0 re-pointed the shared Copilot OAuth branch from the generic
api.githubcopilot.com host to the account-specific endpoints.api host
returned by /copilot_internal/user, and made resolve_copilot_api_url
ignore the GITHUB_COPILOT_API_URL override whenever a token resolved.
That change was meant to add --subscription, but it also altered the
pre-existing non-subscription OAuth flow that worked on 0.22.4. The
account host does not serve newer models (e.g. gpt-5.4) on the responses
API, so wrapped requests began failing with unsupported-model errors
while plain Copilot and 0.22.4 kept working.
Restore 0.22.4 routing for non-subscription OAuth (generic host, still
overridable) and keep account resolution only for --subscription.
resolve_copilot_api_url now honors GITHUB_COPILOT_API_URL first, so the
override escape hatch works for every path. BYOK is unaffected.
Add a regression suite that mocks a successful user-info response, the
real-world path the prior test never exercised (it relied on the network
call failing in CI and falling back to the generic host).
* fix(copilot): route subscription + OAuth through the generic host (#610)
The 0.23.0 endpoint resolution derived the Copilot API host from
/copilot_internal/user (endpoints.api), which returns a segmented host
(e.g. api.individual.githubcopilot.com) that does not serve newer models
on the responses API and is not the host the official Copilot client
routes with (that comes from the token-exchange endpoint). --subscription
used the identical resolution, so it carried the same latent regression
as the non-subscription OAuth path.
Make Copilot host resolution override -> generic for BOTH --subscription
and the implicit OAuth path, and stop using user-info to route. Accounts
that require a dedicated host (enterprise / data residency) pin it via
GITHUB_COPILOT_API_URL. resolve_copilot_api_url no longer makes a network
call; _fetch_copilot_user_info is retained for token validation.
Update the subscription smoke tests that encoded the old account-host
assumption, and add wrap-level + unit coverage that --subscription routes
to the generic host even when user-info advertises an account host, and
that the GITHUB_COPILOT_API_URL override flows through both paths.
* docs(copilot): document generic-host routing + enterprise override (#610)
Spell out the routing contract introduced by the #610 fix so enterprise
users have a supported path. Headroom routes wrapped Copilot hosted
traffic (--subscription and OAuth) to the generic api.githubcopilot.com,
and accounts on a dedicated host (Enterprise Cloud data residency, egress
proxy) pin it via GITHUB_COPILOT_API_URL.
- copilot --help: note the generic host + GITHUB_COPILOT_API_URL override.
- TESTING-copilot-subscription.md: add "API host & Enterprise / data
residency" section; correct the stale api.*.githubcopilot.com claim; and
invite enterprise tenants who want token-exchange-based auto-detection to
open an issue.
- integration-guide.md: short hosted-host + override note in the Copilot
section.
* fix(wrap): report unbindable proxy ports (#602)
* fix(proxy): fail-open on corrupt golden bytes instead of RuntimeError (#603)
* fix(proxy): fail-open on corrupt golden bytes instead of RuntimeError
Permanent session corruption: once golden bytes become unreadable
(UnicodeDecodeError / JSONDecodeError), every subsequent request for
the session raised RuntimeError, returning 500 until proxy restart.
Fix: log at ERROR level and recover — skip the corrupt memory tool,
or regenerate a fresh CCR definition — rather than propagating
RuntimeError and permanently breaking the session.
Also change proxy_inbound_request_aborted from logger.info to
logger.error with exc_info=True so tracebacks appear in logs.
Closes: proxy silent-500 sessions in the wild (observed 2026-06-04)
* fix(tests): re-enable headroom log propagation in corrupt-bytes tests
configure_proxy_logging() sets headroom_logger.propagate = False to prevent
duplicate writes when the proxy redirects stderr to a log file. In CI the
proxy initialises its logging stack before the test suite, leaving propagation
disabled. pytest's caplog handler attaches to the root logger, so records that
stop at the headroom logger are never captured.
Added _enable_headroom_log_propagation autouse fixture that temporarily
re-enables propagation for the duration of each test, making caplog capture
work regardless of the surrounding logging configuration.
* fix(tests): remove unused imports from corrupt-bytes regression tests
Remove json, SessionCcrTracker, and SessionToolTracker imports that were
imported but never referenced in the test body. Fixes ruff F401 and I001
lint errors reported by CI.
---------
Co-authored-by: Patrick Ancillotti <patrick.ancillotti@people.inc>
* fix(startup): suppress log noise from litellm, trafilatura, HF hub, and tiktoken warning
* fix(startup): suppress httpx INFO logs from sentence_transformers HEAD checks
* docs(changelog): add entry for startup log noise suppression fixes
* refactor(startup): extract hf_hub_download_local_first into onnx_runtime
The three _hub_download/_hub_dl helpers in embedders.py, onnx_router.py, and
kompress_compressor.py are identical -- try local cache first, fall back to
network download. Extract into a single hf_hub_download_local_first() function
in onnx_runtime.py (the natural home for shared ORT/HF utilities) and update
all three callers to use it.
* fix(lint): sort imports and remove unused _FALLBACK_WARNING_SHOWN import
* fix(lint): cast hf_hub_download return to str for mypy no-any-return
---------
Co-authored-by: Patrick Ancillotti <patrick.ancillotti@people.inc>
2026-06-05 18:44:40 -04:00
|
|
|
def test_litellm_suppress_debug_info_is_set(self):
|
|
|
|
|
"""litellm.suppress_debug_info must be True after importing the litellm provider."""
|
|
|
|
|
litellm = pytest_importorskip_litellm()
|
|
|
|
|
if litellm is None:
|
|
|
|
|
return # litellm not installed — skip gracefully
|
|
|
|
|
|
|
|
|
|
import headroom.providers.litellm # noqa: F401
|
|
|
|
|
|
|
|
|
|
assert litellm.suppress_debug_info is True, (
|
|
|
|
|
"litellm.suppress_debug_info must be True to silence startup banner"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_litellm_set_verbose_is_false(self):
|
|
|
|
|
"""litellm.set_verbose must be False after importing the litellm provider."""
|
|
|
|
|
litellm = pytest_importorskip_litellm()
|
|
|
|
|
if litellm is None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
import headroom.providers.litellm # noqa: F401
|
|
|
|
|
|
|
|
|
|
assert litellm.set_verbose is False, (
|
|
|
|
|
"litellm.set_verbose must be False to suppress verbose debug output"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pytest_importorskip_litellm():
|
|
|
|
|
"""Return litellm if installed, else None (for graceful skip in optional-dep tests)."""
|
|
|
|
|
try:
|
|
|
|
|
import litellm
|
|
|
|
|
|
|
|
|
|
return litellm
|
|
|
|
|
except ImportError:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestTrafilaturaLogLevel:
|
|
|
|
|
"""trafilatura logger must be raised to CRITICAL to suppress parse-error noise."""
|
|
|
|
|
|
|
|
|
|
def test_trafilatura_logger_is_critical(self):
|
|
|
|
|
"""trafilatura logger must be CRITICAL or above after importing html_extractor."""
|
|
|
|
|
pytest_importorskip_trafilatura()
|
|
|
|
|
|
|
|
|
|
import headroom.transforms.html_extractor # noqa: F401
|
|
|
|
|
|
|
|
|
|
level = logging.getLogger("trafilatura").level
|
|
|
|
|
assert level >= logging.CRITICAL, (
|
|
|
|
|
f"Expected trafilatura logger level >= CRITICAL ({logging.CRITICAL}), got {level}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pytest_importorskip_trafilatura():
|
|
|
|
|
"""Skip test if trafilatura is not installed."""
|
|
|
|
|
try:
|
|
|
|
|
import trafilatura # noqa: F401
|
|
|
|
|
except ImportError:
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
pytest.skip("trafilatura not installed")
|