Skip to content

fix(tracing): do not cache a missing OPENAI_API_KEY - #5017

Open
hyeonsang010716 wants to merge 1 commit into
openai:mainfrom
hyeonsang010716:fix/tracing-api-key-resolved-late
Open

hyeonsang010716 wants to merge 1 commit into
openai:mainfrom
hyeonsang010716:fix/tracing-api-key-resolved-late

Conversation

@hyeonsang010716

Copy link
Copy Markdown
Contributor

Summary

BackendSpanExporter.api_key was a cached_property over
self._api_key or os.environ.get("OPENAI_API_KEY"). If the exporter resolved it before the variable
existed, it cached None for the life of the process. Setting OPENAI_API_KEY afterwards never took
effect, and every export kept logging "OPENAI_API_KEY is not set, skipping trace export" even though the
key was now set.

This happens whenever a trace is exported before the key is loaded, for example a notebook run that
fails for lack of a key and is retried after setting it, or an app that calls load_dotenv() or fetches
secrets after its first traced run. The default model client recovers in the same situation, because a
failed client creation is not cached, so model calls start working while tracing stays off. That
contradicts docs/config.md, which says tracing uses the same key as model requests.

The cache was added in #289 so the key is read when the exporter needs it rather than at import time,
and #1339 later made set_api_key() clear it. This change keeps both intents without caching a missing
key:

  • api_key is now a property that reads the explicit key once and consults the environment only when
    it is missing.
  • A key found in the environment is stored and kept, as before, so a later change to the variable does
    not reroute exports that did not call set_api_key().
  • A lookup that finds nothing stores nothing. The export worker resolves the key on its own thread, so
    writing the missing result back could otherwise discard a key that set_api_key() sets meanwhile, on
    every keyless export.
  • A setter keeps exporter.api_key = "..." working. It was never documented, but public code relies on
    it: the maokangkun/SigmaFlow setup page tells users to run default_exporter().api_key = "...". The
    setter writes the key directly, as the old assignment did, and accepts str | None.

Behavior changes worth knowing about, none of which I found documented, tested, or used in public code:

  • del exporter.api_key and instance-level mock.patch.object(exporter, "api_key", ...) now raise
    AttributeError. Class-level patching still works.
  • Assigning None or "" now falls back to the environment, matching set_api_key(""), instead of
    pinning a missing key. set_tracing_disabled(True) remains the way to stop exports.
  • A subclass that assigns self.api_key before calling super().__init__() loses that value, and
    pyright reports a cached_property override of api_key as incompatible.

organization and project are unchanged. They are read only after a key is available, so the
scenario above does not leave them stale. The race in which the one lookup that finds
OPENAI_API_KEY overlaps a concurrent set_api_key() also behaves exactly as before.

Test plan

  • tests/tracing/test_processor_api_key.py, each exercising exporter.export() or the public attribute:
    • test_exporter_uses_env_api_key_set_after_an_export_without_one exports with no key, sets the
      variable, and asserts the next export sends it. It fails on main.
    • test_exporter_keeps_env_api_key_once_resolved changes the variable after a key is resolved and
      asserts the original key is still sent. It passes on main and fails on a property that does not
      cache at all.
    • test_exporter_uses_an_assigned_api_key passes on main and fails without the setter.
    • test_keyless_lookup_does_not_discard_a_key_set_while_it_runs calls set_api_key() from inside the
      environment lookup and asserts the key survives. It fails on main and on a getter that writes back
      whatever the lookup returned.
  • tests/tracing/test_set_api_key_fix.py reset its explicit key by assigning the private _api_key and
    deleting the cache entry, which no longer exists. It now calls set_api_key(""), which covers the same
    fallback through the public API and passes on main as well.
  • ruff format --check, ruff check, check_optional_truthiness.py src/agents, mypy src (312 files)
    and pyright are clean.
  • Parallel suite: 8596 passed, 179 skipped. Serial suite: 77 passed, 4 skipped. The 12 sandbox failures
    are the pre-existing symlink-privilege ones (Sandbox tar extraction and 12 sandbox tests fail on Windows without symlink privilege #4852); a pristine tree on this host produces the same 12.
  • make is not installed here, so the Makefile targets were run directly in the script's order:
    format, lint, typecheck, then the parallel and serial suites.

Issue number

None.

Checks

  • I've added new tests, if relevant
  • I've run .agents/skills/code-change-verification/scripts/run.sh
  • I've confirmed all verification steps pass
  • If using Codex, I've run /review before submitting this PR

`BackendSpanExporter.api_key` was a `cached_property` over
`self._api_key or os.environ.get("OPENAI_API_KEY")`. Resolving it before
the variable existed cached `None`, so a key set afterwards, for example
by `load_dotenv()`, never took effect and every export kept logging
"OPENAI_API_KEY is not set, skipping trace export".

`api_key` is now a property that reads the explicit key once and falls
back to the environment only when it is missing. A key found in the
environment is kept, so a later change to the variable does not reroute
exports. A lookup that finds nothing stores nothing, so it cannot
discard a key that `set_api_key()` sets on another thread meanwhile.

A setter keeps `exporter.api_key = "..."` working, since public code
relies on assigning the attribute.
@00200200

Copy link
Copy Markdown
Contributor

Reviewed the complete diff against fbf59a40.

I found no actionable correctness issue. The property preserves the explicit-key precedence and existing environment fallback, avoids caching a missing key, and the setter keeps direct exporter.api_key = ... assignments working. The added tests cover late environment configuration, stable resolution, direct assignment, and the keyless lookup race.

Focused verification on the PR snapshot passed: tests/tracing/test_processor_api_key.py and tests/tracing/test_set_api_key_fix.py — 8 passed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants