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:
Mason Daugherty
2026-07-02 19:42:29 -04:00
committed by GitHub
parent b1dbf5e66a
commit ddcae5e0bd
5 changed files with 175 additions and 10 deletions
+14
View File
@@ -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."""
+19 -8
View File
@@ -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
+9 -1
View File
@@ -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)
+126
View File
@@ -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: