From 6dc759c012a8c8ac689fae78406fba352926a3fa Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Wed, 21 Jan 2026 11:10:32 +0100 Subject: [PATCH 1/9] skeleton opengradient client --- mcp/opengradient_client.go | 96 ++++++++++++++++++++++++++++++++++++++ trader/auto_trader.go | 5 ++ 2 files changed, 101 insertions(+) create mode 100644 mcp/opengradient_client.go diff --git a/mcp/opengradient_client.go b/mcp/opengradient_client.go new file mode 100644 index 0000000000..b8114437c9 --- /dev/null +++ b/mcp/opengradient_client.go @@ -0,0 +1,96 @@ +package mcp + +import ( + "net/http" +) + +const ( + ProviderOpenGradient = "opengradient" + DefaultOpenGradientBaseURL = "https://api.opengradient.ai/v1" + DefaultOpenGradientModel = "llama-3.3-70b" +) + +type OpenGradientClient struct { + *Client +} + +// NewOpenGradientClient creates OpenGradient client (backward compatible) +func NewOpenGradientClient() AIClient { + return NewOpenGradientClientWithOptions() +} + +// NewOpenGradientClientWithOptions creates OpenGradient client (supports options pattern) +// +// Usage examples: +// +// // Basic usage +// client := mcp.NewOpenGradientClientWithOptions() +// +// // Custom configuration +// client := mcp.NewOpenGradientClientWithOptions( +// mcp.WithAPIKey("sk-xxx"), +// mcp.WithModel("custom-model"), +// ) +func NewOpenGradientClientWithOptions(opts ...ClientOption) AIClient { + // 1. Create OpenGradient preset options + ogOpts := []ClientOption{ + WithProvider(ProviderOpenGradient), + WithModel(DefaultOpenGradientModel), + WithBaseURL(DefaultOpenGradientBaseURL), + } + + // 2. Merge user options (user options have higher priority) + allOpts := append(ogOpts, opts...) + + // 3. Create base client + baseClient := NewClient(allOpts...).(*Client) + + // 4. Create OpenGradient client + ogClient := &OpenGradientClient{ + Client: baseClient, + } + + // 5. Set hooks to point to OpenGradientClient (implement dynamic dispatch) + baseClient.hooks = ogClient + + return ogClient +} + +func (c *OpenGradientClient) SetAPIKey(apiKey string, customURL string, customModel string) { + c.APIKey = apiKey + + if len(apiKey) > 8 { + c.logger.Infof("🔧 [MCP] OpenGradient API Key: %s...%s", apiKey[:4], apiKey[len(apiKey)-4:]) + } + if customURL != "" { + c.BaseURL = customURL + c.logger.Infof("🔧 [MCP] OpenGradient using custom BaseURL: %s", customURL) + } else { + c.logger.Infof("🔧 [MCP] OpenGradient using default BaseURL: %s", c.BaseURL) + } + if customModel != "" { + c.Model = customModel + c.logger.Infof("🔧 [MCP] OpenGradient using custom Model: %s", customModel) + } else { + c.logger.Infof("🔧 [MCP] OpenGradient using default Model: %s", c.Model) + } +} + +func (c *OpenGradientClient) setAuthHeader(reqHeaders http.Header) { + // TODO: Implement x402 authentication + c.Client.setAuthHeader(reqHeaders) +} + +// TODO: Override these hooks when implementing x402 protocol: +// +// func (c *OpenGradientClient) buildMCPRequestBody(systemPrompt, userPrompt string) map[string]any { +// } +// +// func (c *OpenGradientClient) buildUrl() string { +// } +// +// func (c *OpenGradientClient) buildRequest(url string, jsonData []byte) (*http.Request, error) { +// } +// +// func (c *OpenGradientClient) parseMCPResponse(body []byte) (string, error) { +// } diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 3ec70635aa..4d0d29df1a 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -186,6 +186,11 @@ func NewAutoTrader(config AutoTraderConfig, st *store.Store, userID string) (*Au mcpClient.SetAPIKey(apiKey, config.CustomAPIURL, config.CustomModelName) logger.Infof("🤖 [%s] Using Alibaba Cloud Qwen AI", config.Name) + case "opengradient": + mcpClient = mcp.NewOpenGradientClient() + mcpClient.SetAPIKey(config.CustomAPIKey, config.CustomAPIURL, config.CustomModelName) + logger.Infof("🤖 [%s] Using OpenGradient AI", config.Name) + case "custom": mcpClient = mcp.New() mcpClient.SetAPIKey(config.CustomAPIKey, config.CustomAPIURL, config.CustomModelName) From 9f2981d3a1866b29be5d3e2e66003675c47b121d Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Wed, 21 Jan 2026 11:21:07 +0100 Subject: [PATCH 2/9] implementation --- go.mod | 21 +++++---- go.sum | 34 ++++---------- mcp/config.go | 9 ++-- mcp/opengradient_client.go | 90 ++++++++++++++++++++++++++++---------- mcp/options.go | 11 +++++ 5 files changed, 102 insertions(+), 63 deletions(-) diff --git a/go.mod b/go.mod index eb33cad413..9701a05d72 100644 --- a/go.mod +++ b/go.mod @@ -5,19 +5,25 @@ go 1.25.3 require ( github.com/adshao/go-binance/v2 v2.8.9 github.com/agiledragon/gomonkey/v2 v2.13.0 + github.com/bybit-exchange/bybit.go.api v0.0.0-20250727214011-c9347d6804d6 + github.com/coinbase/x402/go v0.0.0-20260120205736-fb6527d8f56d + github.com/elliottech/lighter-go v0.0.0-20251104171447-78b9b55ebc48 github.com/ethereum/go-ethereum v1.16.7 github.com/gin-gonic/gin v1.11.0 - github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 github.com/golang-jwt/jwt/v5 v5.2.0 github.com/google/uuid v1.6.0 - github.com/gorilla/websocket v1.5.3 github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.9 github.com/pquerna/otp v1.4.0 github.com/rs/zerolog v1.34.0 github.com/sirupsen/logrus v1.9.3 github.com/sonirico/go-hyperliquid v0.26.0 github.com/stretchr/testify v1.11.1 golang.org/x/crypto v0.42.0 + golang.org/x/net v0.43.0 + gorm.io/driver/postgres v1.6.0 + gorm.io/driver/sqlite v1.6.0 + gorm.io/gorm v1.31.1 modernc.org/sqlite v1.40.0 ) @@ -27,7 +33,6 @@ require ( github.com/bitly/go-simplejson v0.5.1 // indirect github.com/bits-and-blooms/bitset v1.24.0 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect - github.com/bybit-exchange/bybit.go.api v0.0.0-20250727214011-c9347d6804d6 // indirect github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect @@ -39,7 +44,6 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/elastic/go-sysinfo v1.15.4 // indirect github.com/elastic/go-windows v1.0.2 // indirect - github.com/elliottech/lighter-go v0.0.0-20251104171447-78b9b55ebc48 // indirect github.com/elliottech/poseidon_crypto v0.0.11 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect github.com/ethereum/go-verkle v0.2.2 // indirect @@ -50,6 +54,7 @@ require ( github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/goccy/go-json v0.10.4 // indirect github.com/goccy/go-yaml v1.18.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/holiman/uint256 v1.3.2 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect @@ -62,7 +67,6 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/lib/pq v1.10.9 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -75,7 +79,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/procfs v0.17.0 // indirect github.com/quic-go/qpack v0.5.1 // indirect - github.com/quic-go/quic-go v0.54.0 // indirect + github.com/quic-go/quic-go v0.55.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sonirico/vago v0.10.0 // indirect @@ -89,20 +93,15 @@ require ( go.elastic.co/apm/module/apmzerolog/v2 v2.7.1 // indirect go.elastic.co/apm/v2 v2.7.1 // indirect go.elastic.co/fastjson v1.5.1 // indirect - go.uber.org/mock v0.5.0 // indirect golang.org/x/arch v0.20.0 // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/mod v0.27.0 // indirect - golang.org/x/net v0.43.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/text v0.29.0 // indirect golang.org/x/tools v0.36.0 // indirect google.golang.org/protobuf v1.36.9 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gorm.io/driver/postgres v1.6.0 // indirect - gorm.io/driver/sqlite v1.6.0 // indirect - gorm.io/gorm v1.31.1 // indirect howett.net/plist v1.0.1 // indirect modernc.org/libc v1.66.10 // indirect modernc.org/mathutil v1.7.1 // indirect diff --git a/go.sum b/go.sum index 4830278038..28aa384bbb 100644 --- a/go.sum +++ b/go.sum @@ -2,22 +2,16 @@ github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608 github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= -github.com/adshao/go-binance/v2 v2.8.7 h1:n7jkhwIHMdtd/9ZU2gTqFV15XVSbUCjyFlOUAtTd8uU= -github.com/adshao/go-binance/v2 v2.8.7/go.mod h1:XkkuecSyJKPolaCGf/q4ovJYB3t0P+7RUYTbGr+LMGM= github.com/adshao/go-binance/v2 v2.8.9 h1:NX+4u/LgEmrjTS7OMWU+9ZgfHKFM61RPhnr9/SqWPhc= github.com/adshao/go-binance/v2 v2.8.9/go.mod h1:XkkuecSyJKPolaCGf/q4ovJYB3t0P+7RUYTbGr+LMGM= github.com/agiledragon/gomonkey/v2 v2.13.0 h1:B24Jg6wBI1iB8EFR1c+/aoTg7QN/Cum7YffG8KMIyYo= github.com/agiledragon/gomonkey/v2 v2.13.0/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= -github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= github.com/bits-and-blooms/bitset v1.24.0 h1:H4x4TuulnokZKvHLfzVRTHJfFfnHEeSYJizujEZvmAM= github.com/bits-and-blooms/bitset v1.24.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bybit-exchange/bybit.go.api v0.0.0-20250727214011-c9347d6804d6 h1:41FLQtKmxWEdyjdgrAm9lZFdS0Ax2XsDxkd/fuztsyQ= @@ -28,6 +22,8 @@ github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZw github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/coinbase/x402/go v0.0.0-20260120205736-fb6527d8f56d h1:eWrkhrrZfE87PaNKUgbw1I+PYxPezjB1y+VrwJa/Akg= +github.com/coinbase/x402/go v0.0.0-20260120205736-fb6527d8f56d/go.mod h1:iNI3Kf6WZGnlV89JxKPIt2pH4qep/0e5sioekycurcQ= github.com/consensys/gnark-crypto v0.19.0 h1:zXCqeY2txSaMl6G5wFpZzMWJU9HPNh8qxPnYJ1BL9vA= github.com/consensys/gnark-crypto v0.19.0/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -36,7 +32,6 @@ github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -58,8 +53,6 @@ github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= -github.com/ethereum/go-ethereum v1.16.5 h1:GZI995PZkzP7ySCxEFaOPzS8+bd8NldE//1qvQDQpe0= -github.com/ethereum/go-ethereum v1.16.5/go.mod h1:kId9vOtlYg3PZk9VwKbGlQmSACB5ESPTBGT+M9zjmok= github.com/ethereum/go-ethereum v1.16.7 h1:qeM4TvbrWK0UC0tgkZ7NiRsmBGwsjqc64BHo20U59UQ= github.com/ethereum/go-ethereum v1.16.7/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk= github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= @@ -82,8 +75,6 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= -github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= -github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= @@ -164,7 +155,6 @@ github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -178,7 +168,6 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -188,8 +177,8 @@ github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7D github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= -github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= +github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= @@ -207,12 +196,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/sonirico/go-hyperliquid v0.17.0 h1:eXYACWupwu41O1VtKw17dqe9oOLQ1A2nRElGhg5Ox+4= -github.com/sonirico/go-hyperliquid v0.17.0/go.mod h1:sH51Vsu+tPUwc95TL2MoQ8YXSewLWBEJirgzo7sZx6w= github.com/sonirico/go-hyperliquid v0.26.0 h1:C2KjaD2R/AxH1FOPl6W1LyvAx/XUHdTQYgjb4PUcPN0= github.com/sonirico/go-hyperliquid v0.26.0/go.mod h1:SYzazq5hqC8lI1+MgSO0aJVrf0TAfyibp5NjUqnwv2I= -github.com/sonirico/vago v0.9.0 h1:DF2OWW2Aaf1xPZmnFv79kBrHmjKX3mVvMbP08vERlKo= -github.com/sonirico/vago v0.9.0/go.mod h1:fZxV1RzMe2eaZokbbDvuyoOzG3YapzqRQoOiD9VyJH0= github.com/sonirico/vago v0.10.0 h1:y+4Wo56tK+88a5lUwVrZUO2RRLaPcBgjI5cupKpT1Oc= github.com/sonirico/vago v0.10.0/go.mod h1:HCfnyPHId7V+zBZ5BLfIsdHIO+ewo6+uhF1N0hxlldc= github.com/sonirico/vago/lol v0.0.0-20250901170347-2d1d82c510bd h1:rbvNORW8/0AtH/8W/SUwUykbuh2SeQBrNgFLqYpGTWY= @@ -239,8 +224,6 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= -github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= -github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM= github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= @@ -253,8 +236,10 @@ go.elastic.co/apm/v2 v2.7.1 h1:OFjARuESjBsxw7wHrEAnfSVNCHGBATXSI/kPvBARY/A= go.elastic.co/apm/v2 v2.7.1/go.mod h1:tQhBAjwh93b2leuAdzGwta/sP7Yc7QoKTSjeIHHDuog= go.elastic.co/fastjson v1.5.1 h1:zeh1xHrFH79aQ6Xsw7YxixvnOdAl3OSv0xch/jRDzko= go.elastic.co/fastjson v1.5.1/go.mod h1:WtvH5wz8z9pDOPqNYSYKoLLv/9zCWZLeejHWuvdL/EM= -go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= -go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go= +go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -287,9 +272,8 @@ google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXn gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/dnaeon/go-vcr.v4 v4.0.5 h1:I0hpTIvD5rII+8LgYGrHMA2d4SQPoL6u7ZvJakWKsiA= -gopkg.in/dnaeon/go-vcr.v4 v4.0.5/go.mod h1:dRos81TkW9C1WJt6tTaE+uV2Lo8qJT3AG2b35+CB/nQ= gopkg.in/dnaeon/go-vcr.v4 v4.0.6 h1:PiJkrakkmzc5s7EfBnZOnyiLwi7o7A9fwPzN0X2uwe0= +gopkg.in/dnaeon/go-vcr.v4 v4.0.6/go.mod h1:sbq5oMEcM4PXngbcNbHhzfCP9OdZodLhrbRYoyg09HY= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/mcp/config.go b/mcp/config.go index 1aebadd2f9..b4f0797cfa 100644 --- a/mcp/config.go +++ b/mcp/config.go @@ -12,10 +12,11 @@ import ( // Config client configuration (centralized management of all configurations) type Config struct { // Provider configuration - Provider string - APIKey string - BaseURL string - Model string + Provider string + APIKey string + BaseURL string + Model string + OpenGradientPrivateKey string // EVM private key for x402 payments (used by OpenGradient) // Behavior configuration MaxTokens int diff --git a/mcp/opengradient_client.go b/mcp/opengradient_client.go index b8114437c9..fe844f8464 100644 --- a/mcp/opengradient_client.go +++ b/mcp/opengradient_client.go @@ -1,7 +1,13 @@ package mcp import ( + "fmt" "net/http" + + x402 "github.com/coinbase/x402/go" + x402http "github.com/coinbase/x402/go/http" + evm "github.com/coinbase/x402/go/mechanisms/evm/exact/client" + evmsigners "github.com/coinbase/x402/go/signers/evm" ) const ( @@ -12,6 +18,9 @@ const ( type OpenGradientClient struct { *Client + privateKey string + x402Client *x402.X402Client + x402Wrapped bool } // NewOpenGradientClient creates OpenGradient client (backward compatible) @@ -23,12 +32,12 @@ func NewOpenGradientClient() AIClient { // // Usage examples: // -// // Basic usage +// // Basic usage (requires private key to be set later via SetPrivateKey) // client := mcp.NewOpenGradientClientWithOptions() // -// // Custom configuration +// // With private key for x402 payments // client := mcp.NewOpenGradientClientWithOptions( -// mcp.WithAPIKey("sk-xxx"), +// mcp.WithOpenGradientPrivateKey("0x..."), // mcp.WithModel("custom-model"), // ) func NewOpenGradientClientWithOptions(opts ...ClientOption) AIClient { @@ -47,20 +56,58 @@ func NewOpenGradientClientWithOptions(opts ...ClientOption) AIClient { // 4. Create OpenGradient client ogClient := &OpenGradientClient{ - Client: baseClient, + Client: baseClient, + privateKey: baseClient.config.OpenGradientPrivateKey, + } + + // 5. Initialize x402 if private key is provided + if ogClient.privateKey != "" { + if err := ogClient.initX402(); err != nil { + baseClient.logger.Warnf("⚠️ [MCP] Failed to initialize x402: %v", err) + } } - // 5. Set hooks to point to OpenGradientClient (implement dynamic dispatch) + // 6. Set hooks to point to OpenGradientClient (implement dynamic dispatch) baseClient.hooks = ogClient return ogClient } +// initX402 initializes the x402 client with EVM signer +func (c *OpenGradientClient) initX402() error { + // Create EVM signer from private key + signer, err := evmsigners.NewClientSignerFromPrivateKey(c.privateKey) + if err != nil { + return fmt.Errorf("failed to create EVM signer: %w", err) + } + + // Create x402 client with EVM scheme registration + c.x402Client = x402.Newx402Client(). + Register("eip155:*", evm.NewExactEvmScheme(signer)) + + // Wrap HTTP client with x402 payment support + c.httpClient = x402http.WrapHTTPClientWithPayment( + c.httpClient, + x402http.Newx402HTTPClient(c.x402Client), + ) + c.x402Wrapped = true + + c.logger.Infof("🔐 [MCP] OpenGradient x402 payment initialized") + return nil +} + +// SetPrivateKey sets the EVM private key and initializes x402 +func (c *OpenGradientClient) SetPrivateKey(privateKey string) error { + c.privateKey = privateKey + return c.initX402() +} + func (c *OpenGradientClient) SetAPIKey(apiKey string, customURL string, customModel string) { - c.APIKey = apiKey + // For OpenGradient with x402, the apiKey parameter is used as the private key + c.privateKey = apiKey if len(apiKey) > 8 { - c.logger.Infof("🔧 [MCP] OpenGradient API Key: %s...%s", apiKey[:4], apiKey[len(apiKey)-4:]) + c.logger.Infof("🔧 [MCP] OpenGradient Private Key: %s...%s", apiKey[:4], apiKey[len(apiKey)-4:]) } if customURL != "" { c.BaseURL = customURL @@ -74,23 +121,20 @@ func (c *OpenGradientClient) SetAPIKey(apiKey string, customURL string, customMo } else { c.logger.Infof("🔧 [MCP] OpenGradient using default Model: %s", c.Model) } + + // Initialize x402 with the private key + if c.privateKey != "" && !c.x402Wrapped { + if err := c.initX402(); err != nil { + c.logger.Warnf("⚠️ [MCP] Failed to initialize x402: %v", err) + } + } + + // Set a placeholder API key for the base client (required for CallWithMessages check) + c.APIKey = "x402-authenticated" } func (c *OpenGradientClient) setAuthHeader(reqHeaders http.Header) { - // TODO: Implement x402 authentication - c.Client.setAuthHeader(reqHeaders) + // x402 handles authentication via the wrapped HTTP client + // No Bearer token needed - the x402 wrapper intercepts 402 responses + // and automatically adds payment signatures } - -// TODO: Override these hooks when implementing x402 protocol: -// -// func (c *OpenGradientClient) buildMCPRequestBody(systemPrompt, userPrompt string) map[string]any { -// } -// -// func (c *OpenGradientClient) buildUrl() string { -// } -// -// func (c *OpenGradientClient) buildRequest(url string, jsonData []byte) (*http.Request, error) { -// } -// -// func (c *OpenGradientClient) parseMCPResponse(body []byte) (string, error) { -// } diff --git a/mcp/options.go b/mcp/options.go index 3e962627b2..17d21b0d76 100644 --- a/mcp/options.go +++ b/mcp/options.go @@ -103,6 +103,17 @@ func WithAPIKey(apiKey string) ClientOption { } } +// WithOpenGradientPrivateKey sets EVM private key for x402 payments +// +// Usage example: +// +// client := mcp.NewOpenGradientClientWithOptions(mcp.WithOpenGradientPrivateKey("0x...")) +func WithOpenGradientPrivateKey(privateKey string) ClientOption { + return func(c *Config) { + c.OpenGradientPrivateKey = privateKey + } +} + // WithBaseURL sets base URL func WithBaseURL(baseURL string) ClientOption { return func(c *Config) { From 9941692b8e11d2e3b66ed572b8f2fa45ea58bcb0 Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Wed, 21 Jan 2026 11:25:26 +0100 Subject: [PATCH 3/9] add test --- mcp/opengradient_client_test.go | 317 ++++++++++++++++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 mcp/opengradient_client_test.go diff --git a/mcp/opengradient_client_test.go b/mcp/opengradient_client_test.go new file mode 100644 index 0000000000..c5c110db69 --- /dev/null +++ b/mcp/opengradient_client_test.go @@ -0,0 +1,317 @@ +package mcp + +import ( + "testing" + "time" +) + +// ============================================================ +// Test OpenGradientClient Creation and Configuration +// ============================================================ + +func TestNewOpenGradientClient_Default(t *testing.T) { + client := NewOpenGradientClient() + + if client == nil { + t.Fatal("client should not be nil") + } + + // Type assertion check + ogClient, ok := client.(*OpenGradientClient) + if !ok { + t.Fatal("client should be *OpenGradientClient") + } + + // Verify default values + if ogClient.Provider != ProviderOpenGradient { + t.Errorf("Provider should be '%s', got '%s'", ProviderOpenGradient, ogClient.Provider) + } + + if ogClient.BaseURL != DefaultOpenGradientBaseURL { + t.Errorf("BaseURL should be '%s', got '%s'", DefaultOpenGradientBaseURL, ogClient.BaseURL) + } + + if ogClient.Model != DefaultOpenGradientModel { + t.Errorf("Model should be '%s', got '%s'", DefaultOpenGradientModel, ogClient.Model) + } + + if ogClient.logger == nil { + t.Error("logger should not be nil") + } + + if ogClient.httpClient == nil { + t.Error("httpClient should not be nil") + } + + // x402 should not be initialized without private key + if ogClient.x402Wrapped { + t.Error("x402 should not be wrapped without private key") + } +} + +func TestNewOpenGradientClientWithOptions(t *testing.T) { + mockLogger := NewMockLogger() + customModel := "llama-3.1-8b" + + client := NewOpenGradientClientWithOptions( + WithLogger(mockLogger), + WithModel(customModel), + WithMaxTokens(4000), + ) + + ogClient := client.(*OpenGradientClient) + + // Verify custom options are applied + if ogClient.logger != mockLogger { + t.Error("logger should be set from option") + } + + if ogClient.Model != customModel { + t.Errorf("Model should be '%s', got '%s'", customModel, ogClient.Model) + } + + if ogClient.MaxTokens != 4000 { + t.Error("MaxTokens should be 4000") + } + + // Verify OpenGradient default values are retained + if ogClient.Provider != ProviderOpenGradient { + t.Errorf("Provider should still be '%s'", ProviderOpenGradient) + } + + if ogClient.BaseURL != DefaultOpenGradientBaseURL { + t.Errorf("BaseURL should still be '%s'", DefaultOpenGradientBaseURL) + } +} + +func TestNewOpenGradientClientWithOptions_CustomBaseURL(t *testing.T) { + customURL := "https://custom.opengradient.com/v2" + + client := NewOpenGradientClientWithOptions( + WithBaseURL(customURL), + ) + + ogClient := client.(*OpenGradientClient) + + if ogClient.BaseURL != customURL { + t.Errorf("BaseURL should be '%s', got '%s'", customURL, ogClient.BaseURL) + } +} + +// ============================================================ +// Test SetAPIKey +// ============================================================ + +func TestOpenGradientClient_SetAPIKey(t *testing.T) { + mockLogger := NewMockLogger() + client := NewOpenGradientClientWithOptions( + WithLogger(mockLogger), + ) + + ogClient := client.(*OpenGradientClient) + + // Test setting API Key (which is used as private key for OpenGradient) + ogClient.SetAPIKey("0x1234567890abcdef", "", "") + + if ogClient.privateKey != "0x1234567890abcdef" { + t.Errorf("privateKey should be '0x1234567890abcdef', got '%s'", ogClient.privateKey) + } + + // APIKey should be set to placeholder + if ogClient.APIKey != "x402-authenticated" { + t.Errorf("APIKey should be 'x402-authenticated', got '%s'", ogClient.APIKey) + } + + // Verify logging + logs := mockLogger.GetLogsByLevel("INFO") + if len(logs) == 0 { + t.Error("should have logged private key setting") + } + + // Verify BaseURL and Model remain default + if ogClient.BaseURL != DefaultOpenGradientBaseURL { + t.Error("BaseURL should remain default") + } + + if ogClient.Model != DefaultOpenGradientModel { + t.Error("Model should remain default") + } +} + +func TestOpenGradientClient_SetAPIKey_WithCustomURL(t *testing.T) { + mockLogger := NewMockLogger() + client := NewOpenGradientClientWithOptions( + WithLogger(mockLogger), + ) + + ogClient := client.(*OpenGradientClient) + + customURL := "https://custom.api.com/v1" + ogClient.SetAPIKey("0x1234567890abcdef", customURL, "") + + if ogClient.BaseURL != customURL { + t.Errorf("BaseURL should be '%s', got '%s'", customURL, ogClient.BaseURL) + } + + // Verify logging + logs := mockLogger.GetLogsByLevel("INFO") + hasCustomURLLog := false + for _, log := range logs { + if log.Format == "🔧 [MCP] OpenGradient using custom BaseURL: %s" { + hasCustomURLLog = true + break + } + } + + if !hasCustomURLLog { + t.Error("should have logged custom BaseURL") + } +} + +func TestOpenGradientClient_SetAPIKey_WithCustomModel(t *testing.T) { + mockLogger := NewMockLogger() + client := NewOpenGradientClientWithOptions( + WithLogger(mockLogger), + ) + + ogClient := client.(*OpenGradientClient) + + customModel := "llama-3.1-405b" + ogClient.SetAPIKey("0x1234567890abcdef", "", customModel) + + if ogClient.Model != customModel { + t.Errorf("Model should be '%s', got '%s'", customModel, ogClient.Model) + } + + // Verify logging + logs := mockLogger.GetLogsByLevel("INFO") + hasCustomModelLog := false + for _, log := range logs { + if log.Format == "🔧 [MCP] OpenGradient using custom Model: %s" { + hasCustomModelLog = true + break + } + } + + if !hasCustomModelLog { + t.Error("should have logged custom Model") + } +} + +// ============================================================ +// Test Timeout +// ============================================================ + +func TestOpenGradientClient_Timeout(t *testing.T) { + client := NewOpenGradientClientWithOptions( + WithTimeout(30 * time.Second), + ) + + ogClient := client.(*OpenGradientClient) + + if ogClient.httpClient.Timeout != 30*time.Second { + t.Errorf("expected timeout 30s, got %v", ogClient.httpClient.Timeout) + } + + // Test SetTimeout + client.SetTimeout(60 * time.Second) + + if ogClient.httpClient.Timeout != 60*time.Second { + t.Errorf("expected timeout 60s after SetTimeout, got %v", ogClient.httpClient.Timeout) + } +} + +// ============================================================ +// Test hooks Mechanism +// ============================================================ + +func TestOpenGradientClient_HooksIntegration(t *testing.T) { + client := NewOpenGradientClientWithOptions() + ogClient := client.(*OpenGradientClient) + + // Verify hooks point to ogClient itself (implements polymorphism) + if ogClient.hooks != ogClient { + t.Error("hooks should point to ogClient for polymorphism") + } + + // Verify buildUrl uses OpenGradient configuration + url := ogClient.buildUrl() + expectedURL := DefaultOpenGradientBaseURL + "/chat/completions" + if url != expectedURL { + t.Errorf("expected URL '%s', got '%s'", expectedURL, url) + } +} + +// ============================================================ +// Test setAuthHeader (should be empty for x402) +// ============================================================ + +func TestOpenGradientClient_SetAuthHeader(t *testing.T) { + client := NewOpenGradientClientWithOptions() + ogClient := client.(*OpenGradientClient) + + // Create mock headers + headers := make(map[string][]string) + + // Call setAuthHeader + ogClient.setAuthHeader(headers) + + // Verify no Authorization header is set (x402 handles auth) + if _, exists := headers["Authorization"]; exists { + t.Error("Authorization header should not be set for x402 client") + } +} + +// ============================================================ +// Test Constants +// ============================================================ + +func TestOpenGradientConstants(t *testing.T) { + if ProviderOpenGradient != "opengradient" { + t.Errorf("ProviderOpenGradient should be 'opengradient', got '%s'", ProviderOpenGradient) + } + + if DefaultOpenGradientBaseURL != "https://api.opengradient.ai/v1" { + t.Errorf("DefaultOpenGradientBaseURL should be 'https://api.opengradient.ai/v1', got '%s'", DefaultOpenGradientBaseURL) + } + + if DefaultOpenGradientModel != "llama-3.3-70b" { + t.Errorf("DefaultOpenGradientModel should be 'llama-3.3-70b', got '%s'", DefaultOpenGradientModel) + } +} + +// ============================================================ +// Test WithOpenGradientPrivateKey Option +// ============================================================ + +func TestWithOpenGradientPrivateKey(t *testing.T) { + // Note: We can't fully test x402 initialization without a valid private key + // This test verifies the option is properly passed to config + mockLogger := NewMockLogger() + + client := NewOpenGradientClientWithOptions( + WithLogger(mockLogger), + WithOpenGradientPrivateKey("0xinvalidkey"), // Invalid key, will fail x402 init + ) + + ogClient := client.(*OpenGradientClient) + + // Private key should be set + if ogClient.privateKey != "0xinvalidkey" { + t.Errorf("privateKey should be '0xinvalidkey', got '%s'", ogClient.privateKey) + } + + // x402 init should have failed (logged warning) + warnLogs := mockLogger.GetLogsByLevel("WARN") + hasX402Warning := false + for _, log := range warnLogs { + if log.Format == "⚠️ [MCP] Failed to initialize x402: %v" { + hasX402Warning = true + break + } + } + + if !hasX402Warning { + t.Error("should have logged x402 initialization warning for invalid key") + } +} From ff8ff1f81a7ce9da2d5d286ff78a7311a0026dfc Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Wed, 21 Jan 2026 11:58:11 +0100 Subject: [PATCH 4/9] scheme --- mcp/opengradient_client.go | 41 +++++- mcp/opengradient_client_test.go | 8 +- mcp/opengradient_integration_test.go | 203 +++++++++++++++++++++++++++ 3 files changed, 243 insertions(+), 9 deletions(-) create mode 100644 mcp/opengradient_integration_test.go diff --git a/mcp/opengradient_client.go b/mcp/opengradient_client.go index fe844f8464..6c32f7ad61 100644 --- a/mcp/opengradient_client.go +++ b/mcp/opengradient_client.go @@ -1,21 +1,44 @@ package mcp import ( + "context" "fmt" "net/http" x402 "github.com/coinbase/x402/go" x402http "github.com/coinbase/x402/go/http" - evm "github.com/coinbase/x402/go/mechanisms/evm/exact/client" + evmV1 "github.com/coinbase/x402/go/mechanisms/evm/exact/v1/client" evmsigners "github.com/coinbase/x402/go/signers/evm" + "github.com/coinbase/x402/go/types" ) const ( ProviderOpenGradient = "opengradient" - DefaultOpenGradientBaseURL = "https://api.opengradient.ai/v1" - DefaultOpenGradientModel = "llama-3.3-70b" + DefaultOpenGradientBaseURL = "https://llm.opengradient.ai/v1" + DefaultOpenGradientModel = "openai/gpt-4.1" + + // OpenGradient devnet chain ID (0x29f8 = 10744) + ogDevnetNetwork = "og-devnet" + ogDevnetChainID = "eip155:10744" ) +// ogDevnetSchemeWrapper wraps the EVM V1 scheme to translate og-devnet network to CAIP-2 format +type ogDevnetSchemeWrapper struct { + inner *evmV1.ExactEvmSchemeV1 +} + +func (w *ogDevnetSchemeWrapper) Scheme() string { + return w.inner.Scheme() +} + +func (w *ogDevnetSchemeWrapper) CreatePaymentPayload(ctx context.Context, requirements types.PaymentRequirementsV1) (types.PaymentPayloadV1, error) { + // Translate og-devnet to CAIP-2 format + if requirements.Network == ogDevnetNetwork { + requirements.Network = ogDevnetChainID + } + return w.inner.CreatePaymentPayload(ctx, requirements) +} + type OpenGradientClient struct { *Client privateKey string @@ -81,9 +104,14 @@ func (c *OpenGradientClient) initX402() error { return fmt.Errorf("failed to create EVM signer: %w", err) } - // Create x402 client with EVM scheme registration + // Create x402 client with EVM V1 scheme registration + // OpenGradient uses x402 V1 with custom network "og-devnet" + // We wrap the scheme to translate og-devnet to CAIP-2 format (eip155:10744) + wrappedScheme := &ogDevnetSchemeWrapper{ + inner: evmV1.NewExactEvmSchemeV1(signer), + } c.x402Client = x402.Newx402Client(). - Register("eip155:*", evm.NewExactEvmScheme(signer)) + RegisterV1(ogDevnetNetwork, wrappedScheme) // Wrap HTTP client with x402 payment support c.httpClient = x402http.WrapHTTPClientWithPayment( @@ -92,6 +120,9 @@ func (c *OpenGradientClient) initX402() error { ) c.x402Wrapped = true + // Set placeholder API key for base client (required for CallWithMessages check) + c.APIKey = "x402-authenticated" + c.logger.Infof("🔐 [MCP] OpenGradient x402 payment initialized") return nil } diff --git a/mcp/opengradient_client_test.go b/mcp/opengradient_client_test.go index c5c110db69..fe47ce5cef 100644 --- a/mcp/opengradient_client_test.go +++ b/mcp/opengradient_client_test.go @@ -271,12 +271,12 @@ func TestOpenGradientConstants(t *testing.T) { t.Errorf("ProviderOpenGradient should be 'opengradient', got '%s'", ProviderOpenGradient) } - if DefaultOpenGradientBaseURL != "https://api.opengradient.ai/v1" { - t.Errorf("DefaultOpenGradientBaseURL should be 'https://api.opengradient.ai/v1', got '%s'", DefaultOpenGradientBaseURL) + if DefaultOpenGradientBaseURL != "https://llm.opengradient.ai/v1" { + t.Errorf("DefaultOpenGradientBaseURL should be 'https://llm.opengradient.ai/v1', got '%s'", DefaultOpenGradientBaseURL) } - if DefaultOpenGradientModel != "llama-3.3-70b" { - t.Errorf("DefaultOpenGradientModel should be 'llama-3.3-70b', got '%s'", DefaultOpenGradientModel) + if DefaultOpenGradientModel != "openai/gpt-4.1" { + t.Errorf("DefaultOpenGradientModel should be 'openai/gpt-4.1', got '%s'", DefaultOpenGradientModel) } } diff --git a/mcp/opengradient_integration_test.go b/mcp/opengradient_integration_test.go new file mode 100644 index 0000000000..52dae2bab3 --- /dev/null +++ b/mcp/opengradient_integration_test.go @@ -0,0 +1,203 @@ +//go:build integration + +package mcp + +import ( + "os" + "strings" + "testing" +) + +// Integration tests for OpenGradient client with x402 payments. +// These tests require a real EVM private key with funds. +// +// Run with: +// OPENGRADIENT_PRIVATE_KEY="0x..." go test ./mcp/... -v -tags=integration -run TestOpenGradientIntegration + +func getTestPrivateKey(t *testing.T) string { + privateKey := os.Getenv("OPENGRADIENT_PRIVATE_KEY") + if privateKey == "" { + t.Skip("OPENGRADIENT_PRIVATE_KEY not set, skipping integration test") + } + return privateKey +} + +// ============================================================ +// Basic Integration Tests +// ============================================================ + +func TestOpenGradientIntegration_SimpleCall(t *testing.T) { + privateKey := getTestPrivateKey(t) + + client := NewOpenGradientClientWithOptions( + WithOpenGradientPrivateKey(privateKey), + ) + + result, err := client.CallWithMessages( + "You are a helpful assistant. Respond concisely.", + "What is 2 + 2? Reply with just the number.", + ) + + if err != nil { + t.Fatalf("API call failed: %v", err) + } + + if result == "" { + t.Fatal("Response should not be empty") + } + + t.Logf("Response: %s", result) + + // Basic sanity check - response should contain "4" + if !strings.Contains(result, "4") { + t.Logf("Warning: Expected response to contain '4', got: %s", result) + } +} + +func TestOpenGradientIntegration_WithCustomModel(t *testing.T) { + privateKey := getTestPrivateKey(t) + + client := NewOpenGradientClientWithOptions( + WithOpenGradientPrivateKey(privateKey), + WithModel("openai/gpt-4.1"), // Use default model explicitly + ) + + result, err := client.CallWithMessages( + "You are a helpful assistant.", + "Say hello in one word.", + ) + + if err != nil { + t.Fatalf("API call failed: %v", err) + } + + if result == "" { + t.Fatal("Response should not be empty") + } + + t.Logf("Response: %s", result) +} + +func TestOpenGradientIntegration_SetAPIKeyMethod(t *testing.T) { + privateKey := getTestPrivateKey(t) + + // Test using SetAPIKey method (backward compatible approach) + client := NewOpenGradientClient() + client.SetAPIKey(privateKey, "", "") + + result, err := client.CallWithMessages( + "You are a helpful assistant.", + "What color is the sky? Reply in one word.", + ) + + if err != nil { + t.Fatalf("API call failed: %v", err) + } + + if result == "" { + t.Fatal("Response should not be empty") + } + + t.Logf("Response: %s", result) +} + +// ============================================================ +// Builder Pattern Tests +// ============================================================ + +func TestOpenGradientIntegration_CallWithRequest(t *testing.T) { + privateKey := getTestPrivateKey(t) + + client := NewOpenGradientClientWithOptions( + WithOpenGradientPrivateKey(privateKey), + ) + + req := NewRequestBuilder(). + WithSystemPrompt("You are a helpful coding assistant."). + WithUserPrompt("Write a one-line Python hello world program."). + WithMaxTokens(100). + WithTemperature(0.3). + MustBuild() + + result, err := client.CallWithRequest(req) + + if err != nil { + t.Fatalf("API call failed: %v", err) + } + + if result == "" { + t.Fatal("Response should not be empty") + } + + t.Logf("Response: %s", result) + + // Should contain print or hello + lower := strings.ToLower(result) + if !strings.Contains(lower, "print") && !strings.Contains(lower, "hello") { + t.Logf("Warning: Expected Python hello world, got: %s", result) + } +} + +// ============================================================ +// Error Handling Tests +// ============================================================ + +func TestOpenGradientIntegration_InvalidPrivateKey(t *testing.T) { + // This test verifies behavior with an invalid private key + client := NewOpenGradientClientWithOptions( + WithOpenGradientPrivateKey("invalid-key"), + ) + + _, err := client.CallWithMessages( + "You are a helpful assistant.", + "Hello", + ) + + // Should fail - either during x402 init or API call + if err == nil { + t.Log("Note: Call succeeded with invalid key (x402 may not have initialized)") + } else { + t.Logf("Expected error with invalid key: %v", err) + } +} + +// ============================================================ +// Longer Conversation Test +// ============================================================ + +func TestOpenGradientIntegration_MultiTurn(t *testing.T) { + privateKey := getTestPrivateKey(t) + + client := NewOpenGradientClientWithOptions( + WithOpenGradientPrivateKey(privateKey), + WithMaxTokens(500), + ) + + // First turn + result1, err := client.CallWithMessages( + "You are a helpful assistant. Remember the context of our conversation.", + "My favorite color is blue. What did I just tell you?", + ) + + if err != nil { + t.Fatalf("First API call failed: %v", err) + } + + t.Logf("Response 1: %s", result1) + + if !strings.Contains(strings.ToLower(result1), "blue") { + t.Logf("Warning: Expected response to mention 'blue'") + } + + // Second turn (note: this is a new call, not true multi-turn without message history) + result2, err := client.CallWithMessages( + "You are a helpful assistant.", + "Name three things that are typically blue.", + ) + + if err != nil { + t.Fatalf("Second API call failed: %v", err) + } + + t.Logf("Response 2: %s", result2) +} From b89bfb2ae06f12f43c2c8173401542e4b89df5fd Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Wed, 21 Jan 2026 13:12:58 +0100 Subject: [PATCH 5/9] almost working --- mcp/opengradient_client.go | 60 ++++++++++++++++++---------- mcp/opengradient_client_test.go | 4 +- mcp/opengradient_integration_test.go | 1 - 3 files changed, 42 insertions(+), 23 deletions(-) diff --git a/mcp/opengradient_client.go b/mcp/opengradient_client.go index 6c32f7ad61..79d5e6b844 100644 --- a/mcp/opengradient_client.go +++ b/mcp/opengradient_client.go @@ -2,6 +2,7 @@ package mcp import ( "context" + "crypto/tls" "fmt" "net/http" @@ -14,29 +15,42 @@ import ( const ( ProviderOpenGradient = "opengradient" - DefaultOpenGradientBaseURL = "https://llm.opengradient.ai/v1" - DefaultOpenGradientModel = "openai/gpt-4.1" + DefaultOpenGradientBaseURL = "https://llmogevm.opengradient.ai/v1" + DefaultOpenGradientModel = "google/gemini-2.5-flash" - // OpenGradient devnet chain ID (0x29f8 = 10744) - ogDevnetNetwork = "og-devnet" - ogDevnetChainID = "eip155:10744" + // OpenGradient EVM chain ID (0x40000 = 262144) + ogEvmNetwork = "og-evm" + ogEvmChainID = "eip155:262144" ) -// ogDevnetSchemeWrapper wraps the EVM V1 scheme to translate og-devnet network to CAIP-2 format -type ogDevnetSchemeWrapper struct { +// ogEvmSchemeWrapper wraps the EVM V1 scheme to translate og-evm network to CAIP-2 format +type ogEvmSchemeWrapper struct { inner *evmV1.ExactEvmSchemeV1 } -func (w *ogDevnetSchemeWrapper) Scheme() string { +func (w *ogEvmSchemeWrapper) Scheme() string { return w.inner.Scheme() } -func (w *ogDevnetSchemeWrapper) CreatePaymentPayload(ctx context.Context, requirements types.PaymentRequirementsV1) (types.PaymentPayloadV1, error) { - // Translate og-devnet to CAIP-2 format - if requirements.Network == ogDevnetNetwork { - requirements.Network = ogDevnetChainID +func (w *ogEvmSchemeWrapper) CreatePaymentPayload(ctx context.Context, requirements types.PaymentRequirementsV1) (types.PaymentPayloadV1, error) { + // Save original network + originalNetwork := requirements.Network + + // Translate og-evm to CAIP-2 format for EVM signing + if requirements.Network == ogEvmNetwork { + requirements.Network = ogEvmChainID } - return w.inner.CreatePaymentPayload(ctx, requirements) + + // Create payload with translated network (for proper chain ID signing) + payload, err := w.inner.CreatePaymentPayload(ctx, requirements) + if err != nil { + return payload, err + } + + // Restore original network in payload (server expects og-evm) + payload.Network = originalNetwork + + return payload, nil } type OpenGradientClient struct { @@ -105,19 +119,25 @@ func (c *OpenGradientClient) initX402() error { } // Create x402 client with EVM V1 scheme registration - // OpenGradient uses x402 V1 with custom network "og-devnet" - // We wrap the scheme to translate og-devnet to CAIP-2 format (eip155:10744) - wrappedScheme := &ogDevnetSchemeWrapper{ + // OpenGradient uses x402 V1 with custom network "og-evm" + // We wrap the scheme to translate og-evm to CAIP-2 format (eip155:262144) + wrappedScheme := &ogEvmSchemeWrapper{ inner: evmV1.NewExactEvmSchemeV1(signer), } c.x402Client = x402.Newx402Client(). - RegisterV1(ogDevnetNetwork, wrappedScheme) + RegisterV1(ogEvmNetwork, wrappedScheme) + + // Force HTTP/1.1 - OpenGradient's ALB has issues with HTTP/2 + c.httpClient.Transport = &http.Transport{ + TLSNextProto: make(map[string]func(string, *tls.Conn) http.RoundTripper), + } // Wrap HTTP client with x402 payment support c.httpClient = x402http.WrapHTTPClientWithPayment( c.httpClient, x402http.Newx402HTTPClient(c.x402Client), ) + c.x402Wrapped = true // Set placeholder API key for base client (required for CallWithMessages check) @@ -165,7 +185,7 @@ func (c *OpenGradientClient) SetAPIKey(apiKey string, customURL string, customMo } func (c *OpenGradientClient) setAuthHeader(reqHeaders http.Header) { - // x402 handles authentication via the wrapped HTTP client - // No Bearer token needed - the x402 wrapper intercepts 402 responses - // and automatically adds payment signatures + // OpenGradient requires an Authorization header (any value works) + // The actual auth is handled by x402 payment + reqHeaders.Set("Authorization", "Bearer x402") } diff --git a/mcp/opengradient_client_test.go b/mcp/opengradient_client_test.go index fe47ce5cef..e2ae51d6ac 100644 --- a/mcp/opengradient_client_test.go +++ b/mcp/opengradient_client_test.go @@ -275,8 +275,8 @@ func TestOpenGradientConstants(t *testing.T) { t.Errorf("DefaultOpenGradientBaseURL should be 'https://llm.opengradient.ai/v1', got '%s'", DefaultOpenGradientBaseURL) } - if DefaultOpenGradientModel != "openai/gpt-4.1" { - t.Errorf("DefaultOpenGradientModel should be 'openai/gpt-4.1', got '%s'", DefaultOpenGradientModel) + if DefaultOpenGradientModel != "openai/gpt-4.1-2025-04-14" { + t.Errorf("DefaultOpenGradientModel should be 'openai/gpt-4.1-2025-04-14', got '%s'", DefaultOpenGradientModel) } } diff --git a/mcp/opengradient_integration_test.go b/mcp/opengradient_integration_test.go index 52dae2bab3..41e518a34f 100644 --- a/mcp/opengradient_integration_test.go +++ b/mcp/opengradient_integration_test.go @@ -59,7 +59,6 @@ func TestOpenGradientIntegration_WithCustomModel(t *testing.T) { client := NewOpenGradientClientWithOptions( WithOpenGradientPrivateKey(privateKey), - WithModel("openai/gpt-4.1"), // Use default model explicitly ) result, err := client.CallWithMessages( From 2edd91a35cca1ac811ef2c850dd14b11b2c0b631 Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Wed, 21 Jan 2026 13:38:38 +0100 Subject: [PATCH 6/9] working model --- mcp/opengradient_client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcp/opengradient_client.go b/mcp/opengradient_client.go index 79d5e6b844..9e8265aa1c 100644 --- a/mcp/opengradient_client.go +++ b/mcp/opengradient_client.go @@ -16,7 +16,7 @@ import ( const ( ProviderOpenGradient = "opengradient" DefaultOpenGradientBaseURL = "https://llmogevm.opengradient.ai/v1" - DefaultOpenGradientModel = "google/gemini-2.5-flash" + DefaultOpenGradientModel = "gemini-2.5-flash" // OpenGradient EVM chain ID (0x40000 = 262144) ogEvmNetwork = "og-evm" From 93b43d7b06ace776dbabf441849827f537e54774 Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Wed, 21 Jan 2026 13:43:09 +0100 Subject: [PATCH 7/9] fix tests --- mcp/opengradient_client_test.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/mcp/opengradient_client_test.go b/mcp/opengradient_client_test.go index e2ae51d6ac..678a867755 100644 --- a/mcp/opengradient_client_test.go +++ b/mcp/opengradient_client_test.go @@ -243,7 +243,7 @@ func TestOpenGradientClient_HooksIntegration(t *testing.T) { } // ============================================================ -// Test setAuthHeader (should be empty for x402) +// Test setAuthHeader (sets placeholder for x402) // ============================================================ func TestOpenGradientClient_SetAuthHeader(t *testing.T) { @@ -256,9 +256,10 @@ func TestOpenGradientClient_SetAuthHeader(t *testing.T) { // Call setAuthHeader ogClient.setAuthHeader(headers) - // Verify no Authorization header is set (x402 handles auth) - if _, exists := headers["Authorization"]; exists { - t.Error("Authorization header should not be set for x402 client") + // Verify Authorization header is set to "Bearer x402" + // OpenGradient requires an Authorization header (actual auth is handled by x402 payment) + if auth := headers["Authorization"]; len(auth) == 0 || auth[0] != "Bearer x402" { + t.Errorf("Authorization header should be 'Bearer x402', got '%v'", headers["Authorization"]) } } @@ -271,12 +272,12 @@ func TestOpenGradientConstants(t *testing.T) { t.Errorf("ProviderOpenGradient should be 'opengradient', got '%s'", ProviderOpenGradient) } - if DefaultOpenGradientBaseURL != "https://llm.opengradient.ai/v1" { - t.Errorf("DefaultOpenGradientBaseURL should be 'https://llm.opengradient.ai/v1', got '%s'", DefaultOpenGradientBaseURL) + if DefaultOpenGradientBaseURL != "https://llmogevm.opengradient.ai/v1" { + t.Errorf("DefaultOpenGradientBaseURL should be 'https://llmogevm.opengradient.ai/v1', got '%s'", DefaultOpenGradientBaseURL) } - if DefaultOpenGradientModel != "openai/gpt-4.1-2025-04-14" { - t.Errorf("DefaultOpenGradientModel should be 'openai/gpt-4.1-2025-04-14', got '%s'", DefaultOpenGradientModel) + if DefaultOpenGradientModel != "gemini-2.5-flash" { + t.Errorf("DefaultOpenGradientModel should be 'gemini-2.5-flash', got '%s'", DefaultOpenGradientModel) } } From ab6f80f3306487006c3958e98febe38b292095b7 Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Wed, 21 Jan 2026 14:11:22 +0100 Subject: [PATCH 8/9] switch to smarter default model --- mcp/opengradient_client.go | 2 +- mcp/opengradient_client_test.go | 4 ++-- mcp/opengradient_integration_test.go | 2 -- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/mcp/opengradient_client.go b/mcp/opengradient_client.go index 9e8265aa1c..9a56bcdf67 100644 --- a/mcp/opengradient_client.go +++ b/mcp/opengradient_client.go @@ -16,7 +16,7 @@ import ( const ( ProviderOpenGradient = "opengradient" DefaultOpenGradientBaseURL = "https://llmogevm.opengradient.ai/v1" - DefaultOpenGradientModel = "gemini-2.5-flash" + DefaultOpenGradientModel = "gemini-2.5-pro" // OpenGradient EVM chain ID (0x40000 = 262144) ogEvmNetwork = "og-evm" diff --git a/mcp/opengradient_client_test.go b/mcp/opengradient_client_test.go index 678a867755..ba4156aadb 100644 --- a/mcp/opengradient_client_test.go +++ b/mcp/opengradient_client_test.go @@ -276,8 +276,8 @@ func TestOpenGradientConstants(t *testing.T) { t.Errorf("DefaultOpenGradientBaseURL should be 'https://llmogevm.opengradient.ai/v1', got '%s'", DefaultOpenGradientBaseURL) } - if DefaultOpenGradientModel != "gemini-2.5-flash" { - t.Errorf("DefaultOpenGradientModel should be 'gemini-2.5-flash', got '%s'", DefaultOpenGradientModel) + if DefaultOpenGradientModel != "gemini-2.5-pro" { + t.Errorf("DefaultOpenGradientModel should be 'gemini-2.5-pro', got '%s'", DefaultOpenGradientModel) } } diff --git a/mcp/opengradient_integration_test.go b/mcp/opengradient_integration_test.go index 41e518a34f..4f1d014366 100644 --- a/mcp/opengradient_integration_test.go +++ b/mcp/opengradient_integration_test.go @@ -114,8 +114,6 @@ func TestOpenGradientIntegration_CallWithRequest(t *testing.T) { req := NewRequestBuilder(). WithSystemPrompt("You are a helpful coding assistant."). WithUserPrompt("Write a one-line Python hello world program."). - WithMaxTokens(100). - WithTemperature(0.3). MustBuild() result, err := client.CallWithRequest(req) From 9c37c6e1c6dd4e9249ff7c7f7e7cea0cbe23d4b4 Mon Sep 17 00:00:00 2001 From: "balogh.adam@icloud.com" Date: Wed, 21 Jan 2026 14:22:53 +0100 Subject: [PATCH 9/9] rm unnecessary logs --- mcp/opengradient_client.go | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/mcp/opengradient_client.go b/mcp/opengradient_client.go index 9a56bcdf67..647d31a893 100644 --- a/mcp/opengradient_client.go +++ b/mcp/opengradient_client.go @@ -157,21 +157,12 @@ func (c *OpenGradientClient) SetAPIKey(apiKey string, customURL string, customMo // For OpenGradient with x402, the apiKey parameter is used as the private key c.privateKey = apiKey - if len(apiKey) > 8 { - c.logger.Infof("🔧 [MCP] OpenGradient Private Key: %s...%s", apiKey[:4], apiKey[len(apiKey)-4:]) - } if customURL != "" { c.BaseURL = customURL - c.logger.Infof("🔧 [MCP] OpenGradient using custom BaseURL: %s", customURL) - } else { - c.logger.Infof("🔧 [MCP] OpenGradient using default BaseURL: %s", c.BaseURL) - } + } if customModel != "" { c.Model = customModel - c.logger.Infof("🔧 [MCP] OpenGradient using custom Model: %s", customModel) - } else { - c.logger.Infof("🔧 [MCP] OpenGradient using default Model: %s", c.Model) - } + } // Initialize x402 with the private key if c.privateKey != "" && !c.x402Wrapped {