diff --git a/go/go.mod b/go/go.mod index 0659353cd1..aa3eed82f6 100644 --- a/go/go.mod +++ b/go/go.mod @@ -62,6 +62,7 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/leanovate/gopter v0.2.11 // indirect go.opencensus.io v0.24.0 // indirect ) diff --git a/go/go.sum b/go/go.sum index c832ade7c6..7744685873 100644 --- a/go/go.sum +++ b/go/go.sum @@ -1,6 +1,25 @@ cel.dev/expr v0.23.0 h1:wUb94w6OYQS4uXraxo9U+wUAs9jT47Xvl4iPgAwM2ss= cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= cloud.google.com/go/alloydb v1.16.1 h1:pW4D0O2jAfAjoOEI1bgChPwMHWE8X8BjwSO0tfWkWvk= @@ -11,6 +30,12 @@ cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4= cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/bigquery v1.67.0 h1:GXleMyn/cu5+DPLy9Rz5f5IULWTLrepwbQnP/5qrVbY= cloud.google.com/go/bigquery v1.67.0/go.mod h1:HQeP1AHFuAz0Y55heDSb0cjZIhnEkuwFRBGo6EEKHug= cloud.google.com/go/cloudsqlconn v1.17.2 h1:SxSt6ujMxK1KyxKAI2Z5raT2n3geN7ipu6bA8f7iR7E= @@ -19,6 +44,9 @@ cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeO cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= cloud.google.com/go/datacatalog v1.26.0 h1:eFgygb3DTufTWWUB8ARk+dSuXz+aefNJXTlkWlQcWwE= cloud.google.com/go/datacatalog v1.26.0/go.mod h1:bLN2HLBAwB3kLTFT5ZKLHVPj/weNz6bR0c7nYp0LE14= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s= cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU= cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= @@ -29,10 +57,20 @@ cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFs cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.50.0 h1:3TbVkzTooBvnZsk7WaAQfOsNrdoM8QHusXA1cpk6QJs= cloud.google.com/go/storage v1.50.0/go.mod h1:l7XeiD//vx5lfqE3RavfmU9yvk5Pp0Zhcv482poyafY= cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= entgo.io/ent v0.14.3 h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ= entgo.io/ent v0.14.3/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= @@ -40,6 +78,7 @@ filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4 firebase.google.com/go/v4 v4.15.2 h1:KJtV4rAfO2CVCp40hBfVk+mqUqg7+jQKx7yOgFDnXBg= firebase.google.com/go/v4 v4.15.2/go.mod h1:qkD/HtSumrPMTLs0ahQrje5gTw2WKFKrzVFoqy4SbKA= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.52.0 h1:QFgWzcdmJlgEAwJz/zePYVJQxfoJGRtgIqZfIUFg5oQ= @@ -56,14 +95,20 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/anthropics/anthropic-sdk-go v1.19.0 h1:mO6E+ffSzLRvR/YUH9KJC0uGw0uV8GjISIuzem//3KE= github.com/anthropics/anthropic-sdk-go v1.19.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/blues/jsonata-go v1.5.4 h1:XCsXaVVMrt4lcpKeJw6mNJHqQpWU751cnHdCFUq3xd8= github.com/blues/jsonata-go v1.5.4/go.mod h1:uns2jymDrnI7y+UFYCqsRTEiAH22GyHnNXrkupAVFWI= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= @@ -71,10 +116,18 @@ github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx2 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k= github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -84,6 +137,9 @@ github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= @@ -93,10 +149,16 @@ github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJP github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -176,6 +238,8 @@ 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.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY= github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= @@ -184,23 +248,41 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2V github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/dotprompt/go v0.0.0-20251014011017-8d056e027254 h1:okN800+zMJOGHLJCgry+OGzhhtH6YrjQh1rluHmOacE= github.com/google/dotprompt/go v0.0.0-20251014011017-8d056e027254/go.mod h1:k8cjJAQWc//ac/bMnzItyOFbfT01tgRTZGgxELCuxEQ= github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg= @@ -209,14 +291,34 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -225,10 +327,37 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= @@ -265,8 +394,14 @@ github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Cc github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= @@ -274,6 +409,7 @@ github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kK github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -281,8 +417,11 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= @@ -293,16 +432,31 @@ github.com/mark3labs/mcp-go v0.29.0 h1:sH1NBcumKskhxqYzhXfGc201D7P76TVXiT0fGVhab github.com/mark3labs/mcp-go v0.29.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mbleigh/raymond v0.0.0-20250414171441-6b3a58ab9e0a h1:v2cBA3xWKv2cIOVhnzX/gNgkNXqiHfUgJtA3r61Hf7A= github.com/mbleigh/raymond v0.0.0-20250414171441-6b3a58ab9e0a/go.mod h1:Y6ghKH+ZijXn5d9E7qGGZBmjitx7iitZdQiIW97EpTU= github.com/microsoft/go-mssqldb v1.8.2 h1:236sewazvC8FvG6Dr3bszrVhMkAl4KYImryLkRMCd0I= github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= @@ -310,7 +464,9 @@ github.com/openai/openai-go v1.8.2 h1:UqSkJ1vCOPUpz9Ka5tS0324EJFEuOvMc+lA/EarJWP github.com/openai/openai-go v1.8.2/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pgvector/pgvector-go v0.3.0 h1:Ij+Yt78R//uYqs3Zk35evZFvr+G0blW0OUN+Q2D1RWc= github.com/pgvector/pgvector-go v0.3.0/go.mod h1:duFy+PXWfW7QQd5ibqutBO4GxLsUZ9RVXhFZGIBsWSA= github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= @@ -319,24 +475,45 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= 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= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= +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/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -347,6 +524,8 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -354,6 +533,7 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -402,6 +582,11 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= @@ -409,11 +594,21 @@ github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= go.mongodb.org/mongo-driver v1.8.3/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY= go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= @@ -436,112 +631,367 @@ go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFw go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= google.golang.org/api v0.236.0 h1:CAiEiDVtO4D/Qja2IA9VzlFrgPnK3XVMmRoJZlSWbc0= google.golang.org/api v0.236.0/go.mod h1:X1WF9CU2oTc+Jml1tiIxGmWFK/UZezdqEu09gcxZAj4= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= google.golang.org/genai v1.41.0 h1:ayXl75LjTmqTu0y94yr96d17gIb4zF8gWVzX2TgioEY= google.golang.org/genai v1.41.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78= google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk= google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ= @@ -549,10 +999,25 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -563,8 +1028,10 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= @@ -574,7 +1041,9 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8 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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= @@ -589,6 +1058,14 @@ gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo= mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/go/plugins/ollama/INTEGRATION_TESTS.md b/go/plugins/ollama/INTEGRATION_TESTS.md new file mode 100644 index 0000000000..9c82d20de3 --- /dev/null +++ b/go/plugins/ollama/INTEGRATION_TESTS.md @@ -0,0 +1,130 @@ +# Ollama Integration Tests + +This directory contains integration tests for the Ollama plugin's structured output feature. These tests require a running Ollama instance and are tagged with `integration` to separate them from unit tests. + +## Prerequisites + +1. **Ollama must be running**: Install and start Ollama from https://ollama.com +2. **Models must be available**: Pull at least one model that supports structured output + +### Recommended Models + +For best results, use models that support structured output: +- `llama3.2` (default for both chat and generate) +- `llama3.1` +- `qwen2.5` +- `mistral` + +Pull a model: +```bash +ollama pull llama3.2 +``` + +## Running Integration Tests + +### Run all integration tests: +```bash +go test -tags=integration -v ./go/plugins/ollama/... +``` + +### Run specific integration test: +```bash +go test -tags=integration -v -run TestIntegration_ChatModelWithSchema ./go/plugins/ollama/ +``` + +### Run with custom configuration: +```bash +# Use custom Ollama server address +OLLAMA_SERVER_ADDRESS=http://localhost:11434 go test -tags=integration -v ./go/plugins/ollama/ + +# Use specific models +OLLAMA_CHAT_MODEL=llama3.1 OLLAMA_GENERATE_MODEL=llama3.1 go test -tags=integration -v ./go/plugins/ollama/ +``` + +## Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `OLLAMA_SERVER_ADDRESS` | Ollama server URL | `http://localhost:11434` | +| `OLLAMA_CHAT_MODEL` | Model to use for chat tests | `llama3.2` | +| `OLLAMA_GENERATE_MODEL` | Model to use for generate tests | `llama3.2` | + +## Test Coverage + +The integration tests validate: + +### 8.1 Chat Model with Schema (TestIntegration_ChatModelWithSchema) +- Makes actual API call to Ollama chat endpoint with schema +- Verifies response conforms to schema +- **Validates Requirements**: 1.1, 1.2, 1.3, 4.1, 4.2 + +### 8.2 Generate Model with Schema (TestIntegration_GenerateModelWithSchema) +- Makes actual API call to Ollama generate endpoint with schema +- Verifies response conforms to schema +- **Validates Requirements**: 1.1, 1.2, 1.4, 4.1, 4.2 + +### 8.3 Schema-less JSON Mode (TestIntegration_SchemalessJSONMode) +- Makes API calls with format: "json" and no schema +- Verifies responses are valid JSON +- **Validates Requirements**: 2.1, 2.2 + +### 8.4 Streaming with Schemas (TestIntegration_StreamingWithSchema) +- Makes streaming API calls with schemas +- Verifies chunks are parsed correctly +- Verifies final merged output is complete +- **Validates Requirements**: 5.1, 5.2, 5.3, 5.4 + +### 8.5 Error Scenarios (TestIntegration_ErrorScenarios) +- Tests Ollama API error responses +- Tests invalid model names +- Verifies error messages are properly propagated +- **Validates Requirements**: 6.1, 6.4 + +## Troubleshooting + +### Tests are skipped +If tests are skipped with messages like "Ollama not available", ensure: +1. Ollama is running: `ollama serve` +2. The server address is correct +3. The firewall allows connections to Ollama + +### Model not found +If tests are skipped with "Model not available": +1. Pull the required model: `ollama pull llama3.2` +2. Or specify a different model using environment variables + +### Tests timeout +If tests timeout: +1. Ensure your machine has sufficient resources +2. Try using a smaller/faster model +3. Increase the timeout in the test code if needed + +### Connection refused +If you see "connection refused" errors: +1. Check Ollama is running: `curl http://localhost:11434/api/tags` +2. Verify the server address matches your Ollama configuration +3. Check for firewall or network issues + +## CI/CD Integration + +To run integration tests in CI/CD: + +```yaml +# Example GitHub Actions workflow +- name: Start Ollama + run: | + curl -fsSL https://ollama.com/install.sh | sh + ollama serve & + sleep 5 + ollama pull llama3.2 + +- name: Run Integration Tests + run: go test -tags=integration -v ./go/plugins/ollama/... +``` + +## Notes + +- Integration tests make real API calls and may take several seconds to complete +- Tests require network access to the Ollama server +- Some tests may produce different outputs depending on the model used +- The tests validate structure and format, not specific content diff --git a/go/plugins/ollama/README.md b/go/plugins/ollama/README.md new file mode 100644 index 0000000000..02ac5a87c1 --- /dev/null +++ b/go/plugins/ollama/README.md @@ -0,0 +1,228 @@ +# Ollama Plugin for Genkit Go + +The Ollama plugin enables Genkit Go applications to use locally-hosted [Ollama](https://ollama.com/) models for text generation, structured output, tool calling, and more. + +## Installation + +```bash +go get github.com/firebase/genkit/go/plugins/ollama +``` + +## Setup + +1. Install Ollama from [ollama.com](https://ollama.com/) +2. Pull a model: `ollama pull llama3.1` +3. Start the Ollama server (usually runs automatically on `http://localhost:11434`) + +## Basic Usage + +```go +import ( + "github.com/firebase/genkit/go/genkit" + "github.com/firebase/genkit/go/plugins/ollama" +) + +func main() { + ctx := context.Background() + + // Initialize the Ollama plugin + ollamaPlugin := &ollama.Ollama{ + ServerAddress: "http://localhost:11434", + Timeout: 60, + } + + g := genkit.Init(ctx, genkit.WithPlugins(ollamaPlugin)) + + // Define a model + model := ollamaPlugin.DefineModel(g, + ollama.ModelDefinition{ + Name: "llama3.1", + Type: "chat", + }, + nil) + + // Generate text + resp, _ := genkit.Generate(ctx, g, + ai.WithModel(model), + ai.WithMessages(ai.NewUserTextMessage("Hello!")), + ) + + fmt.Println(resp.Text()) +} +``` + +## Structured Output + +The Ollama plugin supports structured output through Ollama's native [structured output capability](https://docs.ollama.com/capabilities/structured-outputs). This feature allows you to constrain model responses to match specific JSON schemas, ensuring reliable extraction of structured data. + +### Schema-Based Structured Output + +Use a JSON schema to enforce a specific structure on the model's response: + +```go +import ( + "encoding/json" + "github.com/invopop/jsonschema" +) + +// Define your output structure +type Person struct { + Name string `json:"name" jsonschema:"required"` + Age int `json:"age" jsonschema:"required"` + Occupation string `json:"occupation" jsonschema:"required"` + Hobbies []string `json:"hobbies" jsonschema:"required"` +} + +// Generate a JSON schema from the struct +reflector := jsonschema.Reflector{ + AllowAdditionalProperties: false, + DoNotReference: true, +} +schema := reflector.Reflect(&Person{}) +schemaBytes, _ := json.Marshal(schema) + +var schemaMap map[string]any +json.Unmarshal(schemaBytes, &schemaMap) + +// Request structured output +resp, _ := genkit.Generate(ctx, g, + ai.WithModel(model), + ai.WithMessages(ai.NewUserTextMessage("Generate info about a software engineer")), + ai.WithOutputConfig(&ai.ModelOutputConfig{ + Format: "json", + Schema: schemaMap, + }), +) + +// Parse the structured response +var person Person +json.Unmarshal([]byte(resp.Text()), &person) +fmt.Printf("Name: %s, Age: %d\n", person.Name, person.Age) +``` + +**See full example:** [samples/ollama-structured](../../samples/ollama-structured) + +### Schema-less JSON Mode + +Request generic JSON output without enforcing a specific structure: + +```go +resp, _ := genkit.Generate(ctx, g, + ai.WithModel(model), + ai.WithMessages(ai.NewUserTextMessage("List 3 programming languages as JSON")), + ai.WithOutputConfig(&ai.ModelOutputConfig{ + Format: "json", + // No Schema specified - model returns valid JSON in any structure + }), +) + +// Parse as generic JSON +var result map[string]any +json.Unmarshal([]byte(resp.Text()), &result) +``` + +**See full example:** [samples/ollama-json-mode](../../samples/ollama-json-mode) + +### When to Use Each Mode + +**Schema-based structured output:** +- You need guaranteed structure for reliable parsing +- You want type-safe Go structs +- Building production systems with strict data requirements +- Extracting specific fields from model responses + +**Schema-less JSON mode:** +- You want valid JSON but don't need strict structure enforcement +- The output structure varies based on the prompt +- Prototyping and want flexibility +- Handling dynamic JSON structures in your application + +### Requirements and Limitations + +- **Model Support:** Structured output works with most recent Ollama models. Older models may not support this feature. +- **Ollama Version:** Requires Ollama version that supports the `format` parameter (most recent versions). +- **Schema Format:** Schemas must be valid JSON Schema objects. +- **No Client-Side Validation:** The plugin does not validate responses against the schema. Ollama is responsible for schema enforcement. +- **Error Handling:** If schema serialization fails, an error is returned before making the API request. + +## Tool Calling + +The Ollama plugin supports tool calling for chat models: + +```go +weatherTool := genkit.DefineTool(g, "weather", "Get weather for a location", + func(ctx *ai.ToolContext, input WeatherInput) (WeatherData, error) { + // Implementation + return getWeather(input.Location), nil + }, +) + +resp, _ := genkit.Generate(ctx, g, + ai.WithModel(model), + ai.WithMessages(ai.NewUserTextMessage("What's the weather in Tokyo?")), + ai.WithTools(weatherTool), +) +``` + +**See full example:** [samples/ollama-tools](../../samples/ollama-tools) + +## Vision Models + +Some Ollama models support image inputs: + +```go +imageData, _ := os.ReadFile("image.jpg") +imagePart := ai.NewMediaPart("image/jpeg", string(imageData)) + +resp, _ := genkit.Generate(ctx, g, + ai.WithModel(model), + ai.WithMessages(ai.NewUserMessage( + ai.NewTextPart("What's in this image?"), + imagePart, + )), +) +``` + +**See full example:** [samples/ollama-vision](../../samples/ollama-vision) + +## Configuration + +### Plugin Options + +```go +ollamaPlugin := &ollama.Ollama{ + ServerAddress: "http://localhost:11434", // Ollama server URL + Timeout: 60, // Request timeout in seconds +} +``` + +### Model Types + +- `"chat"`: For conversational models (supports tools and multi-turn conversations) +- `"generate"`: For completion models (single-turn text generation) + +### Supported Models + +The plugin works with any model available in Ollama. Popular choices include: + +- `llama3.1`, `llama3.2` - Meta's Llama models +- `mistral`, `mixtral` - Mistral AI models +- `phi3` - Microsoft's Phi models +- `qwen2` - Alibaba's Qwen models +- `gemma2` - Google's Gemma models + +Check [ollama.com/library](https://ollama.com/library) for the full list. + +## Additional Resources + +- [Ollama Documentation](https://docs.ollama.com/) +- [Ollama Structured Outputs](https://docs.ollama.com/capabilities/structured-outputs) +- [Genkit Go Documentation](https://genkit.dev/docs/overview/?lang=go) +- [Ollama Model Library](https://ollama.com/library) + +## Examples + +- [ollama-structured](../../samples/ollama-structured) - Schema-based structured output +- [ollama-json-mode](../../samples/ollama-json-mode) - Schema-less JSON mode +- [ollama-tools](../../samples/ollama-tools) - Tool calling with Ollama +- [ollama-vision](../../samples/ollama-vision) - Vision models with image inputs diff --git a/go/plugins/ollama/ollama.go b/go/plugins/ollama/ollama.go index 4eb4469673..e79b8e0597 100644 --- a/go/plugins/ollama/ollama.go +++ b/go/plugins/ollama/ollama.go @@ -226,9 +226,8 @@ func (o *Ollama) Init(ctx context.Context) []api.Action { return []api.Action{} } -// Generate makes a request to the Ollama API and processes the response. -func (g *generator) generate(ctx context.Context, input *ai.ModelRequest, cb func(context.Context, *ai.ModelResponseChunk) error) (*ai.ModelResponse, error) { - stream := cb != nil +// buildPayload creates the request payload for Ollama API. +func (g *generator) buildPayload(input *ai.ModelRequest, stream bool) (any, error) { var payload any isChatModel := g.model.Type == "chat" @@ -246,7 +245,7 @@ func (g *generator) generate(ctx context.Context, input *ai.ModelRequest, cb fun } if !isChatModel { - payload = ollamaModelRequest{ + payload = &ollamaModelRequest{ Model: g.model.Name, Prompt: concatMessages(input, []ai.Role{ai.RoleUser, ai.RoleModel, ai.RoleTool}), System: concatMessages(input, []ai.Role{ai.RoleSystem}), @@ -263,7 +262,7 @@ func (g *generator) generate(ctx context.Context, input *ai.ModelRequest, cb fun } messages = append(messages, message) } - chatReq := ollamaChatRequest{ + chatReq := &ollamaChatRequest{ Messages: messages, Model: g.model.Name, Stream: stream, @@ -279,6 +278,41 @@ func (g *generator) generate(ctx context.Context, input *ai.ModelRequest, cb fun payload = chatReq } + // Handle structured output via Ollama's format parameter. + if input.Output != nil { + var format string + if input.Output.Schema != nil && len(input.Output.Schema) > 0 { + schemaJSON, err := json.Marshal(input.Output.Schema) + if err != nil { + return nil, fmt.Errorf("failed to serialize output schema: %v", err) + } + format = string(schemaJSON) + } else if input.Output.Format == "json" { + format = "json" + } + + if format != "" { + switch p := payload.(type) { + case *ollamaChatRequest: + p.Format = format + case *ollamaModelRequest: + p.Format = format + } + } + } + + return payload, nil +} + +// Generate makes a request to the Ollama API and processes the response. +func (g *generator) generate(ctx context.Context, input *ai.ModelRequest, cb func(context.Context, *ai.ModelResponseChunk) error) (*ai.ModelResponse, error) { + stream := cb != nil + payload, err := g.buildPayload(input, stream) + if err != nil { + return nil, err + } + isChatModel := g.model.Type == "chat" + client := &http.Client{Timeout: time.Duration(g.timeout) * time.Second} payloadBytes, err := json.Marshal(payload) if err != nil { diff --git a/go/plugins/ollama/ollama_integration_test.go b/go/plugins/ollama/ollama_integration_test.go new file mode 100644 index 0000000000..12affed287 --- /dev/null +++ b/go/plugins/ollama/ollama_integration_test.go @@ -0,0 +1,645 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +//go:build integration +// +build integration + +package ollama + +import ( + "context" + "encoding/json" + "net/http" + "os" + "strings" + "testing" + "time" + + "github.com/firebase/genkit/go/ai" + "github.com/firebase/genkit/go/genkit" +) + +// Integration tests require a running Ollama instance +// Run with: go test -tags=integration ./go/plugins/ollama/... +// +// Prerequisites: +// 1. Ollama must be running (default: http://localhost:11434) +// 2. A model must be available (default: llama3.2 for chat, llama3.2 for generate) +// +// Set environment variables to customize: +// - OLLAMA_SERVER_ADDRESS: Ollama server address (default: http://localhost:11434) +// - OLLAMA_CHAT_MODEL: Chat model to use (default: llama3.2) +// - OLLAMA_GENERATE_MODEL: Generate model to use (default: llama3.2) + +const ( + defaultServerAddress = "http://localhost:11434" + defaultChatModel = "llama3.2" + defaultGenerateModel = "llama3.2" + testTimeout = 60 // seconds +) + +func getServerAddress() string { + if addr := os.Getenv("OLLAMA_SERVER_ADDRESS"); addr != "" { + return addr + } + return defaultServerAddress +} + +func getChatModel() string { + if model := os.Getenv("OLLAMA_CHAT_MODEL"); model != "" { + return model + } + return defaultChatModel +} + +func getGenerateModel() string { + if model := os.Getenv("OLLAMA_GENERATE_MODEL"); model != "" { + return model + } + return defaultGenerateModel +} + +// checkOllamaAvailable checks if Ollama is running and accessible +func checkOllamaAvailable(t *testing.T, serverAddress string) { + t.Helper() + + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Get(serverAddress + "/api/tags") + if err != nil { + t.Skipf("Ollama not available at %s: %v", serverAddress, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Skipf("Ollama returned non-200 status: %d", resp.StatusCode) + } +} + +// checkModelAvailable checks if a specific model is available +func checkModelAvailable(t *testing.T, serverAddress, modelName string) { + t.Helper() + + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Get(serverAddress + "/api/tags") + if err != nil { + t.Skipf("Cannot check model availability: %v", err) + } + defer resp.Body.Close() + + var result struct { + Models []struct { + Name string `json:"name"` + } `json:"models"` + } + + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + t.Skipf("Cannot parse model list: %v", err) + } + + for _, model := range result.Models { + if model.Name == modelName || model.Name == modelName+":latest" { + return + } + } + + t.Skipf("Model %s not available. Available models: %v", modelName, result.Models) +} + +// TestIntegration_ChatModelWithSchema tests chat model with schema returns structured JSON +// **Validates: Requirements 1.1, 1.2, 1.3, 4.1, 4.2** +func TestIntegration_ChatModelWithSchema(t *testing.T) { + serverAddress := getServerAddress() + modelName := getChatModel() + + checkOllamaAvailable(t, serverAddress) + checkModelAvailable(t, serverAddress, modelName) + + // Initialize Ollama plugin + ctx := context.Background() + g := genkit.Init(ctx) + + ollama := &Ollama{ + ServerAddress: serverAddress, + Timeout: testTimeout, + } + ollama.Init(ctx) + + // Define chat model + model := ollama.DefineModel(g, ModelDefinition{ + Name: modelName, + Type: "chat", + }, nil) + + // Define schema for person object + schema := map[string]any{ + "type": "object", + "properties": map[string]any{ + "name": map[string]any{ + "type": "string", + "description": "The person's full name", + }, + "age": map[string]any{ + "type": "number", + "description": "The person's age in years", + }, + "occupation": map[string]any{ + "type": "string", + "description": "The person's job or profession", + }, + }, + "required": []string{"name", "age"}, + } + + // Make request with schema + request := &ai.ModelRequest{ + Messages: []*ai.Message{ + { + Role: ai.RoleUser, + Content: []*ai.Part{ + ai.NewTextPart("Generate information about a software engineer named Alice who is 28 years old."), + }, + }, + }, + Output: &ai.ModelOutputConfig{ + Format: "json", + Schema: schema, + }, + } + + response, err := model.Generate(ctx, request, nil) + if err != nil { + t.Fatalf("Generate failed: %v", err) + } + + // Verify response structure + if response == nil { + t.Fatal("Response is nil") + } + + if response.Message == nil { + t.Fatal("Response message is nil") + } + + if len(response.Message.Content) == 0 { + t.Fatal("Response has no content") + } + + if !response.Message.Content[0].IsText() { + t.Fatal("Response content is not text") + } + + // Parse JSON response + var result map[string]any + content := response.Message.Content[0].Text + if err := json.Unmarshal([]byte(content), &result); err != nil { + t.Fatalf("Response is not valid JSON: %v\nContent: %s", err, content) + } + + // Verify schema conformance + if _, ok := result["name"]; !ok { + t.Errorf("Response missing required field 'name': %v", result) + } + + if _, ok := result["age"]; !ok { + t.Errorf("Response missing required field 'age': %v", result) + } + + // Verify types + if name, ok := result["name"].(string); !ok || name == "" { + t.Errorf("Field 'name' is not a non-empty string: %v", result["name"]) + } + + if age, ok := result["age"].(float64); !ok || age <= 0 { + t.Errorf("Field 'age' is not a positive number: %v", result["age"]) + } + + t.Logf("Successfully received structured response: %s", content) +} + +// TestIntegration_GenerateModelWithSchema tests generate model with schema returns structured JSON +// **Validates: Requirements 1.1, 1.2, 1.4, 4.1, 4.2** +func TestIntegration_GenerateModelWithSchema(t *testing.T) { + serverAddress := getServerAddress() + modelName := getGenerateModel() + + checkOllamaAvailable(t, serverAddress) + checkModelAvailable(t, serverAddress, modelName) + + // Initialize Ollama plugin + ctx := context.Background() + g := genkit.Init(ctx) + + ollama := &Ollama{ + ServerAddress: serverAddress, + Timeout: testTimeout, + } + ollama.Init(ctx) + + // Define generate model + model := ollama.DefineModel(g, ModelDefinition{ + Name: modelName, + Type: "generate", + }, nil) + + // Define schema for article object + schema := map[string]any{ + "type": "object", + "properties": map[string]any{ + "title": map[string]any{ + "type": "string", + "description": "The article title", + }, + "summary": map[string]any{ + "type": "string", + "description": "A brief summary of the article", + }, + "wordCount": map[string]any{ + "type": "number", + "description": "Estimated word count", + }, + }, + "required": []string{"title", "summary"}, + } + + // Make request with schema + request := &ai.ModelRequest{ + Messages: []*ai.Message{ + { + Role: ai.RoleUser, + Content: []*ai.Part{ + ai.NewTextPart("Generate an article about artificial intelligence with title and summary."), + }, + }, + }, + Output: &ai.ModelOutputConfig{ + Format: "json", + Schema: schema, + }, + } + + response, err := model.Generate(ctx, request, nil) + if err != nil { + t.Fatalf("Generate failed: %v", err) + } + + // Verify response structure + if response == nil { + t.Fatal("Response is nil") + } + + if response.Message == nil { + t.Fatal("Response message is nil") + } + + if len(response.Message.Content) == 0 { + t.Fatal("Response has no content") + } + + if !response.Message.Content[0].IsText() { + t.Fatal("Response content is not text") + } + + // Parse JSON response + var result map[string]any + content := response.Message.Content[0].Text + if err := json.Unmarshal([]byte(content), &result); err != nil { + t.Fatalf("Response is not valid JSON: %v\nContent: %s", err, content) + } + + // Verify schema conformance + if _, ok := result["title"]; !ok { + t.Errorf("Response missing required field 'title': %v", result) + } + + if _, ok := result["summary"]; !ok { + t.Errorf("Response missing required field 'summary': %v", result) + } + + // Verify types + if title, ok := result["title"].(string); !ok || title == "" { + t.Errorf("Field 'title' is not a non-empty string: %v", result["title"]) + } + + if summary, ok := result["summary"].(string); !ok || summary == "" { + t.Errorf("Field 'summary' is not a non-empty string: %v", result["summary"]) + } + + t.Logf("Successfully received structured response: %s", content) +} + +// TestIntegration_SchemalessJSONMode tests schema-less JSON mode +// **Validates: Requirements 2.1, 2.2** +func TestIntegration_SchemalessJSONMode(t *testing.T) { + serverAddress := getServerAddress() + modelName := getChatModel() + + checkOllamaAvailable(t, serverAddress) + checkModelAvailable(t, serverAddress, modelName) + + // Initialize Ollama plugin + ctx := context.Background() + g := genkit.Init(ctx) + + ollama := &Ollama{ + ServerAddress: serverAddress, + Timeout: testTimeout, + } + ollama.Init(ctx) + + // Define chat model + model := ollama.DefineModel(g, ModelDefinition{ + Name: modelName, + Type: "chat", + }, nil) + + // Make request with format: "json" but no schema + request := &ai.ModelRequest{ + Messages: []*ai.Message{ + { + Role: ai.RoleUser, + Content: []*ai.Part{ + ai.NewTextPart("Respond with a JSON object containing your favorite color and a number between 1 and 10."), + }, + }, + }, + Output: &ai.ModelOutputConfig{ + Format: "json", + }, + } + + response, err := model.Generate(ctx, request, nil) + if err != nil { + t.Fatalf("Generate failed: %v", err) + } + + // Verify response structure + if response == nil { + t.Fatal("Response is nil") + } + + if response.Message == nil { + t.Fatal("Response message is nil") + } + + if len(response.Message.Content) == 0 { + t.Fatal("Response has no content") + } + + if !response.Message.Content[0].IsText() { + t.Fatal("Response content is not text") + } + + // Verify response is valid JSON (no schema validation) + var result map[string]any + content := response.Message.Content[0].Text + if err := json.Unmarshal([]byte(content), &result); err != nil { + t.Fatalf("Response is not valid JSON: %v\nContent: %s", err, content) + } + + t.Logf("Successfully received JSON response: %s", content) +} + +// TestIntegration_StreamingWithSchema tests streaming with schemas +// **Validates: Requirements 5.1, 5.2, 5.3, 5.4** +func TestIntegration_StreamingWithSchema(t *testing.T) { + serverAddress := getServerAddress() + modelName := getChatModel() + + checkOllamaAvailable(t, serverAddress) + checkModelAvailable(t, serverAddress, modelName) + + // Initialize Ollama plugin + ctx := context.Background() + g := genkit.Init(ctx) + + ollama := &Ollama{ + ServerAddress: serverAddress, + Timeout: testTimeout, + } + ollama.Init(ctx) + + // Define chat model + model := ollama.DefineModel(g, ModelDefinition{ + Name: modelName, + Type: "chat", + }, nil) + + // Define schema + schema := map[string]any{ + "type": "object", + "properties": map[string]any{ + "city": map[string]any{ + "type": "string", + "description": "City name", + }, + "country": map[string]any{ + "type": "string", + "description": "Country name", + }, + "population": map[string]any{ + "type": "number", + "description": "Population count", + }, + }, + "required": []string{"city", "country"}, + } + + // Make streaming request with schema + request := &ai.ModelRequest{ + Messages: []*ai.Message{ + { + Role: ai.RoleUser, + Content: []*ai.Part{ + ai.NewTextPart("Generate information about Tokyo, Japan."), + }, + }, + }, + Output: &ai.ModelOutputConfig{ + Format: "json", + Schema: schema, + }, + } + + // Collect chunks + var chunks []*ai.ModelResponseChunk + chunkCount := 0 + + callback := func(ctx context.Context, chunk *ai.ModelResponseChunk) error { + chunkCount++ + chunks = append(chunks, chunk) + t.Logf("Received chunk %d with %d parts", chunkCount, len(chunk.Content)) + return nil + } + + response, err := model.Generate(ctx, request, callback) + if err != nil { + t.Fatalf("Streaming generate failed: %v", err) + } + + // Verify chunks were received + if chunkCount == 0 { + t.Fatal("No chunks received during streaming") + } + + t.Logf("Received %d chunks total", chunkCount) + + // Verify final response + if response == nil { + t.Fatal("Final response is nil") + } + + if response.Message == nil { + t.Fatal("Final response message is nil") + } + + if len(response.Message.Content) == 0 { + t.Fatal("Final response has no content") + } + + // Merge chunks manually to verify completeness + var mergedContent string + for _, chunk := range chunks { + for _, part := range chunk.Content { + if part.IsText() { + mergedContent += part.Text + } + } + } + + // Verify merged content is valid JSON + var mergedResult map[string]any + if err := json.Unmarshal([]byte(mergedContent), &mergedResult); err != nil { + t.Fatalf("Merged chunks are not valid JSON: %v\nContent: %s", err, mergedContent) + } + + // Verify final response content is valid JSON + var finalResult map[string]any + finalContent := "" + for _, part := range response.Message.Content { + if part.IsText() { + finalContent += part.Text + } + } + + if err := json.Unmarshal([]byte(finalContent), &finalResult); err != nil { + t.Fatalf("Final response is not valid JSON: %v\nContent: %s", err, finalContent) + } + + // Verify schema conformance + if _, ok := finalResult["city"]; !ok { + t.Errorf("Response missing required field 'city': %v", finalResult) + } + + if _, ok := finalResult["country"]; !ok { + t.Errorf("Response missing required field 'country': %v", finalResult) + } + + t.Logf("Successfully received streaming structured response: %s", finalContent) + t.Logf("Merged chunks match final response: %v", mergedContent == finalContent) +} + +// TestIntegration_ErrorScenarios tests error scenarios with live Ollama +// **Validates: Requirements 6.1, 6.4** +func TestIntegration_ErrorScenarios(t *testing.T) { + serverAddress := getServerAddress() + + checkOllamaAvailable(t, serverAddress) + + ctx := context.Background() + g := genkit.Init(ctx) + + ollama := &Ollama{ + ServerAddress: serverAddress, + Timeout: testTimeout, + } + ollama.Init(ctx) + + t.Run("Invalid model name", func(t *testing.T) { + // Define model with non-existent name + model := ollama.DefineModel(g, ModelDefinition{ + Name: "nonexistent-model-12345", + Type: "chat", + }, nil) + + request := &ai.ModelRequest{ + Messages: []*ai.Message{ + { + Role: ai.RoleUser, + Content: []*ai.Part{ + ai.NewTextPart("Hello"), + }, + }, + }, + } + + _, err := model.Generate(ctx, request, nil) + if err == nil { + t.Fatal("Expected error for invalid model name, got nil") + } + + // Verify error message contains useful information + errMsg := err.Error() + if !strings.Contains(errMsg, "server returned non-200 status") && !strings.Contains(errMsg, "failed") { + t.Errorf("Error message doesn't contain expected information: %s", errMsg) + } + + t.Logf("Received expected error: %v", err) + }) + + t.Run("Ollama API error response", func(t *testing.T) { + // Use a valid model but with a very short timeout to potentially trigger errors + modelName := getChatModel() + checkModelAvailable(t, serverAddress, modelName) + + shortTimeoutOllama := &Ollama{ + ServerAddress: serverAddress, + Timeout: 1, // 1 second timeout + } + shortTimeoutOllama.Init(ctx) + + model := shortTimeoutOllama.DefineModel(g, ModelDefinition{ + Name: modelName, + Type: "chat", + }, nil) + + // Make a request that might timeout + request := &ai.ModelRequest{ + Messages: []*ai.Message{ + { + Role: ai.RoleUser, + Content: []*ai.Part{ + ai.NewTextPart("Write a very long essay about the history of computing with at least 5000 words."), + }, + }, + }, + } + + _, err := model.Generate(ctx, request, nil) + // This might or might not error depending on model speed + // If it errors, verify the error is properly formatted + if err != nil { + errMsg := err.Error() + t.Logf("Received error (expected with short timeout): %v", err) + + // Verify error message is descriptive + if errMsg == "" { + t.Error("Error message is empty") + } + } else { + t.Log("Request completed within timeout (no error to test)") + } + }) +} diff --git a/go/plugins/ollama/ollama_test.go b/go/plugins/ollama/ollama_test.go index 1e41299e85..a731c857db 100644 --- a/go/plugins/ollama/ollama_test.go +++ b/go/plugins/ollama/ollama_test.go @@ -16,11 +16,19 @@ package ollama + import ( - "testing" + "encoding/json" + "fmt" + "reflect" + "strings" + "testing" "github.com/firebase/genkit/go/ai" "github.com/firebase/genkit/go/core/api" + "github.com/leanovate/gopter" + "github.com/leanovate/gopter/gen" + "github.com/leanovate/gopter/prop" ) var _ api.Plugin = (*Ollama)(nil) @@ -131,3 +139,2287 @@ func equalContent(a, b []*ai.Part) bool { } return true } + +func TestSchemaDetectionAndSerialization(t *testing.T) { + tests := []struct { + name string + output *ai.ModelOutputConfig + isChatModel bool + wantFormat string + wantFormatSet bool + }{ + { + name: "Schema with chat model", + output: &ai.ModelOutputConfig{ + Schema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "name": map[string]any{"type": "string"}, + "age": map[string]any{"type": "number"}, + }, + "required": []string{"name", "age"}, + }, + }, + isChatModel: true, + wantFormat: `{"properties":{"age":{"type":"number"},"name":{"type":"string"}},"required":["name","age"],"type":"object"}`, + wantFormatSet: true, + }, + { + name: "Schema with generate model", + output: &ai.ModelOutputConfig{ + Schema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "title": map[string]any{"type": "string"}, + }, + }, + }, + isChatModel: false, + wantFormat: `{"properties":{"title":{"type":"string"}},"type":"object"}`, + wantFormatSet: true, + }, + { + name: "Schema-less JSON mode with chat model", + output: &ai.ModelOutputConfig{ + Format: "json", + }, + isChatModel: true, + wantFormat: "json", + wantFormatSet: true, + }, + { + name: "Schema-less JSON mode with generate model", + output: &ai.ModelOutputConfig{ + Format: "json", + }, + isChatModel: false, + wantFormat: "json", + wantFormatSet: true, + }, + { + name: "No output config", + output: nil, + isChatModel: true, + wantFormat: "", + wantFormatSet: false, + }, + { + name: "Empty schema", + output: &ai.ModelOutputConfig{ + Schema: map[string]any{}, + }, + isChatModel: true, + wantFormat: "", + wantFormatSet: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + input := &ai.ModelRequest{ + Messages: []*ai.Message{ + { + Role: ai.RoleUser, + Content: []*ai.Part{ai.NewTextPart("Test message")}, + }, + }, + Output: tt.output, + } + + modelType := "generate" + if tt.isChatModel { + modelType = "chat" + } + gen := &generator{ + model: ModelDefinition{Name: "test-model", Type: modelType}, + serverAddress: "http://localhost:11434", + timeout: 30, + } + + payload, err := gen.buildPayload(input, false) + if err != nil { + t.Fatalf("buildPayload() error = %v", err) + } + + var gotFormat string + if tt.isChatModel { + gotFormat = payload.(*ollamaChatRequest).Format + } else { + gotFormat = payload.(*ollamaModelRequest).Format + } + + if tt.wantFormatSet { + if tt.wantFormat == "json" { + if gotFormat != "json" { + t.Errorf("Format = %q, want %q", gotFormat, "json") + } + } else { + var want, got map[string]any + if err := json.Unmarshal([]byte(tt.wantFormat), &want); err != nil { + t.Fatalf("invalid wantFormat in test case: %v", err) + } + if err := json.Unmarshal([]byte(gotFormat), &got); err != nil { + t.Fatalf("gotFormat is not valid JSON: %v", err) + } + if !reflect.DeepEqual(want, got) { + t.Errorf("Format mismatch.\ngot: %v\nwant: %v", got, want) + } + } + } else { + if gotFormat != "" { + t.Errorf("Format should be empty, got %q", gotFormat) + } + } + }) + } +} + +// TestTranslateChatResponseWithStructuredOutput tests that translateChatResponse +// correctly handles structured JSON responses in the Message.Content field. +// **Validates: Requirements 4.1, 4.2** +func TestTranslateChatResponseWithStructuredOutput(t *testing.T) { + tests := []struct { + name string + responseJSON string + wantContent string + wantErr bool + }{ + { + name: "Structured JSON response with schema", + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "{\"name\":\"John Doe\",\"age\":30}" + } + }`, + wantContent: `{"name":"John Doe","age":30}`, + wantErr: false, + }, + { + name: "Structured JSON response with nested objects", + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "{\"user\":{\"name\":\"Jane\",\"email\":\"jane@example.com\"},\"active\":true}" + } + }`, + wantContent: `{"user":{"name":"Jane","email":"jane@example.com"},"active":true}`, + wantErr: false, + }, + { + name: "Structured JSON response with array", + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "{\"items\":[\"apple\",\"banana\",\"cherry\"]}" + } + }`, + wantContent: `{"items":["apple","banana","cherry"]}`, + wantErr: false, + }, + { + name: "Empty structured response", + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "{}" + } + }`, + wantContent: `{}`, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := translateChatResponse([]byte(tt.responseJSON)) + if (err != nil) != tt.wantErr { + t.Errorf("translateChatResponse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if len(got.Message.Content) != 1 { + t.Errorf("Expected 1 content part, got %d", len(got.Message.Content)) + return + } + if !got.Message.Content[0].IsText() { + t.Errorf("Expected text content part") + return + } + if got.Message.Content[0].Text != tt.wantContent { + t.Errorf("translateChatResponse() content = %q, want %q", got.Message.Content[0].Text, tt.wantContent) + } + } + }) + } +} + +// TestTranslateModelResponseWithStructuredOutput tests that translateModelResponse +// correctly handles structured JSON responses in the Message.Content field. +// **Validates: Requirements 4.1, 4.2** +func TestTranslateModelResponseWithStructuredOutput(t *testing.T) { + tests := []struct { + name string + responseJSON string + wantContent string + wantErr bool + }{ + { + name: "Structured JSON response with schema", + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "response": "{\"title\":\"Test Article\",\"author\":\"John Smith\"}" + }`, + wantContent: `{"title":"Test Article","author":"John Smith"}`, + wantErr: false, + }, + { + name: "Structured JSON response with complex nested structure", + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "response": "{\"metadata\":{\"version\":\"1.0\",\"timestamp\":\"2024-01-01\"},\"data\":{\"count\":42}}" + }`, + wantContent: `{"metadata":{"version":"1.0","timestamp":"2024-01-01"},"data":{"count":42}}`, + wantErr: false, + }, + { + name: "Structured JSON response with boolean and null", + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "response": "{\"enabled\":true,\"disabled\":false,\"optional\":null}" + }`, + wantContent: `{"enabled":true,"disabled":false,"optional":null}`, + wantErr: false, + }, + { + name: "Empty structured response", + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "response": "{}" + }`, + wantContent: `{}`, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := translateModelResponse([]byte(tt.responseJSON)) + if (err != nil) != tt.wantErr { + t.Errorf("translateModelResponse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if len(got.Message.Content) != 1 { + t.Errorf("Expected 1 content part, got %d", len(got.Message.Content)) + return + } + if !got.Message.Content[0].IsText() { + t.Errorf("Expected text content part") + return + } + if got.Message.Content[0].Text != tt.wantContent { + t.Errorf("translateModelResponse() content = %q, want %q", got.Message.Content[0].Text, tt.wantContent) + } + } + }) + } +} + +// TestTranslateChatResponseBackwardCompatibility tests that translateChatResponse +// maintains backward compatibility with non-structured responses. +// **Validates: Requirements 4.3** +func TestTranslateChatResponseBackwardCompatibility(t *testing.T) { + tests := []struct { + name string + responseJSON string + wantContent string + wantErr bool + }{ + { + name: "Plain text response without structured output", + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "This is a plain text response from the model." + } + }`, + wantContent: "This is a plain text response from the model.", + wantErr: false, + }, + { + name: "Multi-line text response", + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "Line 1\nLine 2\nLine 3" + } + }`, + wantContent: "Line 1\nLine 2\nLine 3", + wantErr: false, + }, + { + name: "Response with special characters", + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "Response with \"quotes\" and 'apostrophes' and symbols: @#$%" + } + }`, + wantContent: "Response with \"quotes\" and 'apostrophes' and symbols: @#$%", + wantErr: false, + }, + { + name: "Empty content response", + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "" + } + }`, + wantContent: "", + wantErr: false, + }, + { + name: "Response with tool calls (no content)", + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "function": { + "name": "get_weather", + "arguments": {"location": "San Francisco"} + } + } + ] + } + }`, + wantContent: "", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := translateChatResponse([]byte(tt.responseJSON)) + if (err != nil) != tt.wantErr { + t.Errorf("translateChatResponse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + // For tool calls, we expect tool request parts instead of text + if tt.name == "Response with tool calls (no content)" { + if len(got.Message.Content) != 1 { + t.Errorf("Expected 1 content part for tool call, got %d", len(got.Message.Content)) + return + } + if got.Message.Content[0].IsText() { + t.Errorf("Expected tool request part, got text part") + } + return + } + + // For regular text responses + if tt.wantContent == "" { + // Empty content means no parts should be added + if len(got.Message.Content) != 0 { + t.Errorf("Expected 0 content parts for empty content, got %d", len(got.Message.Content)) + } + return + } + + if len(got.Message.Content) != 1 { + t.Errorf("Expected 1 content part, got %d", len(got.Message.Content)) + return + } + if !got.Message.Content[0].IsText() { + t.Errorf("Expected text content part") + return + } + if got.Message.Content[0].Text != tt.wantContent { + t.Errorf("translateChatResponse() content = %q, want %q", got.Message.Content[0].Text, tt.wantContent) + } + } + }) + } +} + +// TestTranslateModelResponseBackwardCompatibility tests that translateModelResponse +// maintains backward compatibility with non-structured responses. +// **Validates: Requirements 4.3** +func TestTranslateModelResponseBackwardCompatibility(t *testing.T) { + tests := []struct { + name string + responseJSON string + wantContent string + wantErr bool + }{ + { + name: "Plain text response without structured output", + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "response": "This is a plain text completion from the model." + }`, + wantContent: "This is a plain text completion from the model.", + wantErr: false, + }, + { + name: "Multi-paragraph response", + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "response": "Paragraph 1.\n\nParagraph 2.\n\nParagraph 3." + }`, + wantContent: "Paragraph 1.\n\nParagraph 2.\n\nParagraph 3.", + wantErr: false, + }, + { + name: "Response with code block", + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "response": "Here's some code:\n\nfunc main() {\n\tfmt.Println(\"Hello\")\n}" + }`, + wantContent: "Here's some code:\n\nfunc main() {\n\tfmt.Println(\"Hello\")\n}", + wantErr: false, + }, + { + name: "Response with unicode characters", + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "response": "Hello world with café" + }`, + wantContent: "Hello world with café", + wantErr: false, + }, + { + name: "Empty response", + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "response": "" + }`, + wantContent: "", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := translateModelResponse([]byte(tt.responseJSON)) + if (err != nil) != tt.wantErr { + t.Errorf("translateModelResponse() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if len(got.Message.Content) != 1 { + t.Errorf("Expected 1 content part, got %d", len(got.Message.Content)) + return + } + if !got.Message.Content[0].IsText() { + t.Errorf("Expected text content part") + return + } + if got.Message.Content[0].Text != tt.wantContent { + t.Errorf("translateModelResponse() content = %q, want %q", got.Message.Content[0].Text, tt.wantContent) + } + } + }) + } +} + +// TestTranslateChatChunkBackwardCompatibility tests that translateChatChunk +// maintains backward compatibility with non-structured streaming responses. +// **Validates: Requirements 4.3** +func TestTranslateChatChunkBackwardCompatibility(t *testing.T) { + tests := []struct { + name string + chunkJSON string + wantContent string + wantErr bool + }{ + { + name: "Plain text chunk", + chunkJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "Hello" + } + }`, + wantContent: "Hello", + wantErr: false, + }, + { + name: "Chunk with single word", + chunkJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "world" + } + }`, + wantContent: "world", + wantErr: false, + }, + { + name: "Empty chunk", + chunkJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "" + } + }`, + wantContent: "", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := translateChatChunk(tt.chunkJSON) + if (err != nil) != tt.wantErr { + t.Errorf("translateChatChunk() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + // Empty content means no parts should be added + if tt.wantContent == "" { + if len(got.Content) != 0 { + t.Errorf("Expected 0 content parts for empty content, got %d", len(got.Content)) + } + return + } + + if len(got.Content) != 1 { + t.Errorf("Expected 1 content part, got %d", len(got.Content)) + return + } + if !got.Content[0].IsText() { + t.Errorf("Expected text content part") + return + } + if got.Content[0].Text != tt.wantContent { + t.Errorf("translateChatChunk() content = %q, want %q", got.Content[0].Text, tt.wantContent) + } + } + }) + } +} + +// TestStreamingFormatParameterInclusion tests that the format parameter is included +// in streaming requests when Output.Schema is provided. +// **Validates: Requirements 5.1** +func TestStreamingFormatParameterInclusion(t *testing.T) { + tests := []struct { + name string + output *ai.ModelOutputConfig + isChatModel bool + isStreaming bool + wantFormat string + wantFormatSet bool + }{ + { + name: "Streaming chat request with schema", + output: &ai.ModelOutputConfig{ + Schema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "name": map[string]any{"type": "string"}, + }, + }, + }, + isChatModel: true, + isStreaming: true, + wantFormat: `{"properties":{"name":{"type":"string"}},"type":"object"}`, + wantFormatSet: true, + }, + { + name: "Streaming generate request with schema", + output: &ai.ModelOutputConfig{ + Schema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "title": map[string]any{"type": "string"}, + }, + }, + }, + isChatModel: false, + isStreaming: true, + wantFormat: `{"properties":{"title":{"type":"string"}},"type":"object"}`, + wantFormatSet: true, + }, + { + name: "Streaming chat request with schema-less JSON mode", + output: &ai.ModelOutputConfig{ + Format: "json", + }, + isChatModel: true, + isStreaming: true, + wantFormat: "json", + wantFormatSet: true, + }, + { + name: "Streaming generate request with schema-less JSON mode", + output: &ai.ModelOutputConfig{ + Format: "json", + }, + isChatModel: false, + isStreaming: true, + wantFormat: "json", + wantFormatSet: true, + }, + { + name: "Non-streaming chat request with schema (comparison)", + output: &ai.ModelOutputConfig{ + Schema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "name": map[string]any{"type": "string"}, + }, + }, + }, + isChatModel: true, + isStreaming: false, + wantFormat: `{"properties":{"name":{"type":"string"}},"type":"object"}`, + wantFormatSet: true, + }, + { + name: "Non-streaming generate request with schema (comparison)", + output: &ai.ModelOutputConfig{ + Schema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "title": map[string]any{"type": "string"}, + }, + }, + }, + isChatModel: false, + isStreaming: false, + wantFormat: `{"properties":{"title":{"type":"string"}},"type":"object"}`, + wantFormatSet: true, + }, + { + name: "Streaming request without output config", + output: nil, + isChatModel: true, + isStreaming: true, + wantFormat: "", + wantFormatSet: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + input := &ai.ModelRequest{ + Messages: []*ai.Message{ + { + Role: ai.RoleUser, + Content: []*ai.Part{ai.NewTextPart("Test message")}, + }, + }, + Output: tt.output, + } + + modelType := "generate" + if tt.isChatModel { + modelType = "chat" + } + gen := &generator{ + model: ModelDefinition{Name: "test-model", Type: modelType}, + serverAddress: "http://localhost:11434", + timeout: 30, + } + + payload, err := gen.buildPayload(input, tt.isStreaming) + if err != nil { + t.Fatalf("buildPayload() error = %v", err) + } + + var gotFormat string + var gotStream bool + if tt.isChatModel { + gotFormat = payload.(*ollamaChatRequest).Format + gotStream = payload.(*ollamaChatRequest).Stream + } else { + gotFormat = payload.(*ollamaModelRequest).Format + gotStream = payload.(*ollamaModelRequest).Stream + } + + if gotStream != tt.isStreaming { + t.Errorf("Stream flag = %v, want %v", gotStream, tt.isStreaming) + } + + if tt.wantFormatSet { + if gotFormat != tt.wantFormat { + t.Errorf("Format = %q, want %q", gotFormat, tt.wantFormat) + } + } else { + if gotFormat != "" { + t.Errorf("Format should be empty, got %q", gotFormat) + } + } + }) + } +} + +// TestTranslateChatChunkWithStructuredOutput tests that translateChatChunk +// correctly handles structured JSON output chunks during streaming. +// **Validates: Requirements 5.2** +func TestTranslateChatChunkWithStructuredOutput(t *testing.T) { + tests := []struct { + name string + chunkJSON string + wantContent string + wantErr bool + }{ + { + name: "Structured JSON chunk - opening brace", + chunkJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "{" + } + }`, + wantContent: "{", + wantErr: false, + }, + { + name: "Structured JSON chunk - property name", + chunkJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "\"name\"" + } + }`, + wantContent: `"name"`, + wantErr: false, + }, + { + name: "Structured JSON chunk - colon and value start", + chunkJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": ":\"John" + } + }`, + wantContent: `:"John`, + wantErr: false, + }, + { + name: "Structured JSON chunk - value continuation", + chunkJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": " Doe\"" + } + }`, + wantContent: ` Doe"`, + wantErr: false, + }, + { + name: "Structured JSON chunk - comma and next property", + chunkJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": ",\"age\":" + } + }`, + wantContent: `,"age":`, + wantErr: false, + }, + { + name: "Structured JSON chunk - number value", + chunkJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "30" + } + }`, + wantContent: "30", + wantErr: false, + }, + { + name: "Structured JSON chunk - closing brace", + chunkJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "}" + } + }`, + wantContent: "}", + wantErr: false, + }, + { + name: "Structured JSON chunk - complete small object", + chunkJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "{\"status\":\"ok\"}" + } + }`, + wantContent: `{"status":"ok"}`, + wantErr: false, + }, + { + name: "Structured JSON chunk - array element", + chunkJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "[\"item1\"," + } + }`, + wantContent: `["item1",`, + wantErr: false, + }, + { + name: "Structured JSON chunk - boolean value", + chunkJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "true" + } + }`, + wantContent: "true", + wantErr: false, + }, + { + name: "Structured JSON chunk - null value", + chunkJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "null" + } + }`, + wantContent: "null", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := translateChatChunk(tt.chunkJSON) + if (err != nil) != tt.wantErr { + t.Errorf("translateChatChunk() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if len(got.Content) != 1 { + t.Errorf("Expected 1 content part, got %d", len(got.Content)) + return + } + if !got.Content[0].IsText() { + t.Errorf("Expected text content part") + return + } + if got.Content[0].Text != tt.wantContent { + t.Errorf("translateChatChunk() content = %q, want %q", got.Content[0].Text, tt.wantContent) + } + } + }) + } +} + +// TestTranslateGenerateChunkWithStructuredOutput tests that translateGenerateChunk +// correctly handles structured JSON output chunks during streaming. +// **Validates: Requirements 5.2** +func TestTranslateGenerateChunkWithStructuredOutput(t *testing.T) { + tests := []struct { + name string + chunkJSON string + wantContent string + wantErr bool + }{ + { + name: "Structured JSON chunk - opening brace", + chunkJSON: `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "response": "{"}`, + wantContent: "{", + wantErr: false, + }, + { + name: "Structured JSON chunk - property name", + chunkJSON: `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "response": "\"title\""}`, + wantContent: `"title"`, + wantErr: false, + }, + { + name: "Structured JSON chunk - colon and value", + chunkJSON: `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "response": ":\"Test"}`, + wantContent: `:"Test`, + wantErr: false, + }, + { + name: "Structured JSON chunk - value continuation", + chunkJSON: `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "response": " Article\""}`, + wantContent: ` Article"`, + wantErr: false, + }, + { + name: "Structured JSON chunk - closing brace", + chunkJSON: `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "response": "}"}`, + wantContent: "}", + wantErr: false, + }, + { + name: "Structured JSON chunk - complete object", + chunkJSON: `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "response": "{\"count\":42}"}`, + wantContent: `{"count":42}`, + wantErr: false, + }, + { + name: "Structured JSON chunk - array start", + chunkJSON: `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "response": "["}`, + wantContent: "[", + wantErr: false, + }, + { + name: "Structured JSON chunk - nested object", + chunkJSON: `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "response": "{\"nested\":{"}`, + wantContent: `{"nested":{`, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := translateGenerateChunk(tt.chunkJSON) + if (err != nil) != tt.wantErr { + t.Errorf("translateGenerateChunk() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if len(got.Content) != 1 { + t.Errorf("Expected 1 content part, got %d", len(got.Content)) + return + } + if !got.Content[0].IsText() { + t.Errorf("Expected text content part") + return + } + if got.Content[0].Text != tt.wantContent { + t.Errorf("translateGenerateChunk() content = %q, want %q", got.Content[0].Text, tt.wantContent) + } + } + }) + } +} + +// TestStreamingMergedResponseCompleteness tests that the final merged response +// from streaming contains the complete structured output. +// **Validates: Requirements 5.3** +func TestStreamingMergedResponseCompleteness(t *testing.T) { + tests := []struct { + name string + chunks []string + isChatModel bool + wantComplete string + wantErr bool + }{ + { + name: "Chat streaming - complete structured JSON", + isChatModel: true, + chunks: []string{ + `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "message": {"role": "assistant", "content": "{"}}`, + `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "message": {"role": "assistant", "content": "\"name\""}}`, + `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "message": {"role": "assistant", "content": ":"}}`, + `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "message": {"role": "assistant", "content": "\"John Doe\""}}`, + `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "message": {"role": "assistant", "content": ","}}`, + `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "message": {"role": "assistant", "content": "\"age\""}}`, + `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "message": {"role": "assistant", "content": ":"}}`, + `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "message": {"role": "assistant", "content": "30"}}`, + `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "message": {"role": "assistant", "content": "}"}}`, + }, + wantComplete: `{"name":"John Doe","age":30}`, + wantErr: false, + }, + { + name: "Generate streaming - complete structured JSON", + isChatModel: false, + chunks: []string{ + `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "response": "{"}`, + `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "response": "\"title\""}`, + `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "response": ":"}`, + `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "response": "\"Test Article\""}`, + `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "response": "}"}`, + }, + wantComplete: `{"title":"Test Article"}`, + wantErr: false, + }, + { + name: "Chat streaming - nested structured JSON", + isChatModel: true, + chunks: []string{ + `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "message": {"role": "assistant", "content": "{"}}`, + `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "message": {"role": "assistant", "content": "\"user\":{"}}`, + `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "message": {"role": "assistant", "content": "\"name\":\"Jane\""}}`, + `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "message": {"role": "assistant", "content": "},\"active\":true}"}}`, + }, + wantComplete: `{"user":{"name":"Jane"},"active":true}`, + wantErr: false, + }, + { + name: "Generate streaming - array structured JSON", + isChatModel: false, + chunks: []string{ + `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "response": "{\"items\":["}`, + `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "response": "\"apple\","}`, + `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "response": "\"banana\""}`, + `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "response": "]}"}`, + }, + wantComplete: `{"items":["apple","banana"]}`, + wantErr: false, + }, + { + name: "Chat streaming - single chunk complete JSON", + isChatModel: true, + chunks: []string{ + `{"model": "llama2", "created_at": "2024-01-01T00:00:00Z", "message": {"role": "assistant", "content": "{\"status\":\"ok\"}"}}`, + }, + wantComplete: `{"status":"ok"}`, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var chunks []*ai.ModelResponseChunk + var err error + + // Parse all chunks + for _, chunkJSON := range tt.chunks { + var chunk *ai.ModelResponseChunk + if tt.isChatModel { + chunk, err = translateChatChunk(chunkJSON) + } else { + chunk, err = translateGenerateChunk(chunkJSON) + } + if err != nil { + if !tt.wantErr { + t.Fatalf("failed to translate chunk: %v", err) + } + return + } + chunks = append(chunks, chunk) + } + + // Merge chunks (simulating what generate() does) + var mergedContent string + for _, chunk := range chunks { + for _, part := range chunk.Content { + if part.IsText() { + mergedContent += part.Text + } + } + } + + // Verify the merged content is complete and valid JSON + if mergedContent != tt.wantComplete { + t.Errorf("Merged content = %q, want %q", mergedContent, tt.wantComplete) + } + + // Verify it's valid JSON by unmarshaling + var jsonData map[string]any + if err := json.Unmarshal([]byte(mergedContent), &jsonData); err != nil { + t.Errorf("Merged content is not valid JSON: %v", err) + } + }) + } +} + +// TestSchemaSerializationErrorHandling tests that schema serialization errors +// are caught before HTTP requests and return descriptive error messages. +// **Validates: Requirements 3.1, 3.2, 6.2, 6.4** +func TestSchemaSerializationErrorHandling(t *testing.T) { + tests := []struct { + name string + schema map[string]any + wantErrContains []string + shouldFailMarshal bool + }{ + { + name: "Schema with unmarshalable channel type", + schema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "channel": make(chan int), // channels cannot be marshaled to JSON + }, + }, + wantErrContains: []string{"failed to serialize output schema"}, + shouldFailMarshal: true, + }, + { + name: "Schema with unmarshalable function type", + schema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "func": func() {}, // functions cannot be marshaled to JSON + }, + }, + wantErrContains: []string{"failed to serialize output schema"}, + shouldFailMarshal: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test that json.Marshal fails for these schemas + _, err := json.Marshal(tt.schema) + if tt.shouldFailMarshal && err == nil { + t.Errorf("Expected json.Marshal to fail for schema, but it succeeded") + } + + // Verify the error message would contain expected strings + if err != nil { + // This simulates what the generator.generate() method does + expectedErr := fmt.Errorf("failed to serialize output schema: %v", err) + errMsg := expectedErr.Error() + + for _, wantStr := range tt.wantErrContains { + if !contains(errMsg, wantStr) { + t.Errorf("Error message %q does not contain %q", errMsg, wantStr) + } + } + } + }) + } +} + +// Helper function to check if a string contains a substring +func contains(s, substr string) bool { + return strings.Contains(s, substr) +} + +// TestAPIErrorMessageFormat tests that API communication errors include +// status codes and response bodies in error messages. +// **Validates: Requirements 6.1, 6.4** +func TestAPIErrorMessageFormat(t *testing.T) { + tests := []struct { + name string + statusCode int + responseBody string + wantErrContains []string + }{ + { + name: "404 Not Found error", + statusCode: 404, + responseBody: "model not found", + wantErrContains: []string{ + "server returned non-200 status", + "404", + "model not found", + }, + }, + { + name: "500 Internal Server Error", + statusCode: 500, + responseBody: "internal server error: database connection failed", + wantErrContains: []string{ + "server returned non-200 status", + "500", + "internal server error", + }, + }, + { + name: "400 Bad Request with JSON error", + statusCode: 400, + responseBody: `{"error": "invalid schema format"}`, + wantErrContains: []string{ + "server returned non-200 status", + "400", + "invalid schema format", + }, + }, + { + name: "503 Service Unavailable", + statusCode: 503, + responseBody: "service temporarily unavailable", + wantErrContains: []string{ + "server returned non-200 status", + "503", + "service temporarily unavailable", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Simulate the error format from generator.generate() + err := fmt.Errorf("server returned non-200 status: %d, body: %s", tt.statusCode, tt.responseBody) + errMsg := err.Error() + + // Verify all expected strings are in the error message + for _, wantStr := range tt.wantErrContains { + if !contains(errMsg, wantStr) { + t.Errorf("Error message %q does not contain %q", errMsg, wantStr) + } + } + + // Verify the error message format matches what's in the implementation + expectedFormat := fmt.Sprintf("server returned non-200 status: %d, body: %s", tt.statusCode, tt.responseBody) + if errMsg != expectedFormat { + t.Errorf("Error message format = %q, want %q", errMsg, expectedFormat) + } + }) + } +} + +// TestNonConformingResponseHandling tests that responses that don't conform +// to the requested schema are still parsed and returned without client-side validation. +// **Validates: Requirements 6.3** +func TestNonConformingResponseHandling(t *testing.T) { + tests := []struct { + name string + requestedSchema map[string]any + responseJSON string + isChatModel bool + wantContent string + wantErr bool + wantValidationErr bool // Should be false - no client-side validation + }{ + { + name: "Chat response missing required field", + requestedSchema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "name": map[string]any{"type": "string"}, + "age": map[string]any{"type": "number"}, + }, + "required": []string{"name", "age"}, + }, + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "{\"name\":\"John\"}" + } + }`, + isChatModel: true, + wantContent: `{"name":"John"}`, + wantErr: false, + wantValidationErr: false, + }, + { + name: "Chat response with extra field not in schema", + requestedSchema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "name": map[string]any{"type": "string"}, + }, + }, + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "{\"name\":\"Jane\",\"age\":25,\"email\":\"jane@example.com\"}" + } + }`, + isChatModel: true, + wantContent: `{"name":"Jane","age":25,"email":"jane@example.com"}`, + wantErr: false, + wantValidationErr: false, + }, + { + name: "Chat response with wrong type", + requestedSchema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "age": map[string]any{"type": "number"}, + }, + }, + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "{\"age\":\"thirty\"}" + } + }`, + isChatModel: true, + wantContent: `{"age":"thirty"}`, + wantErr: false, + wantValidationErr: false, + }, + { + name: "Generate response missing required field", + requestedSchema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "title": map[string]any{"type": "string"}, + "author": map[string]any{"type": "string"}, + }, + "required": []string{"title", "author"}, + }, + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "response": "{\"title\":\"Test Article\"}" + }`, + isChatModel: false, + wantContent: `{"title":"Test Article"}`, + wantErr: false, + wantValidationErr: false, + }, + { + name: "Generate response with wrong structure", + requestedSchema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "items": map[string]any{ + "type": "array", + "items": map[string]any{ + "type": "string", + }, + }, + }, + }, + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "response": "{\"items\":\"not an array\"}" + }`, + isChatModel: false, + wantContent: `{"items":"not an array"}`, + wantErr: false, + wantValidationErr: false, + }, + { + name: "Chat response with completely different structure", + requestedSchema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "user": map[string]any{ + "type": "object", + "properties": map[string]any{ + "name": map[string]any{"type": "string"}, + }, + }, + }, + }, + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": "{\"status\":\"ok\",\"code\":200}" + } + }`, + isChatModel: true, + wantContent: `{"status":"ok","code":200}`, + wantErr: false, + wantValidationErr: false, + }, + { + name: "Generate response as array instead of object", + requestedSchema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "data": map[string]any{"type": "string"}, + }, + }, + responseJSON: `{ + "model": "llama2", + "created_at": "2024-01-01T00:00:00Z", + "response": "[\"item1\",\"item2\",\"item3\"]" + }`, + isChatModel: false, + wantContent: `["item1","item2","item3"]`, + wantErr: false, + wantValidationErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Parse the response using the appropriate translation function + var response *ai.ModelResponse + var err error + + if tt.isChatModel { + response, err = translateChatResponse([]byte(tt.responseJSON)) + } else { + response, err = translateModelResponse([]byte(tt.responseJSON)) + } + + // Verify no error occurred during parsing + if (err != nil) != tt.wantErr { + t.Errorf("Translation error = %v, wantErr %v", err, tt.wantErr) + return + } + + // Verify no validation error occurred (client-side validation should not happen) + if err != nil && tt.wantValidationErr { + t.Errorf("Expected no validation error, but got: %v", err) + return + } + + if !tt.wantErr { + // Verify the response content matches what Ollama returned + if len(response.Message.Content) != 1 { + t.Errorf("Expected 1 content part, got %d", len(response.Message.Content)) + return + } + if !response.Message.Content[0].IsText() { + t.Errorf("Expected text content part") + return + } + if response.Message.Content[0].Text != tt.wantContent { + t.Errorf("Response content = %q, want %q", response.Message.Content[0].Text, tt.wantContent) + } + + // Verify the content is valid JSON (even if it doesn't match schema) + var jsonData any + if err := json.Unmarshal([]byte(response.Message.Content[0].Text), &jsonData); err != nil { + t.Errorf("Response content is not valid JSON: %v", err) + } + } + }) + } +} + +// ============================================================================ +// Property-Based Tests for Ollama Structured Output +// ============================================================================ +// These tests use gopter for property-based testing with minimum 100 iterations +// Feature: ollama-structured-output + +// Helper generators for property-based tests + +// genValidSchema generates random valid JSON schemas +func genValidSchema() gopter.Gen { + return gen.OneGenOf( + // Simple object schema + gen.Const(map[string]any{ + "type": "object", + "properties": map[string]any{ + "name": map[string]any{"type": "string"}, + "age": map[string]any{"type": "number"}, + }, + "required": []string{"name"}, + }), + // Schema with nested object + gen.Const(map[string]any{ + "type": "object", + "properties": map[string]any{ + "user": map[string]any{ + "type": "object", + "properties": map[string]any{ + "name": map[string]any{"type": "string"}, + "email": map[string]any{"type": "string"}, + }, + }, + }, + }), + // Schema with array + gen.Const(map[string]any{ + "type": "object", + "properties": map[string]any{ + "items": map[string]any{ + "type": "array", + "items": map[string]any{ + "type": "string", + }, + }, + }, + }), + // Schema with multiple types + gen.Const(map[string]any{ + "type": "object", + "properties": map[string]any{ + "title": map[string]any{"type": "string"}, + "count": map[string]any{"type": "number"}, + "active": map[string]any{"type": "boolean"}, + "tags": map[string]any{"type": "array", "items": map[string]any{"type": "string"}}, + }, + "required": []string{"title"}, + }), + // Simple schema with single property + gen.Const(map[string]any{ + "type": "object", + "properties": map[string]any{ + "status": map[string]any{"type": "string"}, + }, + }), + ) +} + +// genModelType generates random model types (chat or generate) +func genModelType() gopter.Gen { + return gen.Bool().Map(func(isChatModel bool) string { + if isChatModel { + return "chat" + } + return "generate" + }) +} + +// genEmptyOrNilSchema generates nil or empty schemas +func genEmptyOrNilSchema() gopter.Gen { + return gen.OneGenOf( + gen.Const(map[string]any(nil)), + gen.Const(map[string]any{}), + ) +} + +// Feature: ollama-structured-output, Property 1: Schema Inclusion in Format Parameter +// **Validates: Requirements 1.1, 1.3, 1.4** +func TestProperty1_SchemaInclusionInFormatParameter(t *testing.T) { + parameters := gopter.DefaultTestParameters() + parameters.MinSuccessfulTests = 100 + properties := gopter.NewProperties(parameters) + + properties.Property("schema included in format parameter for all valid schemas", prop.ForAll( + func(schema map[string]any, modelType string) bool { + isChatModel := modelType == "chat" + + // Create request with schema + input := &ai.ModelRequest{ + Messages: []*ai.Message{ + { + Role: ai.RoleUser, + Content: []*ai.Part{ai.NewTextPart("Test message")}, + }, + }, + Output: &ai.ModelOutputConfig{ + Schema: schema, + }, + } + + // Create payload + var payload any + if isChatModel { + payload = ollamaChatRequest{ + Messages: []*ollamaMessage{{Role: "user", Content: "Test"}}, + Model: "test-model", + Stream: false, + } + } else { + payload = ollamaModelRequest{ + Model: "test-model", + Prompt: "Test", + Stream: false, + } + } + + // Apply schema serialization logic + if input.Output != nil && input.Output.Schema != nil && len(input.Output.Schema) > 0 { + schemaJSON, err := json.Marshal(input.Output.Schema) + if err != nil { + return false + } + + if isChatModel { + chatReq := payload.(ollamaChatRequest) + chatReq.Format = string(schemaJSON) + payload = chatReq + } else { + modelReq := payload.(ollamaModelRequest) + modelReq.Format = string(schemaJSON) + payload = modelReq + } + } + + // Verify format field is set + var gotFormat string + if isChatModel { + gotFormat = payload.(ollamaChatRequest).Format + } else { + gotFormat = payload.(ollamaModelRequest).Format + } + + // Format should not be empty and should be valid JSON + if gotFormat == "" { + return false + } + + // Verify it's valid JSON + var unmarshaled map[string]any + if err := json.Unmarshal([]byte(gotFormat), &unmarshaled); err != nil { + return false + } + + return true + }, + genValidSchema(), + genModelType(), + )) + + properties.TestingRun(t) +} + +// Feature: ollama-structured-output, Property 2: Schema Serialization Round-Trip +// **Validates: Requirements 1.2** +func TestProperty2_SchemaSerializationRoundTrip(t *testing.T) { + parameters := gopter.DefaultTestParameters() + parameters.MinSuccessfulTests = 100 + properties := gopter.NewProperties(parameters) + + properties.Property("schema serialization round-trip preserves structure", prop.ForAll( + func(schema map[string]any) bool { + // Serialize schema + schemaJSON, err := json.Marshal(schema) + if err != nil { + return false + } + + // Deserialize back + var roundTripped map[string]any + if err := json.Unmarshal(schemaJSON, &roundTripped); err != nil { + return false + } + + // Re-serialize to compare + roundTrippedJSON, err := json.Marshal(roundTripped) + if err != nil { + return false + } + + // JSON representations should be equivalent + return string(schemaJSON) == string(roundTrippedJSON) + }, + genValidSchema(), + )) + + properties.TestingRun(t) +} + +// Feature: ollama-structured-output, Property 3: Format Parameter Omission for Empty Schemas +// **Validates: Requirements 1.5** +func TestProperty3_FormatParameterOmissionForEmptySchemas(t *testing.T) { + parameters := gopter.DefaultTestParameters() + parameters.MinSuccessfulTests = 100 + properties := gopter.NewProperties(parameters) + + properties.Property("format parameter omitted for nil or empty schemas", prop.ForAll( + func(schema map[string]any, modelType string) bool { + isChatModel := modelType == "chat" + + // Create request with nil/empty schema + var input *ai.ModelRequest + if schema == nil { + input = &ai.ModelRequest{ + Messages: []*ai.Message{ + { + Role: ai.RoleUser, + Content: []*ai.Part{ai.NewTextPart("Test")}, + }, + }, + Output: nil, + } + } else { + input = &ai.ModelRequest{ + Messages: []*ai.Message{ + { + Role: ai.RoleUser, + Content: []*ai.Part{ai.NewTextPart("Test")}, + }, + }, + Output: &ai.ModelOutputConfig{ + Schema: schema, + }, + } + } + + // Create payload + var payload any + if isChatModel { + payload = ollamaChatRequest{ + Messages: []*ollamaMessage{{Role: "user", Content: "Test"}}, + Model: "test-model", + Stream: false, + } + } else { + payload = ollamaModelRequest{ + Model: "test-model", + Prompt: "Test", + Stream: false, + } + } + + // Apply schema logic + if input.Output != nil && input.Output.Schema != nil && len(input.Output.Schema) > 0 { + schemaJSON, err := json.Marshal(input.Output.Schema) + if err != nil { + return false + } + + if isChatModel { + chatReq := payload.(ollamaChatRequest) + chatReq.Format = string(schemaJSON) + payload = chatReq + } else { + modelReq := payload.(ollamaModelRequest) + modelReq.Format = string(schemaJSON) + payload = modelReq + } + } + + // Verify format field is empty + var gotFormat string + if isChatModel { + gotFormat = payload.(ollamaChatRequest).Format + } else { + gotFormat = payload.(ollamaModelRequest).Format + } + + return gotFormat == "" + }, + genEmptyOrNilSchema(), + genModelType(), + )) + + properties.TestingRun(t) +} + +// Feature: ollama-structured-output, Property 4: Schema-less JSON Mode +// **Validates: Requirements 2.1** +func TestProperty4_SchemalessJSONMode(t *testing.T) { + parameters := gopter.DefaultTestParameters() + parameters.MinSuccessfulTests = 100 + properties := gopter.NewProperties(parameters) + + properties.Property("format parameter set to 'json' for schema-less JSON mode", prop.ForAll( + func(modelType string) bool { + isChatModel := modelType == "chat" + + // Create request with format: "json" and no schema + input := &ai.ModelRequest{ + Messages: []*ai.Message{ + { + Role: ai.RoleUser, + Content: []*ai.Part{ai.NewTextPart("Test")}, + }, + }, + Output: &ai.ModelOutputConfig{ + Format: "json", + Schema: nil, + }, + } + + // Create payload + var payload any + if isChatModel { + payload = ollamaChatRequest{ + Messages: []*ollamaMessage{{Role: "user", Content: "Test"}}, + Model: "test-model", + Stream: false, + } + } else { + payload = ollamaModelRequest{ + Model: "test-model", + Prompt: "Test", + Stream: false, + } + } + + // Apply schema logic + if input.Output != nil { + if input.Output.Schema != nil && len(input.Output.Schema) > 0 { + schemaJSON, err := json.Marshal(input.Output.Schema) + if err != nil { + return false + } + + if isChatModel { + chatReq := payload.(ollamaChatRequest) + chatReq.Format = string(schemaJSON) + payload = chatReq + } else { + modelReq := payload.(ollamaModelRequest) + modelReq.Format = string(schemaJSON) + payload = modelReq + } + } else if input.Output.Format == "json" { + if isChatModel { + chatReq := payload.(ollamaChatRequest) + chatReq.Format = "json" + payload = chatReq + } else { + modelReq := payload.(ollamaModelRequest) + modelReq.Format = "json" + payload = modelReq + } + } + } + + // Verify format field is "json" + var gotFormat string + if isChatModel { + gotFormat = payload.(ollamaChatRequest).Format + } else { + gotFormat = payload.(ollamaModelRequest).Format + } + + return gotFormat == "json" + }, + genModelType(), + )) + + properties.TestingRun(t) +} + +// Feature: ollama-structured-output, Property 5: Schema Serialization Error Handling +// **Validates: Requirements 3.1, 3.2, 6.2, 6.4** +func TestProperty5_SchemaSerializationErrorHandling(t *testing.T) { + // Note: This test verifies that the error handling logic is correct + // We can't easily generate schemas that fail JSON marshaling in gopter + // So we test the error message format instead + + parameters := gopter.DefaultTestParameters() + parameters.MinSuccessfulTests = 100 + properties := gopter.NewProperties(parameters) + + properties.Property("schema serialization errors contain expected message format", prop.ForAll( + func(errMsg string) bool { + // Simulate the error format from generator.generate() + expectedErr := fmt.Errorf("failed to serialize output schema: %v", errMsg) + actualMsg := expectedErr.Error() + + // Verify error message contains expected prefix + return contains(actualMsg, "failed to serialize output schema") + }, + gen.AnyString(), + )) + + properties.TestingRun(t) +} + +// Feature: ollama-structured-output, Property 6: Structured Response Content Preservation +// **Validates: Requirements 4.2** +func TestProperty6_StructuredResponseContentPreservation(t *testing.T) { + parameters := gopter.DefaultTestParameters() + parameters.MinSuccessfulTests = 100 + properties := gopter.NewProperties(parameters) + + properties.Property("structured responses are preserved in Message.Content", prop.ForAll( + func(schema map[string]any, modelType string) bool { + isChatModel := modelType == "chat" + + // Generate a mock structured response based on schema + structuredData := map[string]any{ + "name": "Test User", + "age": 30, + } + structuredJSON, err := json.Marshal(structuredData) + if err != nil { + return false + } + + // Create mock response + var responseJSON string + if isChatModel { + responseJSON = fmt.Sprintf(`{ + "model": "test-model", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": %q + } + }`, string(structuredJSON)) + } else { + responseJSON = fmt.Sprintf(`{ + "model": "test-model", + "created_at": "2024-01-01T00:00:00Z", + "response": %q + }`, string(structuredJSON)) + } + + // Parse response + var response *ai.ModelResponse + if isChatModel { + response, err = translateChatResponse([]byte(responseJSON)) + } else { + response, err = translateModelResponse([]byte(responseJSON)) + } + + if err != nil { + return false + } + + // Verify content is preserved + if len(response.Message.Content) != 1 { + return false + } + + if !response.Message.Content[0].IsText() { + return false + } + + // Verify content is valid JSON + var parsedData map[string]any + if err := json.Unmarshal([]byte(response.Message.Content[0].Text), &parsedData); err != nil { + return false + } + + return true + }, + genValidSchema(), + genModelType(), + )) + + properties.TestingRun(t) +} + +// Feature: ollama-structured-output, Property 7: Backward Compatibility Preservation +// **Validates: Requirements 4.3** +func TestProperty7_BackwardCompatibilityPreservation(t *testing.T) { + parameters := gopter.DefaultTestParameters() + parameters.MinSuccessfulTests = 100 + properties := gopter.NewProperties(parameters) + + properties.Property("requests without schemas behave identically to pre-structured-output", prop.ForAll( + func(modelType string, hasOutput bool) bool { + isChatModel := modelType == "chat" + + // Create request without schema + var input *ai.ModelRequest + if hasOutput { + input = &ai.ModelRequest{ + Messages: []*ai.Message{ + { + Role: ai.RoleUser, + Content: []*ai.Part{ai.NewTextPart("Test")}, + }, + }, + Output: &ai.ModelOutputConfig{ + Schema: nil, + }, + } + } else { + input = &ai.ModelRequest{ + Messages: []*ai.Message{ + { + Role: ai.RoleUser, + Content: []*ai.Part{ai.NewTextPart("Test")}, + }, + }, + Output: nil, + } + } + + // Create payload + var payload any + if isChatModel { + payload = ollamaChatRequest{ + Messages: []*ollamaMessage{{Role: "user", Content: "Test"}}, + Model: "test-model", + Stream: false, + } + } else { + payload = ollamaModelRequest{ + Model: "test-model", + Prompt: "Test", + Stream: false, + } + } + + // Apply schema logic (should not modify payload) + if input.Output != nil && input.Output.Schema != nil && len(input.Output.Schema) > 0 { + schemaJSON, err := json.Marshal(input.Output.Schema) + if err != nil { + return false + } + + if isChatModel { + chatReq := payload.(ollamaChatRequest) + chatReq.Format = string(schemaJSON) + payload = chatReq + } else { + modelReq := payload.(ollamaModelRequest) + modelReq.Format = string(schemaJSON) + payload = modelReq + } + } + + // Verify format field is empty (backward compatible) + var gotFormat string + if isChatModel { + gotFormat = payload.(ollamaChatRequest).Format + } else { + gotFormat = payload.(ollamaModelRequest).Format + } + + return gotFormat == "" + }, + genModelType(), + gen.Bool(), + )) + + properties.TestingRun(t) +} + +// Feature: ollama-structured-output, Property 8: Streaming Format Parameter Inclusion +// **Validates: Requirements 5.1** +func TestProperty8_StreamingFormatParameterInclusion(t *testing.T) { + parameters := gopter.DefaultTestParameters() + parameters.MinSuccessfulTests = 100 + properties := gopter.NewProperties(parameters) + + properties.Property("streaming requests include format parameter identically to non-streaming", prop.ForAll( + func(schema map[string]any, modelType string, isStreaming bool) bool { + isChatModel := modelType == "chat" + + // Create request with schema + input := &ai.ModelRequest{ + Messages: []*ai.Message{ + { + Role: ai.RoleUser, + Content: []*ai.Part{ai.NewTextPart("Test")}, + }, + }, + Output: &ai.ModelOutputConfig{ + Schema: schema, + }, + } + + // Create payload + var payload any + if isChatModel { + payload = ollamaChatRequest{ + Messages: []*ollamaMessage{{Role: "user", Content: "Test"}}, + Model: "test-model", + Stream: isStreaming, + } + } else { + payload = ollamaModelRequest{ + Model: "test-model", + Prompt: "Test", + Stream: isStreaming, + } + } + + // Apply schema logic + if input.Output != nil && input.Output.Schema != nil && len(input.Output.Schema) > 0 { + schemaJSON, err := json.Marshal(input.Output.Schema) + if err != nil { + return false + } + + if isChatModel { + chatReq := payload.(ollamaChatRequest) + chatReq.Format = string(schemaJSON) + payload = chatReq + } else { + modelReq := payload.(ollamaModelRequest) + modelReq.Format = string(schemaJSON) + payload = modelReq + } + } + + // Verify format field is set regardless of streaming + var gotFormat string + if isChatModel { + gotFormat = payload.(ollamaChatRequest).Format + } else { + gotFormat = payload.(ollamaModelRequest).Format + } + + // Format should be set and valid JSON + if gotFormat == "" { + return false + } + + var unmarshaled map[string]any + return json.Unmarshal([]byte(gotFormat), &unmarshaled) == nil + }, + genValidSchema(), + genModelType(), + gen.Bool(), + )) + + properties.TestingRun(t) +} + +// Feature: ollama-structured-output, Property 9: Streaming Response Completeness +// **Validates: Requirements 5.3, 5.4** +func TestProperty9_StreamingResponseCompleteness(t *testing.T) { + parameters := gopter.DefaultTestParameters() + parameters.MinSuccessfulTests = 100 + properties := gopter.NewProperties(parameters) + + properties.Property("streaming responses merge to complete structured output", prop.ForAll( + func(modelType string) bool { + isChatModel := modelType == "chat" + + // Create chunks that form a complete JSON object + var chunks []string + if isChatModel { + chunks = []string{ + `{"model": "test", "created_at": "2024-01-01T00:00:00Z", "message": {"role": "assistant", "content": "{"}}`, + `{"model": "test", "created_at": "2024-01-01T00:00:00Z", "message": {"role": "assistant", "content": "\"name\""}}`, + `{"model": "test", "created_at": "2024-01-01T00:00:00Z", "message": {"role": "assistant", "content": ":"}}`, + `{"model": "test", "created_at": "2024-01-01T00:00:00Z", "message": {"role": "assistant", "content": "\"Test\""}}`, + `{"model": "test", "created_at": "2024-01-01T00:00:00Z", "message": {"role": "assistant", "content": "}"}}`, + } + } else { + chunks = []string{ + `{"model": "test", "created_at": "2024-01-01T00:00:00Z", "response": "{"}`, + `{"model": "test", "created_at": "2024-01-01T00:00:00Z", "response": "\"name\""}`, + `{"model": "test", "created_at": "2024-01-01T00:00:00Z", "response": ":"}`, + `{"model": "test", "created_at": "2024-01-01T00:00:00Z", "response": "\"Test\""}`, + `{"model": "test", "created_at": "2024-01-01T00:00:00Z", "response": "}"}`, + } + } + + // Parse chunks + var mergedContent string + for _, chunkJSON := range chunks { + var chunk *ai.ModelResponseChunk + var err error + + if isChatModel { + chunk, err = translateChatChunk(chunkJSON) + } else { + chunk, err = translateGenerateChunk(chunkJSON) + } + + if err != nil { + return false + } + + for _, part := range chunk.Content { + if part.IsText() { + mergedContent += part.Text + } + } + } + + // Verify merged content is complete and valid JSON + expectedJSON := `{"name":"Test"}` + if mergedContent != expectedJSON { + return false + } + + var jsonData map[string]any + return json.Unmarshal([]byte(mergedContent), &jsonData) == nil + }, + genModelType(), + )) + + properties.TestingRun(t) +} + +// Feature: ollama-structured-output, Property 10: Non-Conforming Response Handling +// **Validates: Requirements 6.3** +func TestProperty10_NonConformingResponseHandling(t *testing.T) { + parameters := gopter.DefaultTestParameters() + parameters.MinSuccessfulTests = 100 + properties := gopter.NewProperties(parameters) + + properties.Property("non-conforming responses are parsed without validation errors", prop.ForAll( + func(modelType string) bool { + isChatModel := modelType == "chat" + + // Create a response that doesn't conform to a hypothetical schema + // (e.g., schema expects {name: string, age: number} but response has different structure) + nonConformingJSON := `{"status":"ok","code":200}` + + var responseJSON string + if isChatModel { + responseJSON = fmt.Sprintf(`{ + "model": "test-model", + "created_at": "2024-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": %q + } + }`, nonConformingJSON) + } else { + responseJSON = fmt.Sprintf(`{ + "model": "test-model", + "created_at": "2024-01-01T00:00:00Z", + "response": %q + }`, nonConformingJSON) + } + + // Parse response + var response *ai.ModelResponse + var err error + + if isChatModel { + response, err = translateChatResponse([]byte(responseJSON)) + } else { + response, err = translateModelResponse([]byte(responseJSON)) + } + + // Should not error (no client-side validation) + if err != nil { + return false + } + + // Verify content is preserved as-is + if len(response.Message.Content) != 1 { + return false + } + + if !response.Message.Content[0].IsText() { + return false + } + + // Content should match what Ollama returned + if response.Message.Content[0].Text != nonConformingJSON { + return false + } + + // Verify it's valid JSON (even if it doesn't match schema) + var jsonData map[string]any + return json.Unmarshal([]byte(response.Message.Content[0].Text), &jsonData) == nil + }, + genModelType(), + )) + + properties.TestingRun(t) +} diff --git a/go/samples/ollama-json-mode/main.go b/go/samples/ollama-json-mode/main.go new file mode 100644 index 0000000000..27843cfa67 --- /dev/null +++ b/go/samples/ollama-json-mode/main.go @@ -0,0 +1,102 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + + "github.com/firebase/genkit/go/ai" + "github.com/firebase/genkit/go/genkit" + "github.com/firebase/genkit/go/plugins/ollama" +) + +func main() { + ctx := context.Background() + + // Initialize Genkit with the Ollama plugin + ollamaPlugin := &ollama.Ollama{ + ServerAddress: "http://localhost:11434", // Default Ollama server address + Timeout: 60, // Response timeout in seconds + } + + g := genkit.Init(ctx, genkit.WithPlugins(ollamaPlugin)) + + // Define the Ollama model + model := ollamaPlugin.DefineModel(g, + ollama.ModelDefinition{ + Name: "llama3.1", // Choose an appropriate model + Type: "chat", + }, + nil) + + fmt.Println("=== Ollama Schema-less JSON Mode Example ===\n") + fmt.Println("This example demonstrates how to request generic JSON output") + fmt.Println("without specifying a schema. The model will return valid JSON") + fmt.Println("but is not constrained to any specific structure.\n") + + // Example: Schema-less JSON mode + fmt.Println("--- Requesting Generic JSON Output ---") + fmt.Println("Setting format to 'json' without a schema...\n") + + // Request generic JSON output by setting Format to "json" without a Schema + // The model will return valid JSON, but the structure is determined by the prompt + resp, err := genkit.Generate(ctx, g, + ai.WithModel(model), + ai.WithMessages( + ai.NewUserTextMessage(`List 3 popular programming languages with their key features. +Return the response as JSON with a "languages" array where each item has "name" and "features" fields.`), + ), + ai.WithOutputConfig(&ai.ModelOutputConfig{ + Format: "json", // Request JSON output without schema constraints + // Schema is nil/empty, so Ollama returns generic JSON + }), + ) + if err != nil { + log.Fatalf("Error generating JSON output: %v", err) + } + + // The response text contains valid JSON + responseText := resp.Text() + fmt.Printf("Raw JSON response:\n%s\n\n", responseText) + + // Parse the JSON response + // Since we don't have a predefined schema, we use a generic map structure + var result map[string]any + if err := json.Unmarshal([]byte(responseText), &result); err != nil { + log.Fatalf("Failed to parse JSON response: %v", err) + } + + // Access the data dynamically + fmt.Println("Parsed JSON data:") + prettyJSON, err := json.MarshalIndent(result, "", " ") + if err != nil { + log.Fatalf("Failed to format JSON: %v", err) + } + fmt.Println(string(prettyJSON)) + + fmt.Println("\n✓ Successfully received and parsed generic JSON output!") + fmt.Println("\nWhen to use schema-less JSON mode:") + fmt.Println(" • You want valid JSON but don't need strict structure enforcement") + fmt.Println(" • The output structure varies based on the prompt") + fmt.Println(" • You're prototyping and want flexibility") + fmt.Println(" • You'll handle dynamic JSON structures in your application") + fmt.Println("\nWhen to use schema-based output instead:") + fmt.Println(" • You need guaranteed structure for reliable parsing") + fmt.Println(" • You want type-safe Go structs") + fmt.Println(" • You're building production systems with strict data requirements") +} diff --git a/go/samples/ollama-structured/main.go b/go/samples/ollama-structured/main.go new file mode 100644 index 0000000000..33017b42e6 --- /dev/null +++ b/go/samples/ollama-structured/main.go @@ -0,0 +1,131 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + + "github.com/firebase/genkit/go/ai" + "github.com/firebase/genkit/go/genkit" + "github.com/firebase/genkit/go/plugins/ollama" + "github.com/invopop/jsonschema" +) + +// Person represents a person with basic information +type Person struct { + Name string `json:"name" jsonschema:"required,description=Full name of the person"` + Age int `json:"age" jsonschema:"required,description=Age in years"` + Occupation string `json:"occupation" jsonschema:"required,description=Current job or profession"` + Hobbies []string `json:"hobbies" jsonschema:"required,description=List of hobbies and interests"` + City string `json:"city" jsonschema:"required,description=City of residence"` + Email string `json:"email,omitempty" jsonschema:"description=Email address if available"` +} + +func main() { + ctx := context.Background() + + // Initialize Genkit with the Ollama plugin + ollamaPlugin := &ollama.Ollama{ + ServerAddress: "http://localhost:11434", // Default Ollama server address + Timeout: 60, // Response timeout in seconds + } + + g := genkit.Init(ctx, genkit.WithPlugins(ollamaPlugin)) + + // Define the Ollama model + model := ollamaPlugin.DefineModel(g, + ollama.ModelDefinition{ + Name: "llama3.1", // Choose an appropriate model + Type: "chat", + }, + nil) + + fmt.Println("=== Ollama Structured Output Example ===\n") + fmt.Println("This example demonstrates how to use JSON schemas with Ollama") + fmt.Println("to get structured, type-safe responses from language models.\n") + + // Example 1: Schema-based structured output + fmt.Println("--- Example 1: Schema-Based Structured Output ---") + fmt.Println("Using a JSON schema to constrain the model's response...\n") + + // Step 1: Define a Go struct with the desired output structure + // (Already defined above as Person struct) + + // Step 2: Convert the Go struct to a JSON schema + // We use the jsonschema library to generate a schema from the struct + reflector := jsonschema.Reflector{ + AllowAdditionalProperties: false, + DoNotReference: true, + } + schema := reflector.Reflect(&Person{}) + schemaBytes, err := json.Marshal(schema) + if err != nil { + log.Fatalf("Failed to marshal schema: %v", err) + } + + // Convert the schema to a map for use with Genkit + var schemaMap map[string]any + if err := json.Unmarshal(schemaBytes, &schemaMap); err != nil { + log.Fatalf("Failed to unmarshal schema: %v", err) + } + + // Step 3: Use the schema with ModelRequest + // The Output field tells Genkit to request structured output from Ollama + resp, err := genkit.Generate(ctx, g, + ai.WithModel(model), + ai.WithMessages( + ai.NewUserTextMessage("Generate information about a fictional software engineer named Alice who lives in San Francisco."), + ), + ai.WithOutputConfig(&ai.ModelOutputConfig{ + Format: "json", + Schema: schemaMap, + }), + ) + if err != nil { + log.Fatalf("Error generating structured output: %v", err) + } + + // Step 4: Parse the structured response + // The response text contains JSON that conforms to our schema + responseText := resp.Text() + fmt.Printf("Raw JSON response:\n%s\n\n", responseText) + + // Parse the JSON into our Person struct + var person Person + if err := json.Unmarshal([]byte(responseText), &person); err != nil { + log.Fatalf("Failed to parse response: %v", err) + } + + // Now we have type-safe access to the structured data + fmt.Printf("Parsed Person:\n") + fmt.Printf(" Name: %s\n", person.Name) + fmt.Printf(" Age: %d\n", person.Age) + fmt.Printf(" Occupation: %s\n", person.Occupation) + fmt.Printf(" City: %s\n", person.City) + fmt.Printf(" Hobbies: %v\n", person.Hobbies) + if person.Email != "" { + fmt.Printf(" Email: %s\n", person.Email) + } + + fmt.Println("\n✓ Successfully received and parsed structured output!") + fmt.Println("\nKey Benefits:") + fmt.Println(" • Guaranteed JSON structure matching your schema") + fmt.Println(" • Type-safe parsing into Go structs") + fmt.Println(" • No need for complex prompt engineering") + fmt.Println(" • Reliable data extraction for downstream processing") +}