mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-22 01:35:28 -04:00
fix(code): allow suppressing LangSmith key override warning (#4436)
Reword the LangSmith env var override warning and add `DEEPAGENTS_CODE_SUPPRESS_ENV_OVERRIDE_WARNING` to silence it. --- When both `LANGSMITH_API_KEY` and `DEEPAGENTS_CODE_LANGSMITH_API_KEY` are set to different values, bootstrap logs a warning. The old wording implied the two variables can't coexist. This rewords it to explain the override is expected (the prefixed value wins inside the Deep Agents Code process; the canonical shell var is left untouched for other tools) and adds an opt-in `DEEPAGENTS_CODE_SUPPRESS_ENV_OVERRIDE_WARNING` flag to silence it — so users who intentionally keep `LANGSMITH_API_KEY` set for other SDKs no longer need the `unset` alias workaround. ### Why the warning exists (the footgun it guards against) The precedence is silent: when both are set, the `DEEPAGENTS_CODE_`-prefixed value always wins and overwrites the canonical one inside the process, with no other signal. The risk is that a user thinks their traces are going to one place when they're actually going to another. User story: months ago I exported `DEEPAGENTS_CODE_LANGSMITH_API_KEY` (pointing at my personal scratch workspace) while testing dcode, and it's still in my shell profile. Today I set `LANGSMITH_API_KEY` to my team's shared workspace key, expecting dcode's traces to show up there for a debugging session with a teammate. Without any warning, the stale prefixed key silently overrides it — my traces land in my personal workspace, my teammate sees nothing, and I burn time wondering why. Sending traces (which can contain prompts/tool output) to an unexpected LangSmith account is exactly the kind of quiet mistake that's hard to notice and hard to debug. The warning surfaces that mismatch so the override is a deliberate choice rather than a silent surprise. The tradeoff is that it also nags users who set both on purpose — which is why this PR both softens the wording and adds an explicit opt-out. Made by [Open SWE](https://openswe.vercel.app/agents/5cb0013b-5a70-8f23-b828-aff0fd26c720) --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
@@ -225,6 +225,20 @@ Defaults to enabled; set to a falsy value (`0`, `false`, `no`, `off`, or empty)
|
||||
to suppress the success toast while still opening URLs normally.
|
||||
"""
|
||||
|
||||
SUPPRESS_ENV_OVERRIDE_WARNING = "DEEPAGENTS_CODE_SUPPRESS_ENV_OVERRIDE_WARNING"
|
||||
"""Silence the startup warning emitted when a `DEEPAGENTS_CODE_`-prefixed
|
||||
LangSmith variable overrides its canonical counterpart (e.g. both
|
||||
`LANGSMITH_API_KEY` and `DEEPAGENTS_CODE_LANGSMITH_API_KEY` are set to
|
||||
different values).
|
||||
|
||||
The override is intentional: the prefixed value overwrites the canonical
|
||||
variable inside the Deep Agents Code process (so the LangSmith SDK, which
|
||||
only reads canonical names, picks it up). The value you exported in your own
|
||||
shell is unaffected, since a process cannot change its parent's environment.
|
||||
Off by default; set to a truthy value (`1`, `true`, `yes`, `on`) to suppress
|
||||
the warning when this coexistence is expected. Parsed by `is_env_truthy`.
|
||||
"""
|
||||
|
||||
THEME = "DEEPAGENTS_CODE_THEME"
|
||||
"""Force the CLI to launch with this theme name when set."""
|
||||
|
||||
|
||||
@@ -745,8 +745,11 @@ def _ensure_bootstrap() -> None:
|
||||
# LangSmith SDK reads os.environ directly and has no knowledge
|
||||
# of the DEEPAGENTS_CODE_ prefix. Setting canonical vars here
|
||||
# bridges that gap.
|
||||
from deepagents_code._env_vars import SUPPRESS_ENV_OVERRIDE_WARNING
|
||||
from deepagents_code.model_config import _ENV_PREFIX
|
||||
|
||||
suppress_override_warning = is_env_truthy(SUPPRESS_ENV_OVERRIDE_WARNING)
|
||||
|
||||
for canonical in (
|
||||
"LANGSMITH_API_KEY",
|
||||
"LANGCHAIN_API_KEY",
|
||||
@@ -762,14 +765,22 @@ def _ensure_bootstrap() -> None:
|
||||
os.environ[canonical] = prefixed_val
|
||||
elif os.environ[canonical] != prefixed_val:
|
||||
os.environ[canonical] = prefixed_val
|
||||
logger.warning(
|
||||
"Both %s and %s are set with different values; "
|
||||
"using %s. Unset %s to silence this warning.",
|
||||
canonical,
|
||||
prefixed,
|
||||
prefixed,
|
||||
canonical,
|
||||
)
|
||||
if not suppress_override_warning:
|
||||
logger.warning(
|
||||
"%s and %s are both set to different values. Deep "
|
||||
"Agents Code uses %s for this session (the "
|
||||
"%s-prefixed value takes precedence). The %s you "
|
||||
"exported in your own shell is unaffected. This is "
|
||||
"expected. To silence this warning, unset %s or set "
|
||||
"%s=1.",
|
||||
canonical,
|
||||
prefixed,
|
||||
prefixed,
|
||||
_ENV_PREFIX,
|
||||
canonical,
|
||||
canonical,
|
||||
SUPPRESS_ENV_OVERRIDE_WARNING,
|
||||
)
|
||||
|
||||
# Bridge stored service keys, apply stored LangSmith tracing defaults,
|
||||
# disable orphaned tracing, and route active tracing to the displayed
|
||||
|
||||
@@ -1071,7 +1071,7 @@ _STATIC_OPTIONS: tuple[ConfigOption, ...] = (
|
||||
kind=OptionKind.STRUCTURED,
|
||||
toml_keys=("threads", "columns"),
|
||||
),
|
||||
# --- Warnings (config.toml-only) -----------------------------------
|
||||
# --- Warnings ------------------------------------------------------
|
||||
ConfigOption(
|
||||
key="warnings.suppress",
|
||||
group="Warnings",
|
||||
@@ -1079,6 +1079,14 @@ _STATIC_OPTIONS: tuple[ConfigOption, ...] = (
|
||||
kind=OptionKind.STRUCTURED,
|
||||
toml_keys=("warnings", "suppress"),
|
||||
),
|
||||
ConfigOption(
|
||||
key="warnings.suppress_env_override",
|
||||
group="Warnings",
|
||||
summary="Silence the LangSmith env-var override warning at startup.",
|
||||
kind=OptionKind.BOOL,
|
||||
default=False,
|
||||
env_var=_env_vars.SUPPRESS_ENV_OVERRIDE_WARNING,
|
||||
),
|
||||
# --- Updates --------------------------------------------------------
|
||||
ConfigOption(
|
||||
key="update.auto_update",
|
||||
|
||||
@@ -859,11 +859,17 @@ class TestGetProjectFiles:
|
||||
|
||||
@staticmethod
|
||||
def _init_repo(root: Path) -> None:
|
||||
"""Initialize a throwaway git repo with a test identity."""
|
||||
"""Initialize a throwaway git repo with a test identity.
|
||||
|
||||
Commit signing is disabled locally so commits succeed even when the
|
||||
host has `commit.gpgsign=true` set globally (no signing key is
|
||||
available in the throwaway repo).
|
||||
"""
|
||||
for args in (
|
||||
["init"],
|
||||
["config", "user.email", "test@example.com"],
|
||||
["config", "user.name", "Test"],
|
||||
["config", "commit.gpgsign", "false"],
|
||||
):
|
||||
subprocess.run(["git", *args], cwd=root, check=True, capture_output=True)
|
||||
|
||||
|
||||
@@ -4792,6 +4792,132 @@ class TestLazyModuleAttributes:
|
||||
config_mod._bootstrap_state.done = original_done
|
||||
config_mod._bootstrap_state.original_langsmith_project = original_ls
|
||||
|
||||
def test_bootstrap_warns_on_conflicting_override(
|
||||
self, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""A conflicting prefixed override logs a single explanatory warning."""
|
||||
import logging
|
||||
|
||||
import deepagents_code.config as config_mod
|
||||
from deepagents_code.config import _ensure_bootstrap
|
||||
|
||||
original_done = config_mod._bootstrap_state.done
|
||||
original_ls = config_mod._bootstrap_state.original_langsmith_project
|
||||
config_mod._bootstrap_state.done = False
|
||||
|
||||
try:
|
||||
monkeypatch.setenv("LANGSMITH_API_KEY", "lsv2_original")
|
||||
monkeypatch.setenv("DEEPAGENTS_CODE_LANGSMITH_API_KEY", "lsv2_override")
|
||||
monkeypatch.delenv(
|
||||
"DEEPAGENTS_CODE_SUPPRESS_ENV_OVERRIDE_WARNING", raising=False
|
||||
)
|
||||
monkeypatch.delenv("DEEPAGENTS_CODE_LANGSMITH_PROJECT", raising=False)
|
||||
|
||||
with (
|
||||
patch("deepagents_code.config._load_dotenv"),
|
||||
patch(
|
||||
"deepagents_code.project_utils.get_server_project_context",
|
||||
return_value=None,
|
||||
),
|
||||
caplog.at_level(logging.WARNING, logger="deepagents_code.config"),
|
||||
):
|
||||
_ensure_bootstrap()
|
||||
|
||||
warnings = [
|
||||
r.getMessage()
|
||||
for r in caplog.records
|
||||
if "DEEPAGENTS_CODE_LANGSMITH_API_KEY" in r.getMessage()
|
||||
]
|
||||
assert len(warnings) == 1
|
||||
assert "DEEPAGENTS_CODE_SUPPRESS_ENV_OVERRIDE_WARNING=1" in warnings[0]
|
||||
finally:
|
||||
config_mod._bootstrap_state.done = original_done
|
||||
config_mod._bootstrap_state.original_langsmith_project = original_ls
|
||||
|
||||
def test_bootstrap_suppresses_override_warning(
|
||||
self, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""The suppression flag silences the override warning but keeps the override."""
|
||||
import logging
|
||||
import os
|
||||
|
||||
import deepagents_code.config as config_mod
|
||||
from deepagents_code.config import _ensure_bootstrap
|
||||
|
||||
original_done = config_mod._bootstrap_state.done
|
||||
original_ls = config_mod._bootstrap_state.original_langsmith_project
|
||||
config_mod._bootstrap_state.done = False
|
||||
|
||||
try:
|
||||
monkeypatch.setenv("LANGSMITH_API_KEY", "lsv2_original")
|
||||
monkeypatch.setenv("DEEPAGENTS_CODE_LANGSMITH_API_KEY", "lsv2_override")
|
||||
monkeypatch.setenv("DEEPAGENTS_CODE_SUPPRESS_ENV_OVERRIDE_WARNING", "1")
|
||||
monkeypatch.delenv("DEEPAGENTS_CODE_LANGSMITH_PROJECT", raising=False)
|
||||
|
||||
with (
|
||||
patch("deepagents_code.config._load_dotenv"),
|
||||
patch(
|
||||
"deepagents_code.project_utils.get_server_project_context",
|
||||
return_value=None,
|
||||
),
|
||||
caplog.at_level(logging.WARNING, logger="deepagents_code.config"),
|
||||
):
|
||||
_ensure_bootstrap()
|
||||
|
||||
# Override still applies; only the warning is silenced.
|
||||
assert os.environ["LANGSMITH_API_KEY"] == "lsv2_override"
|
||||
assert not [
|
||||
r
|
||||
for r in caplog.records
|
||||
if "DEEPAGENTS_CODE_LANGSMITH_API_KEY" in r.getMessage()
|
||||
]
|
||||
finally:
|
||||
config_mod._bootstrap_state.done = original_done
|
||||
config_mod._bootstrap_state.original_langsmith_project = original_ls
|
||||
|
||||
def test_bootstrap_no_warning_when_values_match(
|
||||
self, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Matching canonical and prefixed values propagate without warning."""
|
||||
import logging
|
||||
import os
|
||||
|
||||
import deepagents_code.config as config_mod
|
||||
from deepagents_code.config import _ensure_bootstrap
|
||||
|
||||
original_done = config_mod._bootstrap_state.done
|
||||
original_ls = config_mod._bootstrap_state.original_langsmith_project
|
||||
config_mod._bootstrap_state.done = False
|
||||
|
||||
try:
|
||||
monkeypatch.setenv("LANGSMITH_API_KEY", "lsv2_same")
|
||||
monkeypatch.setenv("DEEPAGENTS_CODE_LANGSMITH_API_KEY", "lsv2_same")
|
||||
monkeypatch.delenv(
|
||||
"DEEPAGENTS_CODE_SUPPRESS_ENV_OVERRIDE_WARNING", raising=False
|
||||
)
|
||||
monkeypatch.delenv("DEEPAGENTS_CODE_LANGSMITH_PROJECT", raising=False)
|
||||
|
||||
with (
|
||||
patch("deepagents_code.config._load_dotenv"),
|
||||
patch(
|
||||
"deepagents_code.project_utils.get_server_project_context",
|
||||
return_value=None,
|
||||
),
|
||||
caplog.at_level(logging.WARNING, logger="deepagents_code.config"),
|
||||
):
|
||||
_ensure_bootstrap()
|
||||
|
||||
# No conflict, so no warning; the shared value stays in place.
|
||||
assert os.environ["LANGSMITH_API_KEY"] == "lsv2_same"
|
||||
assert not [
|
||||
r
|
||||
for r in caplog.records
|
||||
if "DEEPAGENTS_CODE_LANGSMITH_API_KEY" in r.getMessage()
|
||||
]
|
||||
finally:
|
||||
config_mod._bootstrap_state.done = original_done
|
||||
config_mod._bootstrap_state.original_langsmith_project = original_ls
|
||||
|
||||
def test_bootstrap_propagates_empty_string(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user