Skip to content

feat: add live data pull for opengradient token#55

Closed
kylexqian wants to merge 27 commits intomainfrom
claude/admiring-goldberg
Closed

feat: add live data pull for opengradient token#55
kylexqian wants to merge 27 commits intomainfrom
claude/admiring-goldberg

Conversation

@kylexqian
Copy link
Copy Markdown
Collaborator

@kylexqian kylexqian commented Apr 16, 2026

  • Add live data pull for opengradient token. Currently set to ETH as our token is not yet listed
  • Added more CI/CD tests surrounding pulling data from coingecko
  • This also includes a TTL of the coin check time as 2 minutes

dixitaniket and others added 26 commits April 1, 2026 18:53
…ation

Replaces the hardcoded Decimal("1") mock with a real CoinGecko ETH/USD
price fetch (ETH used as OPG proxy until OPG is listed). Caches the
price for 2 minutes so at most one network call is made per TTL window.
Falls back to last-known-good price on refresh failure, or a hard-coded
$2000 floor if no price has ever been fetched.

Config constants (TTL, CoinGecko ID, fallback price) are centralised in
config.py so switching to real OPG requires only a one-line change.

Adds test_util.py with 18 tests covering the fetch, cache, fallback,
and full dynamic_session_cost_calculator pipeline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
test_pricing.py was relying on the old Decimal("1") mock price; patch
get_token_a_price_usd to $1 via setUpModule/tearDownModule so the
hardcoded expected wei values remain valid as pure USD pricing unit tests.

Add tests/test_integration.py (pytest -m integration) with 4 live tests:
- _fetch_opg_price_usd returns a positive Decimal from real CoinGecko
- ETH price falls within a sanity range ($100–$100 000)
- Second call within TTL returns cached value (no second network call)
- Full dynamic_session_cost_calculator pipeline with live price

Add a separate integration-tests CI job in test.yml and register the
integration marker in pyproject.toml. Also add pythonpath = ["."] to
pytest config so tests/ can import tee_gateway when run standalone.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add OPG_PRICE_SANITY_MIN_USD / OPG_PRICE_SANITY_MAX_USD to config.py
($0.000001–$1 000 000) so the integration test stays in sync when
switching from the ETH proxy to real OPG without touching test code.

Widen lower bound from $100 to $0.000001 to accommodate a potentially
low OPG token price. Rename test to test_price_is_within_sanity_bounds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All mentions of ETH/ethereum in comments, log messages, error strings,
and test helpers now refer to OPG_PRICE_COINGECKO_ID so that switching
to the real OPG token requires only a one-line change in config.py.

- util.py: docstring, error message, and log line use the coin ID var
- test_util.py: mock response body and URL assertion key off the config
- test_integration.py: comments and print output use the coin ID var
- config.py: comments no longer mention ETH specifically

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
That method no longer exists in the current og-x402 library — the patch
was a no-op and mypy flagged it as attr-defined on PaymentMiddleware.

In the current library the session_cost_calculator is called inside
_accumulate_session_cost(), which is wrapped by a broad try/except in
StreamingSessionResponse.close(). Replace the dead code with a comment
explaining the current behaviour and where to patch if stricter error
propagation is needed in future.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add OPG_PRICE_FETCH_RETRIES = 3 to config.py. _fetch_opg_price_usd now
loops up to that many attempts, logging a warning on each failure and
re-raising the last exception if all attempts are exhausted.

Add two tests: succeeds-on-last-attempt and raises-after-all-retries,
both keyed off the config constant so they stay correct if the default changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add validate_pricing_preflight(model) to util.py. It checks two things
before any LLM call is made:
  1. model is in the registry (get_model_config raises on unknown models)
  2. token price is positive (guards against a degenerate price feed state)

Both controllers call it immediately after parsing the request and return
400 Bad Request if it raises, so an unrecognised model or broken price
feed is rejected before inference rather than silently producing free
inference after the response has already been sent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- config.py: use "opengradient" as OPG_PRICE_COINGECKO_ID; drop
  OPG_PRICE_HARD_FALLBACK_USD and OPG_PRICE_SANITY_MIN_USD
- util.py: raise clear ValueError when CoinGecko returns no price for
  the coin (token listed but no trading price yet); remove stale-cache
  and hard-fallback branches in get_token_a_price_usd — failures now
  propagate so inference is blocked with a 400 rather than silently
  using a made-up price
- test_util.py: replace hard-fallback test with a "no usd key" test;
  update stale-cache test to assert the error propagates
- test_integration.py: restructure around OPG having no price yet —
  verify the slug is recognised, verify the correct error is raised,
  skip price-dependent tests until OPG begins trading

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- _fetch_opg_price_usd: don't retry ValueError — no-price and
  non-positive-price are deterministic CoinGecko responses that won't
  change on retry; only network/transport exceptions get retried now
- validate_pricing_preflight: remove dead price <= 0 guard (impossible
  since get_token_a_price_usd() now raises instead of returning a
  fallback) and fix stale docstring that still mentioned the removed
  hard fallback
- test_raises_when_coin_has_no_price: assert call_count == 1 to lock
  in the no-retry behaviour

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
test_tool_forwarding.py: the chat controller now runs validate_pricing_preflight
before every request, which hits CoinGecko for real. These tests mock the model
and TEE keys but not the price feed, so they were getting 400s or 429s. Added
the same module-level get_token_a_price_usd patch used in test_pricing.py.

test_integration.py: _opg_has_price() makes one CoinGecko call at collection
time; test_fetch_raises_clear_error_when_no_price then made a second immediate
call, triggering a 429. The test now handles HTTPError 429 by skipping rather
than failing — rate-limiting is transient and not the thing being tested.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ani/token-update removed BASE_TESTNET_OPG_ADDRESS and BASE_TESTNET_NETWORK;
update hardcoded asset address in test_util.py and the import in
test_integration.py to use BASE_MAINNET_OPG_ADDRESS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a live CoinGecko-backed USD price feed for the OPG payment token (with caching/TTL) and wires a pricing preflight into request handlers so pricing failures are detected before any LLM call. It also updates payment/network constants to Base mainnet, bumps og-x402, and expands CI test coverage with offline unit tests plus an opt-in live integration test job.

Changes:

  • Implement live OPG/USD fetch via CoinGecko with retry + 2-minute TTL cache, and add validate_pricing_preflight() to gate requests early.
  • Switch gateway payment configuration from Base testnet to Base mainnet (network + token + recipient), and add the ERC20 approval gas sponsoring extension to x402 routes.
  • Add offline unit tests for price fetching/caching + dynamic pricing, add a live-network integration test file, and split CI into unit vs integration jobs.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tee_gateway/util.py Adds CoinGecko fetch + cached token pricing and a pricing preflight helper.
tee_gateway/config.py Introduces CoinGecko feed configuration (TTL/retries/coin id/sanity max).
tee_gateway/controllers/chat_controller.py Calls pricing preflight before handling chat requests.
tee_gateway/controllers/completions_controller.py Calls pricing preflight before handling completion requests.
tee_gateway/definitions.py Moves network/token constants to Base mainnet and updates recipient/token address constants.
tee_gateway/__main__.py Updates x402 registration/routes for mainnet and adds gas sponsoring extension.
tee_gateway/test/test_util.py New offline unit tests for fetch/caching and dynamic cost math with mocked network.
tests/test_integration.py New live-network integration tests targeting CoinGecko behavior.
tests/test_pricing.py Pins token price to keep pricing unit tests deterministic and updates token address constant.
tee_gateway/test/test_tool_forwarding.py Pins token price to avoid live network calls during tool-forwarding tests.
.github/workflows/test.yml Runs new unit test file and adds a separate integration test job.
pyproject.toml Bumps og-x402 constraint and adds pytest integration marker config.
uv.lock Locks updated dependency graph including og-x402 and nest-asyncio.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tee_gateway/util.py
Comment on lines +158 to 169
import json # noqa: E402
import urllib.request # noqa: E402

from tee_gateway.config import ( # noqa: E402
OPG_PRICE_CACHE_TTL_SECONDS,
OPG_PRICE_COINGECKO_ID,
OPG_PRICE_FETCH_RETRIES,
)
from tee_gateway.definitions import ( # noqa: E402
ASSET_DECIMALS_BY_ADDRESS,
)
from tee_gateway.model_registry import get_model_config # noqa: E402
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These imports are introduced mid-file and suppressed with multiple # noqa: E402. This makes the module harder to read/maintain and can mask real import-order issues. Consider moving the new CoinGecko/config imports up with the rest of the imports at the top of the file (or isolating the price-feed code into its own module) so E402 suppression isn’t needed.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

definitely fix this



class TestGetTokenAPriceUSD(unittest.TestCase):
"""get_token_a_price_usd must respect the TTL and fallback gracefully."""
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring says get_token_a_price_usd "must respect the TTL and fallback gracefully", but the implementation and other tests explicitly expect refresh failures to raise immediately (no fallback). Update this docstring to match the actual behavior to avoid confusion when maintaining the cache semantics.

Suggested change
"""get_token_a_price_usd must respect the TTL and fallback gracefully."""
"""get_token_a_price_usd must respect the TTL and raise on refresh failures."""

Copilot uses AI. Check for mistakes.
Comment thread tee_gateway/config.py
Comment on lines +25 to +28
# CoinGecko coin ID for the OPG token.
# https://www.coingecko.com/en/coins/opengradient
OPG_PRICE_COINGECKO_ID: str = "opengradient"

Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description mentions the live-price pull is "currently set to ETH" because OPG isn't listed yet, but the code is configured to query CoinGecko ID "opengradient". Either update the PR description (or switch config to ETH if that’s still the intended temporary behavior) so implementation and documentation match.

Copilot uses AI. Check for mistakes.
try:
validate_pricing_preflight(chat_request.model)
except ValueError as exc:
return {"error": "Bad Request", "message": str(exc)}, 400
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate_pricing_preflight() can raise non-ValueError exceptions (e.g. urllib.error.URLError / OSError) when CoinGecko is unreachable. This try/except only catches ValueError, so transient network failures will bubble up as an unhandled 500 rather than a controlled client error. Consider catching broader exceptions from validate_pricing_preflight and returning an appropriate status (e.g. 503) while still using 400 for true client input errors (unknown model, no price data).

Suggested change
return {"error": "Bad Request", "message": str(exc)}, 400
return {"error": "Bad Request", "message": str(exc)}, 400
except Exception as exc:
logger.exception(
"Pricing preflight validation failed due to an upstream/service error"
)
return {
"error": "Service Unavailable",
"message": "Pricing validation is temporarily unavailable. Please try again later.",
}, 503

Copilot uses AI. Check for mistakes.
try:
validate_pricing_preflight(body.model)
except ValueError as exc:
return {"error": "Bad Request", "message": str(exc)}, 400
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate_pricing_preflight() may raise non-ValueError exceptions (e.g. URLError/OSError) when the CoinGecko fetch fails. Since only ValueError is caught here (and this call is outside the main try/except below), network failures can become unhandled 500s. Catch broader exceptions from validate_pricing_preflight (and map them to something like 503) while reserving 400 for invalid request inputs.

Suggested change
return {"error": "Bad Request", "message": str(exc)}, 400
return {"error": "Bad Request", "message": str(exc)}, 400
except Exception as exc:
logger.error(f"Pricing preflight error: {str(exc)}", exc_info=True)
return {
"error": "Service Unavailable",
"message": "Pricing validation unavailable",
}, 503

Copilot uses AI. Check for mistakes.
Comment thread tee_gateway/__main__.py
Comment on lines 141 to 149
"POST /v1/completions": RouteConfig(
accepts=[
PaymentOption(
scheme="upto",
pay_to=EVM_PAYMENT_ADDRESS,
price=AssetAmount(
amount=COMPLETIONS_OPG_SESSION_MAX_SPEND,
asset=BASE_OPG_ADDRESS,
amount=CHAT_COMPLETIONS_OPG_SESSION_MAX_SPEND,
asset=BASE_MAINNET_OPG_ADDRESS,
extra={
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The /v1/completions RouteConfig is using CHAT_COMPLETIONS_OPG_SESSION_MAX_SPEND for its static session cap. definitions.py has a separate COMPLETIONS_OPG_SESSION_MAX_SPEND constant; using the chat cap here is likely accidental and will be wrong if the two caps diverge. Import and use COMPLETIONS_OPG_SESSION_MAX_SPEND for the completions route.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix this

Comment thread tests/test_integration.py
Comment thread tests/test_integration.py
Comment thread tee_gateway/__main__.py
),
],
extensions={
**declare_erc20_approval_gas_sponsoring_extension(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this new?

try:
validate_pricing_preflight(body.model)
except ValueError as exc:
return {"error": "Bad Request", "message": str(exc)}, 400
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just let the exception propagate?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normally this would be 500 no? since it's outside of the user's control

Comment thread tee_gateway/util.py
"last_good": None,
"updated_at": 0.0,
}
_token_price_lock = threading.Lock()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there really a need for a lock?

Comment thread tee_gateway/__main__.py
# StreamingSessionResponse.close(). That close() wraps the call in a broad
# try/except, so any exception raised by the calculator (e.g. ValueError for
# an unrecognised model or missing usage data) is logged but otherwise
# swallowed — the session cost is simply not incremented for that request.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this?

Comment thread tee_gateway/util.py
# "updated_at" – epoch seconds of last successful fetch (float)
_token_price_cache: dict[str, Any] = {
"value": Decimal("1"),
"last_good": None,
Copy link
Copy Markdown
Contributor

@adambalogh adambalogh Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please create a namedtuple at least instead of raw dicts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also it wouldn't hurt to create a class instead of global variables for storing these if possible

Comment thread tee_gateway/util.py
inference after the response has already been sent.
"""
get_model_config(model) # raises ValueError for unknown models
get_token_a_price_usd() # raises if price is unavailable
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should run in the background not add potentially 5s delay to requests

Comment thread tee_gateway/__main__.py
# response to the client instead of an incorrect silent charge.
# A previous version of this file monkey-patched _resolve_session_request_cost
# to let exceptions propagate, but that method no longer exists in the library.
# If stricter error propagation is needed in future, patch _accumulate_session_cost
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't this some regression?

Copy link
Copy Markdown
Contributor

@adambalogh adambalogh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please clean the code up

Log at ERROR level when validate_pricing_preflight raises — covers both
the CoinGecko no-price case in util.py and the catch sites in the chat
and completions controllers, so the root cause is visible in server logs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@kylexqian
Copy link
Copy Markdown
Collaborator Author

Changed approach in #56

@kylexqian kylexqian closed this Apr 21, 2026
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.

4 participants